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
68 item_id => $itemnumber, # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
75 my ( $self, $params ) = @_;
77 my $amount = $params->{amount};
78 my $description = $params->{description};
79 my $note = $params->{note} || q{};
80 my $library_id = $params->{library_id};
81 my $lines = $params->{lines};
82 my $type = $params->{type} || 'PAYMENT';
83 my $payment_type = $params->{payment_type} || undef;
84 my $credit_type = $params->{credit_type};
85 my $offset_type = $params->{offset_type} || $type eq 'WRITEOFF' ? 'Writeoff' : 'Payment';
86 my $cash_register = $params->{cash_register};
87 my $item_id = $params->{item_id};
89 my $userenv = C4::Context->userenv;
96 my $patron = Koha::Patrons->find( $self->{patron_id} );
98 my $manager_id = $userenv ? $userenv->{number} : undef;
99 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
100 Koha::Exceptions::Account::RegisterRequired->throw()
101 if ( C4::Context->preference("UseCashRegisters")
102 && defined($payment_type)
103 && ( $payment_type eq 'CASH' )
104 && !defined($cash_register) );
106 my @fines_paid; # List of account lines paid on with this payment
108 # The outcome of any attempted item renewals as a result of fines being
110 my $renew_outcomes = [];
112 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
113 $balance_remaining ||= 0;
117 # We were passed a specific line to pay
118 foreach my $fine ( @$lines ) {
120 $fine->amountoutstanding > $balance_remaining
122 : $fine->amountoutstanding;
124 my $old_amountoutstanding = $fine->amountoutstanding;
125 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
126 $fine->amountoutstanding($new_amountoutstanding)->store();
127 $balance_remaining = $balance_remaining - $amount_to_pay;
129 # Attempt to renew the item associated with this debit if
131 if ($fine->is_renewable) {
132 # We're ignoring the definition of $interface above, by all
133 # accounts we can't rely on C4::Context::interface, so here
134 # we're only using what we've been explicitly passed
135 my $outcome = $fine->renew_item({ interface => $interface });
136 push @{$renew_outcomes}, $outcome if $outcome;
139 # Same logic exists in Koha::Account::Line::apply
140 if ( C4::Context->preference('MarkLostItemsAsReturned') =~ m|onpayment|
141 && $fine->debit_type_code
142 && $fine->debit_type_code eq 'LOST'
143 && $new_amountoutstanding == 0
145 && !( $credit_type eq 'LOST_FOUND'
146 && $item_id == $fine->itemnumber ) )
148 C4::Circulation::ReturnLostItem( $self->{patron_id},
152 my $account_offset = Koha::Account::Offset->new(
154 debit_id => $fine->id,
155 type => $offset_type,
156 amount => $amount_to_pay * -1,
159 push( @account_offsets, $account_offset );
161 if ( C4::Context->preference("FinesLog") ) {
167 action => 'fee_payment',
168 borrowernumber => $fine->borrowernumber,
169 old_amountoutstanding => $old_amountoutstanding,
170 new_amountoutstanding => 0,
171 amount_paid => $old_amountoutstanding,
172 accountlines_id => $fine->id,
173 manager_id => $manager_id,
179 push( @fines_paid, $fine->id );
183 # Were not passed a specific line to pay, or the payment was for more
184 # than the what was owed on the given line. In that case pay down other
185 # lines with remaining balance.
186 my @outstanding_fines;
187 @outstanding_fines = $self->lines->search(
189 amountoutstanding => { '>' => 0 },
191 ) if $balance_remaining > 0;
193 foreach my $fine (@outstanding_fines) {
195 $fine->amountoutstanding > $balance_remaining
197 : $fine->amountoutstanding;
199 my $old_amountoutstanding = $fine->amountoutstanding;
200 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
203 # If we need to make a note of the item associated with this line,
204 # in order that we can potentially renew it, do so.
205 my $amt = $old_amountoutstanding - $amount_to_pay;
206 if ( $fine->is_renewable ) {
207 my $outcome = $fine->renew_item({ interface => $interface });
208 push @{$renew_outcomes}, $outcome if $outcome;
211 if ( C4::Context->preference('MarkLostItemsAsReturned') =~ m|onpayment|
212 && $fine->debit_type_code
213 && $fine->debit_type_code eq 'LOST'
214 && $fine->amountoutstanding == 0
216 && !( $credit_type eq 'LOST_FOUND'
217 && $item_id == $fine->itemnumber ) )
219 C4::Circulation::ReturnLostItem( $self->{patron_id},
223 my $account_offset = Koha::Account::Offset->new(
225 debit_id => $fine->id,
226 type => $offset_type,
227 amount => $amount_to_pay * -1,
230 push( @account_offsets, $account_offset );
232 if ( C4::Context->preference("FinesLog") ) {
238 action => "fee_$type",
239 borrowernumber => $fine->borrowernumber,
240 old_amountoutstanding => $old_amountoutstanding,
241 new_amountoutstanding => $fine->amountoutstanding,
242 amount_paid => $amount_to_pay,
243 accountlines_id => $fine->id,
244 manager_id => $manager_id,
250 push( @fines_paid, $fine->id );
253 $balance_remaining = $balance_remaining - $amount_to_pay;
254 last unless $balance_remaining > 0;
257 $description ||= $type eq 'WRITEOFF' ? 'Writeoff' : q{};
259 my $payment = Koha::Account::Line->new(
261 borrowernumber => $self->{patron_id},
262 date => dt_from_string(),
263 amount => 0 - $amount,
264 description => $description,
265 credit_type_code => $credit_type,
266 payment_type => $payment_type,
267 amountoutstanding => 0 - $balance_remaining,
268 manager_id => $manager_id,
269 interface => $interface,
270 branchcode => $library_id,
271 register_id => $cash_register,
273 itemnumber => $item_id,
277 foreach my $o ( @account_offsets ) {
278 $o->credit_id( $payment->id() );
282 C4::Stats::UpdateStats(
284 branch => $library_id,
287 borrowernumber => $self->{patron_id},
291 if ( C4::Context->preference("FinesLog") ) {
297 action => "create_$type",
298 borrowernumber => $self->{patron_id},
299 amount => 0 - $amount,
300 amountoutstanding => 0 - $balance_remaining,
301 credit_type_code => $credit_type,
302 accountlines_paid => \@fines_paid,
303 manager_id => $manager_id,
310 if ( C4::Context->preference('UseEmailReceipts') ) {
312 my $letter = C4::Letters::GetPreparedLetter(
313 module => 'circulation',
314 letter_code => uc("ACCOUNT_$type"),
315 message_transport_type => 'email',
316 lang => $patron->lang,
318 borrowers => $self->{patron_id},
319 branches => $library_id,
323 offsets => \@account_offsets,
328 C4::Letters::EnqueueLetter(
331 borrowernumber => $self->{patron_id},
332 message_transport_type => 'email',
334 ) or warn "can't enqueue letter $letter";
338 return { payment_id => $payment->id, renew_result => $renew_outcomes };
343 This method allows adding credits to a patron's account
345 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
348 description => $description,
351 interface => $interface,
352 library_id => $library_id,
353 payment_type => $payment_type,
354 type => $credit_type,
359 $credit_type can be any of:
372 my ( $self, $params ) = @_;
374 # check for mandatory params
375 my @mandatory = ( 'interface', 'amount' );
376 for my $param (@mandatory) {
377 unless ( defined( $params->{$param} ) ) {
378 Koha::Exceptions::MissingParameter->throw(
379 error => "The $param parameter is mandatory" );
383 # amount should always be passed as a positive value
384 my $amount = $params->{amount} * -1;
385 unless ( $amount < 0 ) {
386 Koha::Exceptions::Account::AmountNotPositive->throw(
387 error => 'Debit amount passed is not positive' );
390 my $description = $params->{description} // q{};
391 my $note = $params->{note} // q{};
392 my $user_id = $params->{user_id};
393 my $interface = $params->{interface};
394 my $library_id = $params->{library_id};
395 my $cash_register = $params->{cash_register};
396 my $payment_type = $params->{payment_type};
397 my $credit_type = $params->{type} || 'PAYMENT';
398 my $item_id = $params->{item_id};
400 Koha::Exceptions::Account::RegisterRequired->throw()
401 if ( C4::Context->preference("UseCashRegisters")
402 && defined($payment_type)
403 && ( $payment_type eq 'CASH' )
404 && !defined($cash_register) );
407 my $schema = Koha::Database->new->schema;
412 # Insert the account line
413 $line = Koha::Account::Line->new(
415 borrowernumber => $self->{patron_id},
418 description => $description,
419 credit_type_code => $credit_type,
420 amountoutstanding => $amount,
421 payment_type => $payment_type,
423 manager_id => $user_id,
424 interface => $interface,
425 branchcode => $library_id,
426 register_id => $cash_register,
427 itemnumber => $item_id,
431 # Record the account offset
432 my $account_offset = Koha::Account::Offset->new(
434 credit_id => $line->id,
435 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
440 C4::Stats::UpdateStats(
442 branch => $library_id,
443 type => lc($credit_type),
445 borrowernumber => $self->{patron_id},
447 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
449 if ( C4::Context->preference("FinesLog") ) {
455 action => "create_$credit_type",
456 borrowernumber => $self->{patron_id},
458 description => $description,
459 amountoutstanding => $amount,
460 credit_type_code => $credit_type,
462 itemnumber => $item_id,
463 manager_id => $user_id,
464 branchcode => $library_id,
474 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
475 if ( $_->broken_fk eq 'credit_type_code' ) {
476 Koha::Exceptions::Account::UnrecognisedType->throw(
477 error => 'Type of credit not recognised' );
490 my $credit = $account->payin_amount(
493 credit_type => $credit_type,
494 payment_type => $payment_type,
495 cash_register => $register_id,
496 interface => $interface,
497 library_id => $branchcode,
498 user_id => $staff_id,
499 debits => $debit_lines,
500 description => $description,
505 This method allows an amount to be paid into a patrons account and immediately applied against debts.
507 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
509 $credit_type can be any of:
517 my ( $self, $params ) = @_;
519 # check for mandatory params
520 my @mandatory = ( 'interface', 'amount', 'type' );
521 for my $param (@mandatory) {
522 unless ( defined( $params->{$param} ) ) {
523 Koha::Exceptions::MissingParameter->throw(
524 error => "The $param parameter is mandatory" );
528 # Check for mandatory register
529 Koha::Exceptions::Account::RegisterRequired->throw()
530 if ( C4::Context->preference("UseCashRegisters")
531 && defined( $params->{payment_type} )
532 && ( $params->{payment_type} eq 'CASH' )
533 && !defined($params->{cash_register}) );
535 # amount should always be passed as a positive value
536 my $amount = $params->{amount};
537 unless ( $amount > 0 ) {
538 Koha::Exceptions::Account::AmountNotPositive->throw(
539 error => 'Payin amount passed is not positive' );
543 my $schema = Koha::Database->new->schema;
548 $credit = $self->add_credit($params);
550 # Offset debts passed first
551 if ( exists( $params->{debits} ) ) {
552 $credit = $credit->apply(
554 debits => $params->{debits},
555 offset_type => $params->{type}
560 # Offset against remaining balance if AutoReconcile
561 if ( C4::Context->preference("AccountAutoReconcile")
562 && $credit->amountoutstanding != 0 )
564 $credit = $credit->apply(
566 debits => [ $self->outstanding_debits->as_list ],
567 offset_type => $params->{type}
579 This method allows adding debits to a patron's account
581 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
584 description => $description,
587 interface => $interface,
588 library_id => $library_id,
590 transaction_type => $transaction_type,
591 cash_register => $register_id,
593 issue_id => $issue_id
597 $debit_type can be any of:
617 my ( $self, $params ) = @_;
619 # check for mandatory params
620 my @mandatory = ( 'interface', 'type', 'amount' );
621 for my $param (@mandatory) {
622 unless ( defined( $params->{$param} ) ) {
623 Koha::Exceptions::MissingParameter->throw(
624 error => "The $param parameter is mandatory" );
628 # check for cash register if using cash
629 Koha::Exceptions::Account::RegisterRequired->throw()
630 if ( C4::Context->preference("UseCashRegisters")
631 && defined( $params->{transaction_type} )
632 && ( $params->{transaction_type} eq 'CASH' )
633 && !defined( $params->{cash_register} ) );
635 # amount should always be a positive value
636 my $amount = $params->{amount};
637 unless ( $amount > 0 ) {
638 Koha::Exceptions::Account::AmountNotPositive->throw(
639 error => 'Debit amount passed is not positive' );
642 my $description = $params->{description} // q{};
643 my $note = $params->{note} // q{};
644 my $user_id = $params->{user_id};
645 my $interface = $params->{interface};
646 my $library_id = $params->{library_id};
647 my $cash_register = $params->{cash_register};
648 my $debit_type = $params->{type};
649 my $transaction_type = $params->{transaction_type};
650 my $item_id = $params->{item_id};
651 my $issue_id = $params->{issue_id};
652 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
655 my $schema = Koha::Database->new->schema;
660 # Insert the account line
661 $line = Koha::Account::Line->new(
663 borrowernumber => $self->{patron_id},
666 description => $description,
667 debit_type_code => $debit_type,
668 amountoutstanding => $amount,
669 payment_type => $transaction_type,
671 manager_id => $user_id,
672 interface => $interface,
673 itemnumber => $item_id,
674 issue_id => $issue_id,
675 branchcode => $library_id,
676 register_id => $cash_register,
678 $debit_type eq 'OVERDUE'
679 ? ( status => 'UNRETURNED' )
685 # Record the account offset
686 my $account_offset = Koha::Account::Offset->new(
688 debit_id => $line->id,
689 type => $offset_type,
694 if ( C4::Context->preference("FinesLog") ) {
700 action => "create_$debit_type",
701 borrowernumber => $self->{patron_id},
703 description => $description,
704 amountoutstanding => $amount,
705 debit_type_code => $debit_type,
707 itemnumber => $item_id,
708 manager_id => $user_id,
718 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
719 if ( $_->broken_fk eq 'debit_type_code' ) {
720 Koha::Exceptions::Account::UnrecognisedType->throw(
721 error => 'Type of debit not recognised' );
734 my $debit = $account->payout_amount(
736 payout_type => $payout_type,
737 register_id => $register_id,
738 staff_id => $staff_id,
739 interface => 'intranet',
741 credits => $credit_lines
745 This method allows an amount to be paid out from a patrons account against outstanding credits.
747 $payout_type can be any of the defined payment_types:
752 my ( $self, $params ) = @_;
754 # Check for mandatory parameters
756 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
757 for my $param (@mandatory) {
758 unless ( defined( $params->{$param} ) ) {
759 Koha::Exceptions::MissingParameter->throw(
760 error => "The $param parameter is mandatory" );
764 # Check for mandatory register
765 Koha::Exceptions::Account::RegisterRequired->throw()
766 if ( C4::Context->preference("UseCashRegisters")
767 && ( $params->{payout_type} eq 'CASH' )
768 && !defined($params->{cash_register}) );
770 # Amount should always be passed as a positive value
771 my $amount = $params->{amount};
772 unless ( $amount > 0 ) {
773 Koha::Exceptions::Account::AmountNotPositive->throw(
774 error => 'Payout amount passed is not positive' );
777 # Amount should always be less than or equal to outstanding credit
779 my $outstanding_credits =
780 exists( $params->{credits} )
782 : $self->outstanding_credits->as_list;
783 for my $credit ( @{$outstanding_credits} ) {
784 $outstanding += $credit->amountoutstanding;
786 $outstanding = $outstanding * -1;
787 Koha::Exceptions::ParameterTooHigh->throw( error =>
788 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
789 ) unless ( $outstanding >= $amount );
792 my $schema = Koha::Database->new->schema;
796 # A 'payout' is a 'debit'
797 $payout = $self->add_debit(
799 amount => $params->{amount},
801 transaction_type => $params->{payout_type},
802 amountoutstanding => $params->{amount},
803 manager_id => $params->{staff_id},
804 interface => $params->{interface},
805 branchcode => $params->{branch},
806 cash_register => $params->{cash_register}
810 # Offset against credits
811 for my $credit ( @{$outstanding_credits} ) {
813 { debits => [$payout], offset_type => 'PAYOUT' } );
814 $payout->discard_changes;
815 last if $payout->amountoutstanding == 0;
819 $payout->status('PAID')->store;
828 my $balance = $self->balance
830 Return the balance (sum of amountoutstanding columns)
836 return $self->lines->total_outstanding;
839 =head3 outstanding_debits
841 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
843 It returns the debit lines with outstanding amounts for the patron.
845 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
846 return a list of Koha::Account::Line objects.
850 sub outstanding_debits {
853 return $self->lines->search(
855 amount => { '>' => 0 },
856 amountoutstanding => { '>' => 0 }
861 =head3 outstanding_credits
863 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
865 It returns the credit lines with outstanding amounts for the patron.
867 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
868 return a list of Koha::Account::Line objects.
872 sub outstanding_credits {
875 return $self->lines->search(
877 amount => { '<' => 0 },
878 amountoutstanding => { '<' => 0 }
883 =head3 non_issues_charges
885 my $non_issues_charges = $self->non_issues_charges
887 Calculates amount immediately owing by the patron - non-issue charges.
889 Charges exempt from non-issue are:
890 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
891 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
892 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
896 sub non_issues_charges {
899 #NOTE: With bug 23049 these preferences could be moved to being attached
900 #to individual debit types to give more flexability and specificity.
902 push @not_fines, 'RESERVE'
903 unless C4::Context->preference('HoldsInNoissuesCharge');
904 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
905 unless C4::Context->preference('RentalsInNoissuesCharge');
906 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
907 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
908 push @not_fines, @man_inv;
911 return $self->lines->search(
913 debit_type_code => { -not_in => \@not_fines }
915 )->total_outstanding;
920 my $lines = $self->lines;
922 Return all credits and debits for the user, outstanding or otherwise
929 return Koha::Account::Lines->search(
931 borrowernumber => $self->{patron_id},
936 =head3 reconcile_balance
938 $account->reconcile_balance();
940 Find outstanding credits and use them to pay outstanding debits.
941 Currently, this implicitly uses the 'First In First Out' rule for
942 applying credits against debits.
946 sub reconcile_balance {
949 my $outstanding_debits = $self->outstanding_debits;
950 my $outstanding_credits = $self->outstanding_credits;
952 while ( $outstanding_debits->total_outstanding > 0
953 and my $credit = $outstanding_credits->next )
955 # there's both outstanding debits and credits
956 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
958 $outstanding_debits = $self->outstanding_debits;
974 'CREDIT' => 'Manual Credit',
975 'FORGIVEN' => 'Writeoff',
976 'LOST_FOUND' => 'Lost Item Found',
977 'OVERPAYMENT' => 'Overpayment',
978 'PAYMENT' => 'Payment',
979 'WRITEOFF' => 'Writeoff',
980 'ACCOUNT' => 'Account Fee',
981 'ACCOUNT_RENEW' => 'Account Fee',
982 'RESERVE' => 'Reserve Fee',
983 'PROCESSING' => 'Processing Fee',
984 'LOST' => 'Lost Item',
985 'RENT' => 'Rental Fee',
986 'RENT_DAILY' => 'Rental Fee',
987 'RENT_RENEW' => 'Rental Fee',
988 'RENT_DAILY_RENEW' => 'Rental Fee',
989 'OVERDUE' => 'OVERDUE',
990 'RESERVE_EXPIRED' => 'Hold Expired',
991 'PAYOUT' => 'PAYOUT',
998 Kyle M Hall <kyle.m.hall@gmail.com>
999 Tomás Cohen Arazi <tomascohen@gmail.com>
1000 Martin Renvoize <martin.renvoize@ptfs-europe.com>