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 );
27 use C4::Circulation qw( ReturnLostItem CanBookBeRenewed AddRenewal );
29 use C4::Log qw( logaction );
30 use C4::Stats qw( UpdateStats );
31 use C4::Overdues qw(GetFine);
34 use Koha::Account::Lines;
35 use Koha::Account::Offsets;
36 use Koha::Account::DebitTypes;
37 use Koha::DateUtils qw( dt_from_string );
39 use Koha::Exceptions::Account;
43 Koha::Accounts - Module for managing payments and fees for patrons
48 my ( $class, $params ) = @_;
50 Carp::croak("No patron id passed in!") unless $params->{patron_id};
52 return bless( $params, $class );
57 This method allows payments to be made against fees/fines
59 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
63 description => $description,
64 library_id => $branchcode,
65 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
66 credit_type => $type, # credit_type_code code
67 offset_type => $offset_type, # offset type code
74 my ( $self, $params ) = @_;
76 my $amount = $params->{amount};
77 my $description = $params->{description};
78 my $note = $params->{note} || q{};
79 my $library_id = $params->{library_id};
80 my $lines = $params->{lines};
81 my $type = $params->{type} || 'PAYMENT';
82 my $payment_type = $params->{payment_type} || undef;
83 my $credit_type = $params->{credit_type};
84 my $offset_type = $params->{offset_type} || $type eq 'WRITEOFF' ? 'Writeoff' : 'Payment';
85 my $cash_register = $params->{cash_register};
87 my $userenv = C4::Context->userenv;
89 my $patron = Koha::Patrons->find( $self->{patron_id} );
91 my $manager_id = $userenv ? $userenv->{number} : 0;
92 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
93 Koha::Exceptions::Account::RegisterRequired->throw()
94 if ( C4::Context->preference("UseCashRegisters")
95 && defined($payment_type)
96 && ( $payment_type eq 'CASH' )
97 && !defined($cash_register) );
99 my @fines_paid; # List of account lines paid on with this payment
100 # Item numbers that have had a fine paid where the line has a accounttype
101 # of OVERDUE and a status of UNRETURNED. We might want to try and renew
103 my $overdue_unreturned = {};
105 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
106 $balance_remaining ||= 0;
110 # We were passed a specific line to pay
111 foreach my $fine ( @$lines ) {
113 $fine->amountoutstanding > $balance_remaining
115 : $fine->amountoutstanding;
117 my $old_amountoutstanding = $fine->amountoutstanding;
118 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
119 $fine->amountoutstanding($new_amountoutstanding)->store();
120 $balance_remaining = $balance_remaining - $amount_to_pay;
122 # If we need to make a note of the item associated with this line,
123 # in order that we can potentially renew it, do so.
125 $new_amountoutstanding == 0 &&
126 $fine->accounttype &&
127 $fine->accounttype eq 'OVERDUE' &&
129 $fine->status eq 'UNRETURNED'
131 $overdue_unreturned->{$fine->itemnumber} = $fine;
134 # Same logic exists in Koha::Account::Line::apply
135 if ( $new_amountoutstanding == 0
137 && $fine->debit_type_code
138 && ( $fine->debit_type_code eq 'LOST' ) )
140 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
143 my $account_offset = Koha::Account::Offset->new(
145 debit_id => $fine->id,
146 type => $offset_type,
147 amount => $amount_to_pay * -1,
150 push( @account_offsets, $account_offset );
152 if ( C4::Context->preference("FinesLog") ) {
158 action => 'fee_payment',
159 borrowernumber => $fine->borrowernumber,
160 old_amountoutstanding => $old_amountoutstanding,
161 new_amountoutstanding => 0,
162 amount_paid => $old_amountoutstanding,
163 accountlines_id => $fine->id,
164 manager_id => $manager_id,
170 push( @fines_paid, $fine->id );
174 # Were not passed a specific line to pay, or the payment was for more
175 # than the what was owed on the given line. In that case pay down other
176 # lines with remaining balance.
177 my @outstanding_fines;
178 @outstanding_fines = $self->lines->search(
180 amountoutstanding => { '>' => 0 },
182 ) if $balance_remaining > 0;
184 foreach my $fine (@outstanding_fines) {
186 $fine->amountoutstanding > $balance_remaining
188 : $fine->amountoutstanding;
190 my $old_amountoutstanding = $fine->amountoutstanding;
191 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
194 # If we need to make a note of the item associated with this line,
195 # in order that we can potentially renew it, do so.
197 $old_amountoutstanding - $amount_to_pay == 0 &&
198 $fine->accounttype &&
199 $fine->accounttype eq 'OVERDUE' &&
201 $fine->status eq 'UNRETURNED'
203 $overdue_unreturned->{$fine->itemnumber} = $fine;
206 if ( $fine->amountoutstanding == 0
208 && $fine->debit_type_code
209 && ( $fine->debit_type_code eq 'LOST' ) )
211 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
214 my $account_offset = Koha::Account::Offset->new(
216 debit_id => $fine->id,
217 type => $offset_type,
218 amount => $amount_to_pay * -1,
221 push( @account_offsets, $account_offset );
223 if ( C4::Context->preference("FinesLog") ) {
229 action => "fee_$type",
230 borrowernumber => $fine->borrowernumber,
231 old_amountoutstanding => $old_amountoutstanding,
232 new_amountoutstanding => $fine->amountoutstanding,
233 amount_paid => $amount_to_pay,
234 accountlines_id => $fine->id,
235 manager_id => $manager_id,
241 push( @fines_paid, $fine->id );
244 $balance_remaining = $balance_remaining - $amount_to_pay;
245 last unless $balance_remaining > 0;
253 $description ||= $type eq 'WRITEOFF' ? 'Writeoff' : q{};
255 my $payment = Koha::Account::Line->new(
257 borrowernumber => $self->{patron_id},
258 date => dt_from_string(),
259 amount => 0 - $amount,
260 description => $description,
261 credit_type_code => $credit_type,
262 payment_type => $payment_type,
263 amountoutstanding => 0 - $balance_remaining,
264 manager_id => $manager_id,
265 interface => $interface,
266 branchcode => $library_id,
267 register_id => $cash_register,
272 foreach my $o ( @account_offsets ) {
273 $o->credit_id( $payment->id() );
279 branch => $library_id,
282 borrowernumber => $self->{patron_id},
286 # If we have overdue unreturned items that have had payments made
287 # against them, check whether the balance on those items is now zero
288 # and, if the syspref is set, renew them
289 # Same logic exists in Koha::Account::Line::apply
291 C4::Context->preference('RenewAccruingItemWhenPaid') &&
292 keys %{$overdue_unreturned}
294 foreach my $itemnumber (keys %{$overdue_unreturned}) {
295 # Only do something if this item has no fines left on it
296 my $fine = C4::Overdues::GetFine( $itemnumber, $self->{patron_id} );
297 next if $fine && $fine > 0;
299 my ( $renew_ok, $error ) =
300 C4::Circulation::CanBookBeRenewed(
301 $self->{patron_id}, $itemnumber
304 C4::Circulation::AddRenewal(
316 if ( C4::Context->preference("FinesLog") ) {
322 action => "create_$type",
323 borrowernumber => $self->{patron_id},
324 amount => 0 - $amount,
325 amountoutstanding => 0 - $balance_remaining,
326 credit_type_code => $credit_type,
327 accountlines_paid => \@fines_paid,
328 manager_id => $manager_id,
335 if ( C4::Context->preference('UseEmailReceipts') ) {
337 my $letter = C4::Letters::GetPreparedLetter(
338 module => 'circulation',
339 letter_code => uc("ACCOUNT_$type"),
340 message_transport_type => 'email',
341 lang => $patron->lang,
343 borrowers => $self->{patron_id},
344 branches => $library_id,
348 offsets => \@account_offsets,
353 C4::Letters::EnqueueLetter(
356 borrowernumber => $self->{patron_id},
357 message_transport_type => 'email',
359 ) or warn "can't enqueue letter $letter";
368 This method allows adding credits to a patron's account
370 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
373 description => $description,
376 interface => $interface,
377 library_id => $library_id,
378 payment_type => $payment_type,
379 type => $credit_type,
384 $credit_type can be any of:
395 my ( $self, $params ) = @_;
397 # check for mandatory params
398 my @mandatory = ( 'interface', 'amount' );
399 for my $param (@mandatory) {
400 unless ( defined( $params->{$param} ) ) {
401 Koha::Exceptions::MissingParameter->throw(
402 error => "The $param parameter is mandatory" );
406 # amount should always be passed as a positive value
407 my $amount = $params->{amount} * -1;
408 unless ( $amount < 0 ) {
409 Koha::Exceptions::Account::AmountNotPositive->throw(
410 error => 'Debit amount passed is not positive' );
413 my $description = $params->{description} // q{};
414 my $note = $params->{note} // q{};
415 my $user_id = $params->{user_id};
416 my $interface = $params->{interface};
417 my $library_id = $params->{library_id};
418 my $cash_register = $params->{cash_register};
419 my $payment_type = $params->{payment_type};
420 my $credit_type = $params->{type} || 'PAYMENT';
421 my $item_id = $params->{item_id};
423 Koha::Exceptions::Account::RegisterRequired->throw()
424 if ( C4::Context->preference("UseCashRegisters")
425 && defined($payment_type)
426 && ( $payment_type eq 'CASH' )
427 && !defined($cash_register) );
430 my $schema = Koha::Database->new->schema;
435 # Insert the account line
436 $line = Koha::Account::Line->new(
438 borrowernumber => $self->{patron_id},
441 description => $description,
442 credit_type_code => $credit_type,
443 amountoutstanding => $amount,
444 payment_type => $payment_type,
446 manager_id => $user_id,
447 interface => $interface,
448 branchcode => $library_id,
449 register_id => $cash_register,
450 itemnumber => $item_id,
454 # Record the account offset
455 my $account_offset = Koha::Account::Offset->new(
457 credit_id => $line->id,
458 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
465 branch => $library_id,
466 type => lc($credit_type),
468 borrowernumber => $self->{patron_id},
470 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
472 if ( C4::Context->preference("FinesLog") ) {
478 action => "create_$credit_type",
479 borrowernumber => $self->{patron_id},
481 description => $description,
482 amountoutstanding => $amount,
483 credit_type_code => $credit_type,
485 itemnumber => $item_id,
486 manager_id => $user_id,
487 branchcode => $library_id,
497 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
498 if ( $_->broken_fk eq 'credit_type_code' ) {
499 Koha::Exceptions::Account::UnrecognisedType->throw(
500 error => 'Type of credit not recognised' );
513 This method allows adding debits to a patron's account
515 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
518 description => $description,
521 interface => $interface,
522 library_id => $library_id,
525 issue_id => $issue_id
529 $debit_type can be any of:
548 my ( $self, $params ) = @_;
550 # check for mandatory params
551 my @mandatory = ( 'interface', 'type', 'amount' );
552 for my $param (@mandatory) {
553 unless ( defined( $params->{$param} ) ) {
554 Koha::Exceptions::MissingParameter->throw(
555 error => "The $param parameter is mandatory" );
559 # amount should always be a positive value
560 my $amount = $params->{amount};
561 unless ( $amount > 0 ) {
562 Koha::Exceptions::Account::AmountNotPositive->throw(
563 error => 'Debit amount passed is not positive' );
566 my $description = $params->{description} // q{};
567 my $note = $params->{note} // q{};
568 my $user_id = $params->{user_id};
569 my $interface = $params->{interface};
570 my $library_id = $params->{library_id};
571 my $debit_type = $params->{type};
572 my $item_id = $params->{item_id};
573 my $issue_id = $params->{issue_id};
574 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
577 my $schema = Koha::Database->new->schema;
582 # Insert the account line
583 $line = Koha::Account::Line->new(
585 borrowernumber => $self->{patron_id},
588 description => $description,
589 debit_type_code => $debit_type,
590 amountoutstanding => $amount,
591 payment_type => undef,
593 manager_id => $user_id,
594 interface => $interface,
595 itemnumber => $item_id,
596 issue_id => $issue_id,
597 branchcode => $library_id,
599 $debit_type eq 'OVERDUE'
600 ? ( status => 'UNRETURNED' )
606 # Record the account offset
607 my $account_offset = Koha::Account::Offset->new(
609 debit_id => $line->id,
610 type => $offset_type,
615 if ( C4::Context->preference("FinesLog") ) {
621 action => "create_$debit_type",
622 borrowernumber => $self->{patron_id},
624 description => $description,
625 amountoutstanding => $amount,
626 debit_type_code => $debit_type,
628 itemnumber => $item_id,
629 manager_id => $user_id,
639 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
640 if ( $_->broken_fk eq 'debit_type_code' ) {
641 Koha::Exceptions::Account::UnrecognisedType->throw(
642 error => 'Type of debit not recognised' );
655 my $balance = $self->balance
657 Return the balance (sum of amountoutstanding columns)
663 return $self->lines->total_outstanding;
666 =head3 outstanding_debits
668 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
670 It returns the debit lines with outstanding amounts for the patron.
672 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
673 return a list of Koha::Account::Line objects.
677 sub outstanding_debits {
680 return $self->lines->search(
682 amount => { '>' => 0 },
683 amountoutstanding => { '>' => 0 }
688 =head3 outstanding_credits
690 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
692 It returns the credit lines with outstanding amounts for the patron.
694 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
695 return a list of Koha::Account::Line objects.
699 sub outstanding_credits {
702 return $self->lines->search(
704 amount => { '<' => 0 },
705 amountoutstanding => { '<' => 0 }
710 =head3 non_issues_charges
712 my $non_issues_charges = $self->non_issues_charges
714 Calculates amount immediately owing by the patron - non-issue charges.
716 Charges exempt from non-issue are:
717 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
718 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
719 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
723 sub non_issues_charges {
726 #NOTE: With bug 23049 these preferences could be moved to being attached
727 #to individual debit types to give more flexability and specificity.
729 push @not_fines, 'RESERVE'
730 unless C4::Context->preference('HoldsInNoissuesCharge');
731 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
732 unless C4::Context->preference('RentalsInNoissuesCharge');
733 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
734 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
735 push @not_fines, @man_inv;
738 return $self->lines->search(
740 debit_type_code => { -not_in => \@not_fines }
742 )->total_outstanding;
747 my $lines = $self->lines;
749 Return all credits and debits for the user, outstanding or otherwise
756 return Koha::Account::Lines->search(
758 borrowernumber => $self->{patron_id},
763 =head3 reconcile_balance
765 $account->reconcile_balance();
767 Find outstanding credits and use them to pay outstanding debits.
768 Currently, this implicitly uses the 'First In First Out' rule for
769 applying credits against debits.
773 sub reconcile_balance {
776 my $outstanding_debits = $self->outstanding_debits;
777 my $outstanding_credits = $self->outstanding_credits;
779 while ( $outstanding_debits->total_outstanding > 0
780 and my $credit = $outstanding_credits->next )
782 # there's both outstanding debits and credits
783 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
785 $outstanding_debits = $self->outstanding_debits;
801 'CREDIT' => 'Manual Credit',
802 'FORGIVEN' => 'Writeoff',
803 'LOST_FOUND' => 'Lost Item Found',
804 'PAYMENT' => 'Payment',
805 'WRITEOFF' => 'Writeoff',
806 'ACCOUNT' => 'Account Fee',
807 'ACCOUNT_RENEW' => 'Account Fee',
808 'RESERVE' => 'Reserve Fee',
809 'PROCESSING' => 'Processing Fee',
810 'LOST' => 'Lost Item',
811 'RENT' => 'Rental Fee',
812 'RENT_DAILY' => 'Rental Fee',
813 'RENT_RENEW' => 'Rental Fee',
814 'RENT_DAILY_RENEW' => 'Rental Fee',
815 'OVERDUE' => 'OVERDUE',
816 'RESERVE_EXPIRED' => 'Hold Expired'
823 Kyle M Hall <kyle.m.hall@gmail.com>
824 Tomás Cohen Arazi <tomascohen@gmail.com>
825 Martin Renvoize <martin.renvoize@ptfs-europe.com>