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(
61 description => $description,
62 library_id => $branchcode,
63 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
64 account_type => $type, # accounttype code
65 offset_type => $offset_type, # offset type code
72 my ( $self, $params ) = @_;
74 my $amount = $params->{amount};
75 my $sip = $params->{sip};
76 my $description = $params->{description};
77 my $note = $params->{note} || q{};
78 my $library_id = $params->{library_id};
79 my $lines = $params->{lines};
80 my $type = $params->{type} || 'payment';
81 my $payment_type = $params->{payment_type} || undef;
82 my $account_type = $params->{account_type};
83 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
85 my $userenv = C4::Context->userenv;
87 my $patron = Koha::Patrons->find( $self->{patron_id} );
89 my $manager_id = $userenv ? $userenv->{number} : 0;
90 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
92 my @fines_paid; # List of account lines paid on with this payment
94 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
95 $balance_remaining ||= 0;
99 # We were passed a specific line to pay
100 foreach my $fine ( @$lines ) {
102 $fine->amountoutstanding > $balance_remaining
104 : $fine->amountoutstanding;
106 my $old_amountoutstanding = $fine->amountoutstanding;
107 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
108 $fine->amountoutstanding($new_amountoutstanding)->store();
109 $balance_remaining = $balance_remaining - $amount_to_pay;
111 if ( $new_amountoutstanding == 0 && $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'L' ) )
113 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
116 my $account_offset = Koha::Account::Offset->new(
118 debit_id => $fine->id,
119 type => $offset_type,
120 amount => $amount_to_pay * -1,
123 push( @account_offsets, $account_offset );
125 if ( C4::Context->preference("FinesLog") ) {
131 action => 'fee_payment',
132 borrowernumber => $fine->borrowernumber,
133 old_amountoutstanding => $old_amountoutstanding,
134 new_amountoutstanding => 0,
135 amount_paid => $old_amountoutstanding,
136 accountlines_id => $fine->id,
137 manager_id => $manager_id,
143 push( @fines_paid, $fine->id );
147 # Were not passed a specific line to pay, or the payment was for more
148 # than the what was owed on the given line. In that case pay down other
149 # lines with remaining balance.
150 my @outstanding_fines;
151 @outstanding_fines = $self->lines->search(
153 amountoutstanding => { '>' => 0 },
155 ) if $balance_remaining > 0;
157 foreach my $fine (@outstanding_fines) {
159 $fine->amountoutstanding > $balance_remaining
161 : $fine->amountoutstanding;
163 my $old_amountoutstanding = $fine->amountoutstanding;
164 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
167 if ( $fine->amountoutstanding == 0 && $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'L' ) )
169 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
172 my $account_offset = Koha::Account::Offset->new(
174 debit_id => $fine->id,
175 type => $offset_type,
176 amount => $amount_to_pay * -1,
179 push( @account_offsets, $account_offset );
181 if ( C4::Context->preference("FinesLog") ) {
187 action => "fee_$type",
188 borrowernumber => $fine->borrowernumber,
189 old_amountoutstanding => $old_amountoutstanding,
190 new_amountoutstanding => $fine->amountoutstanding,
191 amount_paid => $amount_to_pay,
192 accountlines_id => $fine->id,
193 manager_id => $manager_id,
199 push( @fines_paid, $fine->id );
202 $balance_remaining = $balance_remaining - $amount_to_pay;
203 last unless $balance_remaining > 0;
207 $type eq 'writeoff' ? 'W'
208 : defined($sip) ? "Pay$sip"
211 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
213 my $payment = Koha::Account::Line->new(
215 borrowernumber => $self->{patron_id},
216 date => dt_from_string(),
217 amount => 0 - $amount,
218 description => $description,
219 accounttype => $account_type,
220 payment_type => $payment_type,
221 amountoutstanding => 0 - $balance_remaining,
222 manager_id => $manager_id,
223 interface => $interface,
224 branchcode => $library_id,
229 foreach my $o ( @account_offsets ) {
230 $o->credit_id( $payment->id() );
236 branch => $library_id,
239 borrowernumber => $self->{patron_id},
243 if ( C4::Context->preference("FinesLog") ) {
249 action => "create_$type",
250 borrowernumber => $self->{patron_id},
251 amount => 0 - $amount,
252 amountoutstanding => 0 - $balance_remaining,
253 accounttype => $account_type,
254 accountlines_paid => \@fines_paid,
255 manager_id => $manager_id,
262 if ( C4::Context->preference('UseEmailReceipts') ) {
264 my $letter = C4::Letters::GetPreparedLetter(
265 module => 'circulation',
266 letter_code => uc("ACCOUNT_$type"),
267 message_transport_type => 'email',
268 lang => $patron->lang,
270 borrowers => $self->{patron_id},
271 branches => $self->{library_id},
275 offsets => \@account_offsets,
280 C4::Letters::EnqueueLetter(
283 borrowernumber => $self->{patron_id},
284 message_transport_type => 'email',
286 ) or warn "can't enqueue letter $letter";
295 This method allows adding credits to a patron's account
297 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
300 description => $description,
303 interface => $interface,
304 library_id => $library_id,
306 payment_type => $payment_type,
307 type => $credit_type,
312 $credit_type can be any of:
323 my ( $self, $params ) = @_;
325 # amount is passed as a positive value, but we store credit as negative values
326 my $amount = $params->{amount} * -1;
327 my $description = $params->{description} // q{};
328 my $note = $params->{note} // q{};
329 my $user_id = $params->{user_id};
330 my $interface = $params->{interface};
331 my $library_id = $params->{library_id};
332 my $sip = $params->{sip};
333 my $payment_type = $params->{payment_type};
334 my $type = $params->{type} || 'payment';
335 my $item_id = $params->{item_id};
337 unless ( $interface ) {
338 Koha::Exceptions::MissingParameter->throw(
339 error => 'The interface parameter is mandatory'
343 my $schema = Koha::Database->new->schema;
345 my $account_type = $Koha::Account::account_type_credit->{$type};
346 $account_type .= $sip
355 # Insert the account line
356 $line = Koha::Account::Line->new(
357 { borrowernumber => $self->{patron_id},
360 description => $description,
361 accounttype => $account_type,
362 amountoutstanding => $amount,
363 payment_type => $payment_type,
365 manager_id => $user_id,
366 interface => $interface,
367 branchcode => $library_id,
368 itemnumber => $item_id,
372 # Record the account offset
373 my $account_offset = Koha::Account::Offset->new(
374 { credit_id => $line->id,
375 type => $Koha::Account::offset_type->{$type},
381 { branch => $library_id,
384 borrowernumber => $self->{patron_id},
386 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
388 if ( C4::Context->preference("FinesLog") ) {
393 { action => "create_$type",
394 borrowernumber => $self->{patron_id},
396 description => $description,
397 amountoutstanding => $amount,
398 accounttype => $account_type,
400 itemnumber => $item_id,
401 manager_id => $user_id,
402 branchcode => $library_id,
416 This method allows adding debits to a patron's account
418 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
421 description => $description,
424 interface => $interface,
425 library_id => $library_id,
428 issue_id => $issue_id
432 $debit_type can be any of:
447 my ( $self, $params ) = @_;
449 # amount should always be a positive value
450 my $amount = $params->{amount};
452 unless ( $amount > 0 ) {
453 Koha::Exceptions::Account::AmountNotPositive->throw(
454 error => 'Debit amount passed is not positive'
458 my $description = $params->{description} // q{};
459 my $note = $params->{note} // q{};
460 my $user_id = $params->{user_id};
461 my $interface = $params->{interface};
462 my $library_id = $params->{library_id};
463 my $type = $params->{type};
464 my $item_id = $params->{item_id};
465 my $issue_id = $params->{issue_id};
467 unless ( $interface ) {
468 Koha::Exceptions::MissingParameter->throw(
469 error => 'The interface parameter is mandatory'
473 my $schema = Koha::Database->new->schema;
475 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
476 Koha::Exceptions::Account::UnrecognisedType->throw(
477 error => 'Type of debit not recognised'
481 my $account_type = $Koha::Account::account_type_debit->{$type};
488 # Insert the account line
489 $line = Koha::Account::Line->new(
490 { borrowernumber => $self->{patron_id},
493 description => $description,
494 accounttype => $account_type,
495 amountoutstanding => $amount,
496 payment_type => undef,
498 manager_id => $user_id,
499 interface => $interface,
500 itemnumber => $item_id,
501 issue_id => $issue_id,
502 branchcode => $library_id,
503 ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : ()),
507 # Record the account offset
508 my $account_offset = Koha::Account::Offset->new(
509 { debit_id => $line->id,
510 type => $Koha::Account::offset_type->{$type},
515 if ( C4::Context->preference("FinesLog") ) {
520 { action => "create_$type",
521 borrowernumber => $self->{patron_id},
523 description => $description,
524 amountoutstanding => $amount,
525 accounttype => $account_type,
527 itemnumber => $item_id,
528 manager_id => $user_id,
542 my $balance = $self->balance
544 Return the balance (sum of amountoutstanding columns)
550 return $self->lines->total_outstanding;
553 =head3 outstanding_debits
555 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
557 It returns the debit lines with outstanding amounts for the patron.
559 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
560 return a list of Koha::Account::Line objects.
564 sub outstanding_debits {
567 return $self->lines->search(
569 amount => { '>' => 0 },
570 amountoutstanding => { '>' => 0 }
575 =head3 outstanding_credits
577 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
579 It returns the credit lines with outstanding amounts for the patron.
581 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
582 return a list of Koha::Account::Line objects.
586 sub outstanding_credits {
589 return $self->lines->search(
591 amount => { '<' => 0 },
592 amountoutstanding => { '<' => 0 }
597 =head3 non_issues_charges
599 my $non_issues_charges = $self->non_issues_charges
601 Calculates amount immediately owing by the patron - non-issue charges.
603 Charges exempt from non-issue are:
604 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
605 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
606 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
610 sub non_issues_charges {
613 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
614 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
617 push @not_fines, 'Res'
618 unless C4::Context->preference('HoldsInNoissuesCharge');
619 push @not_fines, 'Rent'
620 unless C4::Context->preference('RentalsInNoissuesCharge');
621 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
622 my $dbh = C4::Context->dbh;
625 $dbh->selectcol_arrayref(q|
626 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
630 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
632 return $self->lines->search(
634 accounttype => { -not_in => \@not_fines }
636 )->total_outstanding;
641 my $lines = $self->lines;
643 Return all credits and debits for the user, outstanding or otherwise
650 return Koha::Account::Lines->search(
652 borrowernumber => $self->{patron_id},
657 =head3 reconcile_balance
659 $account->reconcile_balance();
661 Find outstanding credits and use them to pay outstanding debits.
662 Currently, this implicitly uses the 'First In First Out' rule for
663 applying credits against debits.
667 sub reconcile_balance {
670 my $outstanding_debits = $self->outstanding_debits;
671 my $outstanding_credits = $self->outstanding_credits;
673 while ( $outstanding_debits->total_outstanding > 0
674 and my $credit = $outstanding_credits->next )
676 # there's both outstanding debits and credits
677 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
679 $outstanding_debits = $self->outstanding_debits;
695 'credit' => 'Manual Credit',
696 'forgiven' => 'Writeoff',
697 'lost_item_return' => 'Lost Item',
698 'payment' => 'Payment',
699 'writeoff' => 'Writeoff',
700 'account' => 'Account Fee',
701 'reserve' => 'Reserve Fee',
702 'processing' => 'Processing Fee',
703 'lost_item' => 'Lost Item',
704 'rent' => 'Rental Fee',
705 'overdue' => 'OVERDUE',
706 'manual_debit' => 'Manual Debit',
707 'hold_expired' => 'Hold Expired'
710 =head3 $account_type_credit
714 our $account_type_credit = {
717 'lost_item_return' => 'CR',
722 =head3 $account_type_debit
726 our $account_type_debit = {
728 'overdue' => 'OVERDUE',
732 'processing' => 'PF',
735 'manual_debit' => 'M',
736 'hold_expired' => 'HE'
743 Kyle M Hall <kyle.m.hall@gmail.com>
744 Tomás Cohen Arazi <tomascohen@gmail.com>
745 Martin Renvoize <martin.renvoize@ptfs-europe.com>