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(
60 description => $description,
61 library_id => $branchcode,
62 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
63 account_type => $type, # accounttype code
64 offset_type => $offset_type, # offset type code
71 my ( $self, $params ) = @_;
73 my $amount = $params->{amount};
74 my $description = $params->{description};
75 my $note = $params->{note} || q{};
76 my $library_id = $params->{library_id};
77 my $lines = $params->{lines};
78 my $type = $params->{type} || 'payment';
79 my $payment_type = $params->{payment_type} || undef;
80 my $account_type = $params->{account_type};
81 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
82 my $cash_register = $params->{cash_register};
84 my $userenv = C4::Context->userenv;
86 my $patron = Koha::Patrons->find( $self->{patron_id} );
88 my $manager_id = $userenv ? $userenv->{number} : 0;
89 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
90 Koha::Exceptions::Account::RegisterRequired->throw()
91 if ( C4::Context->preference("UseCashRegisters")
92 && defined($payment_type)
93 && ( $payment_type eq 'CASH' )
94 && !defined($cash_register) );
96 my @fines_paid; # List of account lines paid on with this payment
98 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
99 $balance_remaining ||= 0;
103 # We were passed a specific line to pay
104 foreach my $fine ( @$lines ) {
106 $fine->amountoutstanding > $balance_remaining
108 : $fine->amountoutstanding;
110 my $old_amountoutstanding = $fine->amountoutstanding;
111 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
112 $fine->amountoutstanding($new_amountoutstanding)->store();
113 $balance_remaining = $balance_remaining - $amount_to_pay;
115 # Same logic exists in Koha::Account::Line::apply
116 if ( $new_amountoutstanding == 0
118 && $fine->debit_type_code
119 && ( $fine->debit_type_code eq 'LOST' ) )
121 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
124 my $account_offset = Koha::Account::Offset->new(
126 debit_id => $fine->id,
127 type => $offset_type,
128 amount => $amount_to_pay * -1,
131 push( @account_offsets, $account_offset );
133 if ( C4::Context->preference("FinesLog") ) {
139 action => 'fee_payment',
140 borrowernumber => $fine->borrowernumber,
141 old_amountoutstanding => $old_amountoutstanding,
142 new_amountoutstanding => 0,
143 amount_paid => $old_amountoutstanding,
144 accountlines_id => $fine->id,
145 manager_id => $manager_id,
151 push( @fines_paid, $fine->id );
155 # Were not passed a specific line to pay, or the payment was for more
156 # than the what was owed on the given line. In that case pay down other
157 # lines with remaining balance.
158 my @outstanding_fines;
159 @outstanding_fines = $self->lines->search(
161 amountoutstanding => { '>' => 0 },
163 ) if $balance_remaining > 0;
165 foreach my $fine (@outstanding_fines) {
167 $fine->amountoutstanding > $balance_remaining
169 : $fine->amountoutstanding;
171 my $old_amountoutstanding = $fine->amountoutstanding;
172 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
175 if ( $fine->amountoutstanding == 0
177 && $fine->debit_type_code
178 && ( $fine->debit_type_code eq 'LOST' ) )
180 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
183 my $account_offset = Koha::Account::Offset->new(
185 debit_id => $fine->id,
186 type => $offset_type,
187 amount => $amount_to_pay * -1,
190 push( @account_offsets, $account_offset );
192 if ( C4::Context->preference("FinesLog") ) {
198 action => "fee_$type",
199 borrowernumber => $fine->borrowernumber,
200 old_amountoutstanding => $old_amountoutstanding,
201 new_amountoutstanding => $fine->amountoutstanding,
202 amount_paid => $amount_to_pay,
203 accountlines_id => $fine->id,
204 manager_id => $manager_id,
210 push( @fines_paid, $fine->id );
213 $balance_remaining = $balance_remaining - $amount_to_pay;
214 last unless $balance_remaining > 0;
222 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
224 my $payment = Koha::Account::Line->new(
226 borrowernumber => $self->{patron_id},
227 date => dt_from_string(),
228 amount => 0 - $amount,
229 description => $description,
230 accounttype => $account_type,
231 payment_type => $payment_type,
232 amountoutstanding => 0 - $balance_remaining,
233 manager_id => $manager_id,
234 interface => $interface,
235 branchcode => $library_id,
236 register_id => $cash_register,
241 foreach my $o ( @account_offsets ) {
242 $o->credit_id( $payment->id() );
248 branch => $library_id,
251 borrowernumber => $self->{patron_id},
255 if ( C4::Context->preference("FinesLog") ) {
261 action => "create_$type",
262 borrowernumber => $self->{patron_id},
263 amount => 0 - $amount,
264 amountoutstanding => 0 - $balance_remaining,
265 accounttype => $account_type,
266 accountlines_paid => \@fines_paid,
267 manager_id => $manager_id,
274 if ( C4::Context->preference('UseEmailReceipts') ) {
276 my $letter = C4::Letters::GetPreparedLetter(
277 module => 'circulation',
278 letter_code => uc("ACCOUNT_$type"),
279 message_transport_type => 'email',
280 lang => $patron->lang,
282 borrowers => $self->{patron_id},
283 branches => $self->{library_id},
287 offsets => \@account_offsets,
292 C4::Letters::EnqueueLetter(
295 borrowernumber => $self->{patron_id},
296 message_transport_type => 'email',
298 ) or warn "can't enqueue letter $letter";
307 This method allows adding credits to a patron's account
309 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
312 description => $description,
315 interface => $interface,
316 library_id => $library_id,
317 payment_type => $payment_type,
318 type => $credit_type,
323 $credit_type can be any of:
334 my ( $self, $params ) = @_;
336 # amount is passed as a positive value, but we store credit as negative values
337 my $amount = $params->{amount} * -1;
338 my $description = $params->{description} // q{};
339 my $note = $params->{note} // q{};
340 my $user_id = $params->{user_id};
341 my $interface = $params->{interface};
342 my $library_id = $params->{library_id};
343 my $cash_register = $params->{cash_register};
344 my $payment_type = $params->{payment_type};
345 my $type = $params->{type} || 'payment';
346 my $item_id = $params->{item_id};
348 unless ( $interface ) {
349 Koha::Exceptions::MissingParameter->throw(
350 error => 'The interface parameter is mandatory'
354 Koha::Exceptions::Account::RegisterRequired->throw()
355 if ( C4::Context->preference("UseCashRegisters")
356 && defined($payment_type)
357 && ( $payment_type eq 'CASH' )
358 && !defined($cash_register) );
360 my $schema = Koha::Database->new->schema;
362 my $account_type = $Koha::Account::account_type_credit->{$type};
368 # Insert the account line
369 $line = Koha::Account::Line->new(
370 { borrowernumber => $self->{patron_id},
373 description => $description,
374 accounttype => $account_type,
375 amountoutstanding => $amount,
376 payment_type => $payment_type,
378 manager_id => $user_id,
379 interface => $interface,
380 branchcode => $library_id,
381 register_id => $cash_register,
382 itemnumber => $item_id,
386 # Record the account offset
387 my $account_offset = Koha::Account::Offset->new(
388 { credit_id => $line->id,
389 type => $Koha::Account::offset_type->{$type},
395 { branch => $library_id,
398 borrowernumber => $self->{patron_id},
400 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
402 if ( C4::Context->preference("FinesLog") ) {
407 { action => "create_$type",
408 borrowernumber => $self->{patron_id},
410 description => $description,
411 amountoutstanding => $amount,
412 accounttype => $account_type,
414 itemnumber => $item_id,
415 manager_id => $user_id,
416 branchcode => $library_id,
430 This method allows adding debits to a patron's account
432 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
435 description => $description,
438 interface => $interface,
439 library_id => $library_id,
442 issue_id => $issue_id
446 $debit_type can be any of:
465 my ( $self, $params ) = @_;
467 # amount should always be a positive value
468 my $amount = $params->{amount};
470 unless ( $amount > 0 ) {
471 Koha::Exceptions::Account::AmountNotPositive->throw(
472 error => 'Debit amount passed is not positive' );
475 my $description = $params->{description} // q{};
476 my $note = $params->{note} // q{};
477 my $user_id = $params->{user_id};
478 my $interface = $params->{interface};
479 my $library_id = $params->{library_id};
480 my $type = $params->{type};
481 my $item_id = $params->{item_id};
482 my $issue_id = $params->{issue_id};
484 unless ($interface) {
485 Koha::Exceptions::MissingParameter->throw(
486 error => 'The interface parameter is mandatory' );
489 my $schema = Koha::Database->new->schema;
491 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
492 Koha::Exceptions::Account::UnrecognisedType->throw(
493 error => 'Type of debit not recognised'
497 my $debit_type_code = $Koha::Account::account_type_debit->{$type};
503 # Insert the account line
504 $line = Koha::Account::Line->new(
506 borrowernumber => $self->{patron_id},
509 description => $description,
510 debit_type_code => $debit_type_code,
511 amountoutstanding => $amount,
512 payment_type => undef,
514 manager_id => $user_id,
515 interface => $interface,
516 itemnumber => $item_id,
517 issue_id => $issue_id,
518 branchcode => $library_id,
519 ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : () ),
523 # Record the account offset
524 my $account_offset = Koha::Account::Offset->new(
526 debit_id => $line->id,
527 type => $Koha::Account::offset_type->{$type},
532 if ( C4::Context->preference("FinesLog") ) {
538 action => "create_$type",
539 borrowernumber => $self->{patron_id},
541 description => $description,
542 amountoutstanding => $amount,
543 debit_type_code => $debit_type_code,
545 itemnumber => $item_id,
546 manager_id => $user_id,
560 my $balance = $self->balance
562 Return the balance (sum of amountoutstanding columns)
568 return $self->lines->total_outstanding;
571 =head3 outstanding_debits
573 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
575 It returns the debit lines with outstanding amounts for the patron.
577 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
578 return a list of Koha::Account::Line objects.
582 sub outstanding_debits {
585 return $self->lines->search(
587 amount => { '>' => 0 },
588 amountoutstanding => { '>' => 0 }
593 =head3 outstanding_credits
595 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
597 It returns the credit lines with outstanding amounts for the patron.
599 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
600 return a list of Koha::Account::Line objects.
604 sub outstanding_credits {
607 return $self->lines->search(
609 amount => { '<' => 0 },
610 amountoutstanding => { '<' => 0 }
615 =head3 non_issues_charges
617 my $non_issues_charges = $self->non_issues_charges
619 Calculates amount immediately owing by the patron - non-issue charges.
621 Charges exempt from non-issue are:
622 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
623 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
624 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
628 sub non_issues_charges {
631 #NOTE: With bug 23049 these preferences could be moved to being attached
632 #to individual debit types to give more flexability and specificity.
634 push @not_fines, 'RESERVE'
635 unless C4::Context->preference('HoldsInNoissuesCharge');
636 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
637 unless C4::Context->preference('RentalsInNoissuesCharge');
638 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
639 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
640 push @not_fines, @man_inv;
643 return $self->lines->search(
645 debit_type_code => { -not_in => \@not_fines }
647 )->total_outstanding;
652 my $lines = $self->lines;
654 Return all credits and debits for the user, outstanding or otherwise
661 return Koha::Account::Lines->search(
663 borrowernumber => $self->{patron_id},
668 =head3 reconcile_balance
670 $account->reconcile_balance();
672 Find outstanding credits and use them to pay outstanding debits.
673 Currently, this implicitly uses the 'First In First Out' rule for
674 applying credits against debits.
678 sub reconcile_balance {
681 my $outstanding_debits = $self->outstanding_debits;
682 my $outstanding_credits = $self->outstanding_credits;
684 while ( $outstanding_debits->total_outstanding > 0
685 and my $credit = $outstanding_credits->next )
687 # there's both outstanding debits and credits
688 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
690 $outstanding_debits = $self->outstanding_debits;
706 'credit' => 'Manual Credit',
707 'forgiven' => 'Writeoff',
708 'lost_item_return' => 'Lost Item',
709 'payment' => 'Payment',
710 'writeoff' => 'Writeoff',
711 'ACCOUNT' => 'Account Fee',
712 'ACCOUNT_RENEW' => 'Account Fee',
713 'RESERVE' => 'Reserve Fee',
714 'PROCESSING' => 'Processing Fee',
715 'LOST' => 'Lost Item',
716 'RENT' => 'Rental Fee',
717 'RENT_DAILY' => 'Rental Fee',
718 'RENT_RENEW' => 'Rental Fee',
719 'RENT_DAILY_RENEW' => 'Rental Fee',
720 'overdue' => 'OVERDUE',
721 'RESERVE_EXPIRED' => 'Hold Expired'
724 =head3 $account_type_credit
728 our $account_type_credit = {
731 'lost_item_return' => 'LOST_RETURN',
736 =head3 $account_type_debit
740 our $account_type_debit = {
741 'ACCOUNT' => 'ACCOUNT',
742 'ACCOUNT_RENEW' => 'ACCOUNT_RENEW',
743 'RESERVE_EXPIRED' => 'RESERVE_EXPIRED',
744 'LOST_ITEM' => 'LOST',
746 'overdue' => 'OVERDUE',
747 'PROCESSING' => 'PROCESSING',
749 'RENT_DAILY' => 'RENT_DAILY',
750 'RENT_RENEW' => 'RENT_RENEW',
751 'RENT_DAILY_RENEW' => 'RENT_DAILY_RENEW',
752 'RESERVE' => 'RESERVE',
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>