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
101 # The outcome of any attempted item renewals as a result of fines being
103 my $renew_outcomes = [];
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 # Attempt to renew the item associated with this debit if
124 if ($fine->renewable) {
125 # We're ignoring the definition of $interface above, by all
126 # accounts we can't rely on C4::Context::interface, so here
127 # we're only using what we've been explicitly passed
128 my $outcome = $fine->renew_item({ interface => $interface });
129 push @{$renew_outcomes}, $outcome if $outcome;
132 # Same logic exists in Koha::Account::Line::apply
133 if ( $new_amountoutstanding == 0
135 && $fine->debit_type_code
136 && ( $fine->debit_type_code eq 'LOST' ) )
138 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
141 my $account_offset = Koha::Account::Offset->new(
143 debit_id => $fine->id,
144 type => $offset_type,
145 amount => $amount_to_pay * -1,
148 push( @account_offsets, $account_offset );
150 if ( C4::Context->preference("FinesLog") ) {
156 action => 'fee_payment',
157 borrowernumber => $fine->borrowernumber,
158 old_amountoutstanding => $old_amountoutstanding,
159 new_amountoutstanding => 0,
160 amount_paid => $old_amountoutstanding,
161 accountlines_id => $fine->id,
162 manager_id => $manager_id,
168 push( @fines_paid, $fine->id );
172 # Were not passed a specific line to pay, or the payment was for more
173 # than the what was owed on the given line. In that case pay down other
174 # lines with remaining balance.
175 my @outstanding_fines;
176 @outstanding_fines = $self->lines->search(
178 amountoutstanding => { '>' => 0 },
180 ) if $balance_remaining > 0;
182 foreach my $fine (@outstanding_fines) {
184 $fine->amountoutstanding > $balance_remaining
186 : $fine->amountoutstanding;
188 my $old_amountoutstanding = $fine->amountoutstanding;
189 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
192 # If we need to make a note of the item associated with this line,
193 # in order that we can potentially renew it, do so.
194 my $amt = $old_amountoutstanding - $amount_to_pay;
195 if ($fine->renewable) {
196 my $outcome = $fine->renew_item;
197 push @{$renew_outcomes}, $outcome;
200 if ( $fine->amountoutstanding == 0
202 && $fine->debit_type_code
203 && ( $fine->debit_type_code eq 'LOST' ) )
205 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
208 my $account_offset = Koha::Account::Offset->new(
210 debit_id => $fine->id,
211 type => $offset_type,
212 amount => $amount_to_pay * -1,
215 push( @account_offsets, $account_offset );
217 if ( C4::Context->preference("FinesLog") ) {
223 action => "fee_$type",
224 borrowernumber => $fine->borrowernumber,
225 old_amountoutstanding => $old_amountoutstanding,
226 new_amountoutstanding => $fine->amountoutstanding,
227 amount_paid => $amount_to_pay,
228 accountlines_id => $fine->id,
229 manager_id => $manager_id,
235 push( @fines_paid, $fine->id );
238 $balance_remaining = $balance_remaining - $amount_to_pay;
239 last unless $balance_remaining > 0;
247 $description ||= $type eq 'WRITEOFF' ? 'Writeoff' : q{};
249 my $payment = Koha::Account::Line->new(
251 borrowernumber => $self->{patron_id},
252 date => dt_from_string(),
253 amount => 0 - $amount,
254 description => $description,
255 credit_type_code => $credit_type,
256 payment_type => $payment_type,
257 amountoutstanding => 0 - $balance_remaining,
258 manager_id => $manager_id,
259 interface => $interface,
260 branchcode => $library_id,
261 register_id => $cash_register,
266 foreach my $o ( @account_offsets ) {
267 $o->credit_id( $payment->id() );
273 branch => $library_id,
276 borrowernumber => $self->{patron_id},
280 if ( C4::Context->preference("FinesLog") ) {
286 action => "create_$type",
287 borrowernumber => $self->{patron_id},
288 amount => 0 - $amount,
289 amountoutstanding => 0 - $balance_remaining,
290 credit_type_code => $credit_type,
291 accountlines_paid => \@fines_paid,
292 manager_id => $manager_id,
299 if ( C4::Context->preference('UseEmailReceipts') ) {
301 my $letter = C4::Letters::GetPreparedLetter(
302 module => 'circulation',
303 letter_code => uc("ACCOUNT_$type"),
304 message_transport_type => 'email',
305 lang => $patron->lang,
307 borrowers => $self->{patron_id},
308 branches => $library_id,
312 offsets => \@account_offsets,
317 C4::Letters::EnqueueLetter(
320 borrowernumber => $self->{patron_id},
321 message_transport_type => 'email',
323 ) or warn "can't enqueue letter $letter";
327 return { payment_id => $payment->id, renew_result => $renew_outcomes };
332 This method allows adding credits to a patron's account
334 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
337 description => $description,
340 interface => $interface,
341 library_id => $library_id,
342 payment_type => $payment_type,
343 type => $credit_type,
348 $credit_type can be any of:
359 my ( $self, $params ) = @_;
361 # check for mandatory params
362 my @mandatory = ( 'interface', 'amount' );
363 for my $param (@mandatory) {
364 unless ( defined( $params->{$param} ) ) {
365 Koha::Exceptions::MissingParameter->throw(
366 error => "The $param parameter is mandatory" );
370 # amount should always be passed as a positive value
371 my $amount = $params->{amount} * -1;
372 unless ( $amount < 0 ) {
373 Koha::Exceptions::Account::AmountNotPositive->throw(
374 error => 'Debit amount passed is not positive' );
377 my $description = $params->{description} // q{};
378 my $note = $params->{note} // q{};
379 my $user_id = $params->{user_id};
380 my $interface = $params->{interface};
381 my $library_id = $params->{library_id};
382 my $cash_register = $params->{cash_register};
383 my $payment_type = $params->{payment_type};
384 my $credit_type = $params->{type} || 'PAYMENT';
385 my $item_id = $params->{item_id};
387 Koha::Exceptions::Account::RegisterRequired->throw()
388 if ( C4::Context->preference("UseCashRegisters")
389 && defined($payment_type)
390 && ( $payment_type eq 'CASH' )
391 && !defined($cash_register) );
394 my $schema = Koha::Database->new->schema;
399 # Insert the account line
400 $line = Koha::Account::Line->new(
402 borrowernumber => $self->{patron_id},
405 description => $description,
406 credit_type_code => $credit_type,
407 amountoutstanding => $amount,
408 payment_type => $payment_type,
410 manager_id => $user_id,
411 interface => $interface,
412 branchcode => $library_id,
413 register_id => $cash_register,
414 itemnumber => $item_id,
418 # Record the account offset
419 my $account_offset = Koha::Account::Offset->new(
421 credit_id => $line->id,
422 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
429 branch => $library_id,
430 type => lc($credit_type),
432 borrowernumber => $self->{patron_id},
434 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
436 if ( C4::Context->preference("FinesLog") ) {
442 action => "create_$credit_type",
443 borrowernumber => $self->{patron_id},
445 description => $description,
446 amountoutstanding => $amount,
447 credit_type_code => $credit_type,
449 itemnumber => $item_id,
450 manager_id => $user_id,
451 branchcode => $library_id,
461 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
462 if ( $_->broken_fk eq 'credit_type_code' ) {
463 Koha::Exceptions::Account::UnrecognisedType->throw(
464 error => 'Type of credit not recognised' );
477 This method allows adding debits to a patron's account
479 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
482 description => $description,
485 interface => $interface,
486 library_id => $library_id,
489 issue_id => $issue_id
493 $debit_type can be any of:
512 my ( $self, $params ) = @_;
514 # check for mandatory params
515 my @mandatory = ( 'interface', 'type', 'amount' );
516 for my $param (@mandatory) {
517 unless ( defined( $params->{$param} ) ) {
518 Koha::Exceptions::MissingParameter->throw(
519 error => "The $param parameter is mandatory" );
523 # amount should always be a positive value
524 my $amount = $params->{amount};
525 unless ( $amount > 0 ) {
526 Koha::Exceptions::Account::AmountNotPositive->throw(
527 error => 'Debit amount passed is not positive' );
530 my $description = $params->{description} // q{};
531 my $note = $params->{note} // q{};
532 my $user_id = $params->{user_id};
533 my $interface = $params->{interface};
534 my $library_id = $params->{library_id};
535 my $debit_type = $params->{type};
536 my $item_id = $params->{item_id};
537 my $issue_id = $params->{issue_id};
538 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
541 my $schema = Koha::Database->new->schema;
546 # Insert the account line
547 $line = Koha::Account::Line->new(
549 borrowernumber => $self->{patron_id},
552 description => $description,
553 debit_type_code => $debit_type,
554 amountoutstanding => $amount,
555 payment_type => undef,
557 manager_id => $user_id,
558 interface => $interface,
559 itemnumber => $item_id,
560 issue_id => $issue_id,
561 branchcode => $library_id,
563 $debit_type eq 'OVERDUE'
564 ? ( status => 'UNRETURNED' )
570 # Record the account offset
571 my $account_offset = Koha::Account::Offset->new(
573 debit_id => $line->id,
574 type => $offset_type,
579 if ( C4::Context->preference("FinesLog") ) {
585 action => "create_$debit_type",
586 borrowernumber => $self->{patron_id},
588 description => $description,
589 amountoutstanding => $amount,
590 debit_type_code => $debit_type,
592 itemnumber => $item_id,
593 manager_id => $user_id,
603 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
604 if ( $_->broken_fk eq 'debit_type_code' ) {
605 Koha::Exceptions::Account::UnrecognisedType->throw(
606 error => 'Type of debit not recognised' );
619 my $balance = $self->balance
621 Return the balance (sum of amountoutstanding columns)
627 return $self->lines->total_outstanding;
630 =head3 outstanding_debits
632 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
634 It returns the debit lines with outstanding amounts for the patron.
636 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
637 return a list of Koha::Account::Line objects.
641 sub outstanding_debits {
644 return $self->lines->search(
646 amount => { '>' => 0 },
647 amountoutstanding => { '>' => 0 }
652 =head3 outstanding_credits
654 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
656 It returns the credit lines with outstanding amounts for the patron.
658 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
659 return a list of Koha::Account::Line objects.
663 sub outstanding_credits {
666 return $self->lines->search(
668 amount => { '<' => 0 },
669 amountoutstanding => { '<' => 0 }
674 =head3 non_issues_charges
676 my $non_issues_charges = $self->non_issues_charges
678 Calculates amount immediately owing by the patron - non-issue charges.
680 Charges exempt from non-issue are:
681 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
682 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
683 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
687 sub non_issues_charges {
690 #NOTE: With bug 23049 these preferences could be moved to being attached
691 #to individual debit types to give more flexability and specificity.
693 push @not_fines, 'RESERVE'
694 unless C4::Context->preference('HoldsInNoissuesCharge');
695 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
696 unless C4::Context->preference('RentalsInNoissuesCharge');
697 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
698 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
699 push @not_fines, @man_inv;
702 return $self->lines->search(
704 debit_type_code => { -not_in => \@not_fines }
706 )->total_outstanding;
711 my $lines = $self->lines;
713 Return all credits and debits for the user, outstanding or otherwise
720 return Koha::Account::Lines->search(
722 borrowernumber => $self->{patron_id},
727 =head3 reconcile_balance
729 $account->reconcile_balance();
731 Find outstanding credits and use them to pay outstanding debits.
732 Currently, this implicitly uses the 'First In First Out' rule for
733 applying credits against debits.
737 sub reconcile_balance {
740 my $outstanding_debits = $self->outstanding_debits;
741 my $outstanding_credits = $self->outstanding_credits;
743 while ( $outstanding_debits->total_outstanding > 0
744 and my $credit = $outstanding_credits->next )
746 # there's both outstanding debits and credits
747 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
749 $outstanding_debits = $self->outstanding_debits;
765 'CREDIT' => 'Manual Credit',
766 'FORGIVEN' => 'Writeoff',
767 'LOST_FOUND' => 'Lost Item Found',
768 'PAYMENT' => 'Payment',
769 'WRITEOFF' => 'Writeoff',
770 'ACCOUNT' => 'Account Fee',
771 'ACCOUNT_RENEW' => 'Account Fee',
772 'RESERVE' => 'Reserve Fee',
773 'PROCESSING' => 'Processing Fee',
774 'LOST' => 'Lost Item',
775 'RENT' => 'Rental Fee',
776 'RENT_DAILY' => 'Rental Fee',
777 'RENT_RENEW' => 'Rental Fee',
778 'RENT_DAILY_RENEW' => 'Rental Fee',
779 'OVERDUE' => 'OVERDUE',
780 'RESERVE_EXPIRED' => 'Hold Expired'
787 Kyle M Hall <kyle.m.hall@gmail.com>
788 Tomás Cohen Arazi <tomascohen@gmail.com>
789 Martin Renvoize <martin.renvoize@ptfs-europe.com>