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 This method allows adding debits to a patron's account
492 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
495 description => $description,
498 interface => $interface,
499 library_id => $library_id,
501 transaction_type => $transaction_type,
502 cash_register => $register_id,
504 issue_id => $issue_id
508 $debit_type can be any of:
528 my ( $self, $params ) = @_;
530 # check for mandatory params
531 my @mandatory = ( 'interface', 'type', 'amount' );
532 for my $param (@mandatory) {
533 unless ( defined( $params->{$param} ) ) {
534 Koha::Exceptions::MissingParameter->throw(
535 error => "The $param parameter is mandatory" );
539 # check for cash register if using cash
540 Koha::Exceptions::Account::RegisterRequired->throw()
541 if ( C4::Context->preference("UseCashRegisters")
542 && defined( $params->{transaction_type} )
543 && ( $params->{transaction_type} eq 'CASH' )
544 && !defined( $params->{cash_register} ) );
546 # amount should always be a positive value
547 my $amount = $params->{amount};
548 unless ( $amount > 0 ) {
549 Koha::Exceptions::Account::AmountNotPositive->throw(
550 error => 'Debit amount passed is not positive' );
553 my $description = $params->{description} // q{};
554 my $note = $params->{note} // q{};
555 my $user_id = $params->{user_id};
556 my $interface = $params->{interface};
557 my $library_id = $params->{library_id};
558 my $cash_register = $params->{cash_register};
559 my $debit_type = $params->{type};
560 my $transaction_type = $params->{transaction_type};
561 my $item_id = $params->{item_id};
562 my $issue_id = $params->{issue_id};
563 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
566 my $schema = Koha::Database->new->schema;
571 # Insert the account line
572 $line = Koha::Account::Line->new(
574 borrowernumber => $self->{patron_id},
577 description => $description,
578 debit_type_code => $debit_type,
579 amountoutstanding => $amount,
580 payment_type => $transaction_type,
582 manager_id => $user_id,
583 interface => $interface,
584 itemnumber => $item_id,
585 issue_id => $issue_id,
586 branchcode => $library_id,
587 register_id => $cash_register,
589 $debit_type eq 'OVERDUE'
590 ? ( status => 'UNRETURNED' )
596 # Record the account offset
597 my $account_offset = Koha::Account::Offset->new(
599 debit_id => $line->id,
600 type => $offset_type,
605 if ( C4::Context->preference("FinesLog") ) {
611 action => "create_$debit_type",
612 borrowernumber => $self->{patron_id},
614 description => $description,
615 amountoutstanding => $amount,
616 debit_type_code => $debit_type,
618 itemnumber => $item_id,
619 manager_id => $user_id,
629 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
630 if ( $_->broken_fk eq 'debit_type_code' ) {
631 Koha::Exceptions::Account::UnrecognisedType->throw(
632 error => 'Type of debit not recognised' );
645 my $debit = $account->payout_amount(
647 payout_type => $payout_type,
648 register_id => $register_id,
649 staff_id => $staff_id,
650 interface => 'intranet',
652 credits => $credit_lines
656 This method allows an amount to be paid out from a patrons account against outstanding credits.
658 $payout_type can be any of the defined payment_types:
663 my ( $self, $params ) = @_;
665 # Check for mandatory parameters
667 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
668 for my $param (@mandatory) {
669 unless ( defined( $params->{$param} ) ) {
670 Koha::Exceptions::MissingParameter->throw(
671 error => "The $param parameter is mandatory" );
675 # Check for mandatory register
676 Koha::Exceptions::Account::RegisterRequired->throw()
677 if ( C4::Context->preference("UseCashRegisters")
678 && ( $params->{payout_type} eq 'CASH' )
679 && !defined($params->{cash_register}) );
681 # Amount should always be passed as a positive value
682 my $amount = $params->{amount};
683 unless ( $amount > 0 ) {
684 Koha::Exceptions::Account::AmountNotPositive->throw(
685 error => 'Payout amount passed is not positive' );
688 # Amount should always be less than or equal to outstanding credit
690 my $outstanding_credits =
691 exists( $params->{credits} )
693 : $self->outstanding_credits->as_list;
694 for my $credit ( @{$outstanding_credits} ) {
695 $outstanding += $credit->amountoutstanding;
697 $outstanding = $outstanding * -1;
698 Koha::Exceptions::ParameterTooHigh->throw( error =>
699 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
700 ) unless ( $outstanding >= $amount );
703 my $schema = Koha::Database->new->schema;
707 # A 'payout' is a 'debit'
708 $payout = $self->add_debit(
710 amount => $params->{amount},
712 transaction_type => $params->{payout_type},
713 amountoutstanding => $params->{amount},
714 manager_id => $params->{staff_id},
715 interface => $params->{interface},
716 branchcode => $params->{branch},
717 cash_register => $params->{cash_register}
721 # Offset against credits
722 for my $credit ( @{$outstanding_credits} ) {
724 { debits => [$payout], offset_type => 'PAYOUT' } );
725 $payout->discard_changes;
726 last if $payout->amountoutstanding == 0;
730 $payout->status('PAID')->store;
739 my $balance = $self->balance
741 Return the balance (sum of amountoutstanding columns)
747 return $self->lines->total_outstanding;
750 =head3 outstanding_debits
752 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
754 It returns the debit lines with outstanding amounts for the patron.
756 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
757 return a list of Koha::Account::Line objects.
761 sub outstanding_debits {
764 return $self->lines->search(
766 amount => { '>' => 0 },
767 amountoutstanding => { '>' => 0 }
772 =head3 outstanding_credits
774 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
776 It returns the credit lines with outstanding amounts for the patron.
778 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
779 return a list of Koha::Account::Line objects.
783 sub outstanding_credits {
786 return $self->lines->search(
788 amount => { '<' => 0 },
789 amountoutstanding => { '<' => 0 }
794 =head3 non_issues_charges
796 my $non_issues_charges = $self->non_issues_charges
798 Calculates amount immediately owing by the patron - non-issue charges.
800 Charges exempt from non-issue are:
801 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
802 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
803 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
807 sub non_issues_charges {
810 #NOTE: With bug 23049 these preferences could be moved to being attached
811 #to individual debit types to give more flexability and specificity.
813 push @not_fines, 'RESERVE'
814 unless C4::Context->preference('HoldsInNoissuesCharge');
815 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
816 unless C4::Context->preference('RentalsInNoissuesCharge');
817 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
818 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
819 push @not_fines, @man_inv;
822 return $self->lines->search(
824 debit_type_code => { -not_in => \@not_fines }
826 )->total_outstanding;
831 my $lines = $self->lines;
833 Return all credits and debits for the user, outstanding or otherwise
840 return Koha::Account::Lines->search(
842 borrowernumber => $self->{patron_id},
847 =head3 reconcile_balance
849 $account->reconcile_balance();
851 Find outstanding credits and use them to pay outstanding debits.
852 Currently, this implicitly uses the 'First In First Out' rule for
853 applying credits against debits.
857 sub reconcile_balance {
860 my $outstanding_debits = $self->outstanding_debits;
861 my $outstanding_credits = $self->outstanding_credits;
863 while ( $outstanding_debits->total_outstanding > 0
864 and my $credit = $outstanding_credits->next )
866 # there's both outstanding debits and credits
867 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
869 $outstanding_debits = $self->outstanding_debits;
885 'CREDIT' => 'Manual Credit',
886 'FORGIVEN' => 'Writeoff',
887 'LOST_FOUND' => 'Lost Item Found',
888 'OVERPAYMENT' => 'Overpayment',
889 'PAYMENT' => 'Payment',
890 'WRITEOFF' => 'Writeoff',
891 'ACCOUNT' => 'Account Fee',
892 'ACCOUNT_RENEW' => 'Account Fee',
893 'RESERVE' => 'Reserve Fee',
894 'PROCESSING' => 'Processing Fee',
895 'LOST' => 'Lost Item',
896 'RENT' => 'Rental Fee',
897 'RENT_DAILY' => 'Rental Fee',
898 'RENT_RENEW' => 'Rental Fee',
899 'RENT_DAILY_RENEW' => 'Rental Fee',
900 'OVERDUE' => 'OVERDUE',
901 'RESERVE_EXPIRED' => 'Hold Expired',
902 'PAYOUT' => 'PAYOUT',
909 Kyle M Hall <kyle.m.hall@gmail.com>
910 Tomás Cohen Arazi <tomascohen@gmail.com>
911 Martin Renvoize <martin.renvoize@ptfs-europe.com>