3 # Copyright 2016 ByWater Solutions
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
24 use List::MoreUtils qw( uniq );
26 use C4::Circulation qw( ReturnLostItem );
28 use C4::Log qw( logaction );
29 use C4::Stats qw( UpdateStats );
32 use Koha::Account::Lines;
33 use Koha::Account::Offsets;
34 use Koha::DateUtils qw( dt_from_string );
36 use Koha::Exceptions::Account;
40 Koha::Accounts - Module for managing payments and fees for patrons
45 my ( $class, $params ) = @_;
47 Carp::croak("No patron id passed in!") unless $params->{patron_id};
49 return bless( $params, $class );
54 This method allows payments to be made against fees/fines
56 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
61 description => $description,
62 library_id => $branchcode,
63 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
64 account_type => $type, # accounttype code
65 offset_type => $offset_type, # offset type code
72 my ( $self, $params ) = @_;
74 my $amount = $params->{amount};
75 my $sip = $params->{sip};
76 my $description = $params->{description};
77 my $note = $params->{note} || q{};
78 my $library_id = $params->{library_id};
79 my $lines = $params->{lines};
80 my $type = $params->{type} || 'payment';
81 my $payment_type = $params->{payment_type} || undef;
82 my $account_type = $params->{account_type};
83 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
85 my $userenv = C4::Context->userenv;
87 my $patron = Koha::Patrons->find( $self->{patron_id} );
89 my $manager_id = $userenv ? $userenv->{number} : 0;
90 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
92 my @fines_paid; # List of account lines paid on with this payment
94 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
95 $balance_remaining ||= 0;
99 # We were passed a specific line to pay
100 foreach my $fine ( @$lines ) {
102 $fine->amountoutstanding > $balance_remaining
104 : $fine->amountoutstanding;
106 my $old_amountoutstanding = $fine->amountoutstanding;
107 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
108 $fine->amountoutstanding($new_amountoutstanding)->store();
109 $balance_remaining = $balance_remaining - $amount_to_pay;
111 # Same logic exists in Koha::Account::Line::apply
112 if ( $new_amountoutstanding == 0
114 && $fine->accounttype
115 && ( $fine->accounttype eq 'LOST' ) )
117 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
120 my $account_offset = Koha::Account::Offset->new(
122 debit_id => $fine->id,
123 type => $offset_type,
124 amount => $amount_to_pay * -1,
127 push( @account_offsets, $account_offset );
129 if ( C4::Context->preference("FinesLog") ) {
135 action => 'fee_payment',
136 borrowernumber => $fine->borrowernumber,
137 old_amountoutstanding => $old_amountoutstanding,
138 new_amountoutstanding => 0,
139 amount_paid => $old_amountoutstanding,
140 accountlines_id => $fine->id,
141 manager_id => $manager_id,
147 push( @fines_paid, $fine->id );
151 # Were not passed a specific line to pay, or the payment was for more
152 # than the what was owed on the given line. In that case pay down other
153 # lines with remaining balance.
154 my @outstanding_fines;
155 @outstanding_fines = $self->lines->search(
157 amountoutstanding => { '>' => 0 },
159 ) if $balance_remaining > 0;
161 foreach my $fine (@outstanding_fines) {
163 $fine->amountoutstanding > $balance_remaining
165 : $fine->amountoutstanding;
167 my $old_amountoutstanding = $fine->amountoutstanding;
168 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
171 if ( $fine->amountoutstanding == 0
173 && $fine->accounttype
174 && ( $fine->accounttype eq 'LOST' ) )
176 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
179 my $account_offset = Koha::Account::Offset->new(
181 debit_id => $fine->id,
182 type => $offset_type,
183 amount => $amount_to_pay * -1,
186 push( @account_offsets, $account_offset );
188 if ( C4::Context->preference("FinesLog") ) {
194 action => "fee_$type",
195 borrowernumber => $fine->borrowernumber,
196 old_amountoutstanding => $old_amountoutstanding,
197 new_amountoutstanding => $fine->amountoutstanding,
198 amount_paid => $amount_to_pay,
199 accountlines_id => $fine->id,
200 manager_id => $manager_id,
206 push( @fines_paid, $fine->id );
209 $balance_remaining = $balance_remaining - $amount_to_pay;
210 last unless $balance_remaining > 0;
214 $type eq 'writeoff' ? 'W'
215 : defined($sip) ? "Pay$sip"
218 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
220 my $payment = Koha::Account::Line->new(
222 borrowernumber => $self->{patron_id},
223 date => dt_from_string(),
224 amount => 0 - $amount,
225 description => $description,
226 accounttype => $account_type,
227 payment_type => $payment_type,
228 amountoutstanding => 0 - $balance_remaining,
229 manager_id => $manager_id,
230 interface => $interface,
231 branchcode => $library_id,
236 foreach my $o ( @account_offsets ) {
237 $o->credit_id( $payment->id() );
243 branch => $library_id,
246 borrowernumber => $self->{patron_id},
250 if ( C4::Context->preference("FinesLog") ) {
256 action => "create_$type",
257 borrowernumber => $self->{patron_id},
258 amount => 0 - $amount,
259 amountoutstanding => 0 - $balance_remaining,
260 accounttype => $account_type,
261 accountlines_paid => \@fines_paid,
262 manager_id => $manager_id,
269 if ( C4::Context->preference('UseEmailReceipts') ) {
271 my $letter = C4::Letters::GetPreparedLetter(
272 module => 'circulation',
273 letter_code => uc("ACCOUNT_$type"),
274 message_transport_type => 'email',
275 lang => $patron->lang,
277 borrowers => $self->{patron_id},
278 branches => $self->{library_id},
282 offsets => \@account_offsets,
287 C4::Letters::EnqueueLetter(
290 borrowernumber => $self->{patron_id},
291 message_transport_type => 'email',
293 ) or warn "can't enqueue letter $letter";
302 This method allows adding credits to a patron's account
304 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
307 description => $description,
310 interface => $interface,
311 library_id => $library_id,
313 payment_type => $payment_type,
314 type => $credit_type,
319 $credit_type can be any of:
330 my ( $self, $params ) = @_;
332 # amount is passed as a positive value, but we store credit as negative values
333 my $amount = $params->{amount} * -1;
334 my $description = $params->{description} // q{};
335 my $note = $params->{note} // q{};
336 my $user_id = $params->{user_id};
337 my $interface = $params->{interface};
338 my $library_id = $params->{library_id};
339 my $sip = $params->{sip};
340 my $payment_type = $params->{payment_type};
341 my $type = $params->{type} || 'payment';
342 my $item_id = $params->{item_id};
344 unless ( $interface ) {
345 Koha::Exceptions::MissingParameter->throw(
346 error => 'The interface parameter is mandatory'
350 my $schema = Koha::Database->new->schema;
352 my $account_type = $Koha::Account::account_type_credit->{$type};
353 $account_type .= $sip
362 # Insert the account line
363 $line = Koha::Account::Line->new(
364 { borrowernumber => $self->{patron_id},
367 description => $description,
368 accounttype => $account_type,
369 amountoutstanding => $amount,
370 payment_type => $payment_type,
372 manager_id => $user_id,
373 interface => $interface,
374 branchcode => $library_id,
375 itemnumber => $item_id,
379 # Record the account offset
380 my $account_offset = Koha::Account::Offset->new(
381 { credit_id => $line->id,
382 type => $Koha::Account::offset_type->{$type},
388 { branch => $library_id,
391 borrowernumber => $self->{patron_id},
393 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
395 if ( C4::Context->preference("FinesLog") ) {
400 { action => "create_$type",
401 borrowernumber => $self->{patron_id},
403 description => $description,
404 amountoutstanding => $amount,
405 accounttype => $account_type,
407 itemnumber => $item_id,
408 manager_id => $user_id,
409 branchcode => $library_id,
423 This method allows adding debits to a patron's account
425 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
428 description => $description,
431 interface => $interface,
432 library_id => $library_id,
435 issue_id => $issue_id
439 $debit_type can be any of:
457 my ( $self, $params ) = @_;
459 # amount should always be a positive value
460 my $amount = $params->{amount};
462 unless ( $amount > 0 ) {
463 Koha::Exceptions::Account::AmountNotPositive->throw(
464 error => 'Debit amount passed is not positive'
468 my $description = $params->{description} // q{};
469 my $note = $params->{note} // q{};
470 my $user_id = $params->{user_id};
471 my $interface = $params->{interface};
472 my $library_id = $params->{library_id};
473 my $type = $params->{type};
474 my $item_id = $params->{item_id};
475 my $issue_id = $params->{issue_id};
477 unless ( $interface ) {
478 Koha::Exceptions::MissingParameter->throw(
479 error => 'The interface parameter is mandatory'
483 my $schema = Koha::Database->new->schema;
485 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
486 Koha::Exceptions::Account::UnrecognisedType->throw(
487 error => 'Type of debit not recognised'
491 my $account_type = $Koha::Account::account_type_debit->{$type};
498 # Insert the account line
499 $line = Koha::Account::Line->new(
500 { borrowernumber => $self->{patron_id},
503 description => $description,
504 accounttype => $account_type,
505 amountoutstanding => $amount,
506 payment_type => undef,
508 manager_id => $user_id,
509 interface => $interface,
510 itemnumber => $item_id,
511 issue_id => $issue_id,
512 branchcode => $library_id,
513 ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : ()),
517 # Record the account offset
518 my $account_offset = Koha::Account::Offset->new(
519 { debit_id => $line->id,
520 type => $Koha::Account::offset_type->{$type},
525 if ( C4::Context->preference("FinesLog") ) {
530 { action => "create_$type",
531 borrowernumber => $self->{patron_id},
533 description => $description,
534 amountoutstanding => $amount,
535 accounttype => $account_type,
537 itemnumber => $item_id,
538 manager_id => $user_id,
552 my $balance = $self->balance
554 Return the balance (sum of amountoutstanding columns)
560 return $self->lines->total_outstanding;
563 =head3 outstanding_debits
565 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
567 It returns the debit lines with outstanding amounts for the patron.
569 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
570 return a list of Koha::Account::Line objects.
574 sub outstanding_debits {
577 return $self->lines->search(
579 amount => { '>' => 0 },
580 amountoutstanding => { '>' => 0 }
585 =head3 outstanding_credits
587 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
589 It returns the credit lines with outstanding amounts for the patron.
591 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
592 return a list of Koha::Account::Line objects.
596 sub outstanding_credits {
599 return $self->lines->search(
601 amount => { '<' => 0 },
602 amountoutstanding => { '<' => 0 }
607 =head3 non_issues_charges
609 my $non_issues_charges = $self->non_issues_charges
611 Calculates amount immediately owing by the patron - non-issue charges.
613 Charges exempt from non-issue are:
614 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
615 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
616 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
620 sub non_issues_charges {
623 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
624 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
627 push @not_fines, 'Res'
628 unless C4::Context->preference('HoldsInNoissuesCharge');
629 push @not_fines, 'Rent'
630 unless C4::Context->preference('RentalsInNoissuesCharge');
631 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
632 my $dbh = C4::Context->dbh;
635 $dbh->selectcol_arrayref(q|
636 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
640 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
642 return $self->lines->search(
644 accounttype => { -not_in => \@not_fines }
646 )->total_outstanding;
651 my $lines = $self->lines;
653 Return all credits and debits for the user, outstanding or otherwise
660 return Koha::Account::Lines->search(
662 borrowernumber => $self->{patron_id},
667 =head3 reconcile_balance
669 $account->reconcile_balance();
671 Find outstanding credits and use them to pay outstanding debits.
672 Currently, this implicitly uses the 'First In First Out' rule for
673 applying credits against debits.
677 sub reconcile_balance {
680 my $outstanding_debits = $self->outstanding_debits;
681 my $outstanding_credits = $self->outstanding_credits;
683 while ( $outstanding_debits->total_outstanding > 0
684 and my $credit = $outstanding_credits->next )
686 # there's both outstanding debits and credits
687 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
689 $outstanding_debits = $self->outstanding_debits;
705 'credit' => 'Manual Credit',
706 'forgiven' => 'Writeoff',
707 'lost_item_return' => 'Lost Item',
708 'payment' => 'Payment',
709 'writeoff' => 'Writeoff',
710 'account' => 'Account Fee',
711 'reserve' => 'Reserve Fee',
712 'processing' => 'Processing Fee',
713 'lost_item' => 'Lost Item',
714 'rent' => 'Rental Fee',
715 'rent_daily' => 'Rental Fee',
716 'rent_renew' => 'Rental Fee',
717 'rent_daily_renew' => 'Rental Fee',
718 'overdue' => 'OVERDUE',
719 'manual_debit' => 'Manual Debit',
720 'hold_expired' => 'Hold Expired'
723 =head3 $account_type_credit
727 our $account_type_credit = {
730 'lost_item_return' => 'LOST_RETURN',
735 =head3 $account_type_debit
739 our $account_type_debit = {
741 'overdue' => 'OVERDUE',
742 'lost_item' => 'LOST',
745 'processing' => 'PF',
747 'rent_daily' => 'RENT_DAILY',
748 'rent_renew' => 'RENT_RENEW',
749 'rent_daily_renew' => 'RENT_DAILY_RENEW',
751 'manual_debit' => 'M',
752 'hold_expired' => 'HE'
759 Kyle M Hall <kyle.m.hall@gmail.com>
760 Tomás Cohen Arazi <tomascohen@gmail.com>
761 Martin Renvoize <martin.renvoize@ptfs-europe.com>