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 # Same logic exists in Koha::Account::Line::apply
112 if ( $new_amountoutstanding == 0
114 && $fine->accounttype
115 && ( $fine->accounttype eq 'LOST' ) )
117 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
120 my $account_offset = Koha::Account::Offset->new(
122 debit_id => $fine->id,
123 type => $offset_type,
124 amount => $amount_to_pay * -1,
127 push( @account_offsets, $account_offset );
129 if ( C4::Context->preference("FinesLog") ) {
135 action => 'fee_payment',
136 borrowernumber => $fine->borrowernumber,
137 old_amountoutstanding => $old_amountoutstanding,
138 new_amountoutstanding => 0,
139 amount_paid => $old_amountoutstanding,
140 accountlines_id => $fine->id,
141 manager_id => $manager_id,
147 push( @fines_paid, $fine->id );
151 # Were not passed a specific line to pay, or the payment was for more
152 # than the what was owed on the given line. In that case pay down other
153 # lines with remaining balance.
154 my @outstanding_fines;
155 @outstanding_fines = $self->lines->search(
157 amountoutstanding => { '>' => 0 },
159 ) if $balance_remaining > 0;
161 foreach my $fine (@outstanding_fines) {
163 $fine->amountoutstanding > $balance_remaining
165 : $fine->amountoutstanding;
167 my $old_amountoutstanding = $fine->amountoutstanding;
168 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
171 if ( $fine->amountoutstanding == 0
173 && $fine->accounttype
174 && ( $fine->accounttype eq 'LOST' ) )
176 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
179 my $account_offset = Koha::Account::Offset->new(
181 debit_id => $fine->id,
182 type => $offset_type,
183 amount => $amount_to_pay * -1,
186 push( @account_offsets, $account_offset );
188 if ( C4::Context->preference("FinesLog") ) {
194 action => "fee_$type",
195 borrowernumber => $fine->borrowernumber,
196 old_amountoutstanding => $old_amountoutstanding,
197 new_amountoutstanding => $fine->amountoutstanding,
198 amount_paid => $amount_to_pay,
199 accountlines_id => $fine->id,
200 manager_id => $manager_id,
206 push( @fines_paid, $fine->id );
209 $balance_remaining = $balance_remaining - $amount_to_pay;
210 last unless $balance_remaining > 0;
214 $type eq 'writeoff' ? 'W'
215 : defined($sip) ? "Pay$sip"
218 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
220 my $payment = Koha::Account::Line->new(
222 borrowernumber => $self->{patron_id},
223 date => dt_from_string(),
224 amount => 0 - $amount,
225 description => $description,
226 accounttype => $account_type,
227 payment_type => $payment_type,
228 amountoutstanding => 0 - $balance_remaining,
229 manager_id => $manager_id,
230 interface => $interface,
231 branchcode => $library_id,
236 foreach my $o ( @account_offsets ) {
237 $o->credit_id( $payment->id() );
243 branch => $library_id,
246 borrowernumber => $self->{patron_id},
250 if ( C4::Context->preference("FinesLog") ) {
256 action => "create_$type",
257 borrowernumber => $self->{patron_id},
258 amount => 0 - $amount,
259 amountoutstanding => 0 - $balance_remaining,
260 accounttype => $account_type,
261 accountlines_paid => \@fines_paid,
262 manager_id => $manager_id,
269 if ( C4::Context->preference('UseEmailReceipts') ) {
271 my $letter = C4::Letters::GetPreparedLetter(
272 module => 'circulation',
273 letter_code => uc("ACCOUNT_$type"),
274 message_transport_type => 'email',
275 lang => $patron->lang,
277 borrowers => $self->{patron_id},
278 branches => $self->{library_id},
282 offsets => \@account_offsets,
287 C4::Letters::EnqueueLetter(
290 borrowernumber => $self->{patron_id},
291 message_transport_type => 'email',
293 ) or warn "can't enqueue letter $letter";
302 This method allows adding credits to a patron's account
304 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
307 description => $description,
310 interface => $interface,
311 library_id => $library_id,
313 payment_type => $payment_type,
314 type => $credit_type,
319 $credit_type can be any of:
330 my ( $self, $params ) = @_;
332 # amount is passed as a positive value, but we store credit as negative values
333 my $amount = $params->{amount} * -1;
334 my $description = $params->{description} // q{};
335 my $note = $params->{note} // q{};
336 my $user_id = $params->{user_id};
337 my $interface = $params->{interface};
338 my $library_id = $params->{library_id};
339 my $sip = $params->{sip};
340 my $payment_type = $params->{payment_type};
341 my $type = $params->{type} || 'payment';
342 my $item_id = $params->{item_id};
344 unless ( $interface ) {
345 Koha::Exceptions::MissingParameter->throw(
346 error => 'The interface parameter is mandatory'
350 my $schema = Koha::Database->new->schema;
352 my $account_type = $Koha::Account::account_type_credit->{$type};
353 $account_type .= $sip
362 # Insert the account line
363 $line = Koha::Account::Line->new(
364 { borrowernumber => $self->{patron_id},
367 description => $description,
368 accounttype => $account_type,
369 amountoutstanding => $amount,
370 payment_type => $payment_type,
372 manager_id => $user_id,
373 interface => $interface,
374 branchcode => $library_id,
375 itemnumber => $item_id,
379 # Record the account offset
380 my $account_offset = Koha::Account::Offset->new(
381 { credit_id => $line->id,
382 type => $Koha::Account::offset_type->{$type},
388 { branch => $library_id,
391 borrowernumber => $self->{patron_id},
393 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
395 if ( C4::Context->preference("FinesLog") ) {
400 { action => "create_$type",
401 borrowernumber => $self->{patron_id},
403 description => $description,
404 amountoutstanding => $amount,
405 accounttype => $account_type,
407 itemnumber => $item_id,
408 manager_id => $user_id,
409 branchcode => $library_id,
423 This method allows adding debits to a patron's account
425 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
428 description => $description,
431 interface => $interface,
432 library_id => $library_id,
435 issue_id => $issue_id
439 $debit_type can be any of:
454 my ( $self, $params ) = @_;
456 # amount should always be a positive value
457 my $amount = $params->{amount};
459 unless ( $amount > 0 ) {
460 Koha::Exceptions::Account::AmountNotPositive->throw(
461 error => 'Debit amount passed is not positive'
465 my $description = $params->{description} // q{};
466 my $note = $params->{note} // q{};
467 my $user_id = $params->{user_id};
468 my $interface = $params->{interface};
469 my $library_id = $params->{library_id};
470 my $type = $params->{type};
471 my $item_id = $params->{item_id};
472 my $issue_id = $params->{issue_id};
474 unless ( $interface ) {
475 Koha::Exceptions::MissingParameter->throw(
476 error => 'The interface parameter is mandatory'
480 my $schema = Koha::Database->new->schema;
482 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
483 Koha::Exceptions::Account::UnrecognisedType->throw(
484 error => 'Type of debit not recognised'
488 my $account_type = $Koha::Account::account_type_debit->{$type};
495 # Insert the account line
496 $line = Koha::Account::Line->new(
497 { borrowernumber => $self->{patron_id},
500 description => $description,
501 accounttype => $account_type,
502 amountoutstanding => $amount,
503 payment_type => undef,
505 manager_id => $user_id,
506 interface => $interface,
507 itemnumber => $item_id,
508 issue_id => $issue_id,
509 branchcode => $library_id,
510 ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : ()),
514 # Record the account offset
515 my $account_offset = Koha::Account::Offset->new(
516 { debit_id => $line->id,
517 type => $Koha::Account::offset_type->{$type},
522 if ( C4::Context->preference("FinesLog") ) {
527 { action => "create_$type",
528 borrowernumber => $self->{patron_id},
530 description => $description,
531 amountoutstanding => $amount,
532 accounttype => $account_type,
534 itemnumber => $item_id,
535 manager_id => $user_id,
549 my $balance = $self->balance
551 Return the balance (sum of amountoutstanding columns)
557 return $self->lines->total_outstanding;
560 =head3 outstanding_debits
562 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
564 It returns the debit lines with outstanding amounts for the patron.
566 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
567 return a list of Koha::Account::Line objects.
571 sub outstanding_debits {
574 return $self->lines->search(
576 amount => { '>' => 0 },
577 amountoutstanding => { '>' => 0 }
582 =head3 outstanding_credits
584 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
586 It returns the credit lines with outstanding amounts for the patron.
588 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
589 return a list of Koha::Account::Line objects.
593 sub outstanding_credits {
596 return $self->lines->search(
598 amount => { '<' => 0 },
599 amountoutstanding => { '<' => 0 }
604 =head3 non_issues_charges
606 my $non_issues_charges = $self->non_issues_charges
608 Calculates amount immediately owing by the patron - non-issue charges.
610 Charges exempt from non-issue are:
611 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
612 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
613 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
617 sub non_issues_charges {
620 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
621 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
624 push @not_fines, 'Res'
625 unless C4::Context->preference('HoldsInNoissuesCharge');
626 push @not_fines, 'Rent'
627 unless C4::Context->preference('RentalsInNoissuesCharge');
628 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
629 my $dbh = C4::Context->dbh;
632 $dbh->selectcol_arrayref(q|
633 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
637 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
639 return $self->lines->search(
641 accounttype => { -not_in => \@not_fines }
643 )->total_outstanding;
648 my $lines = $self->lines;
650 Return all credits and debits for the user, outstanding or otherwise
657 return Koha::Account::Lines->search(
659 borrowernumber => $self->{patron_id},
664 =head3 reconcile_balance
666 $account->reconcile_balance();
668 Find outstanding credits and use them to pay outstanding debits.
669 Currently, this implicitly uses the 'First In First Out' rule for
670 applying credits against debits.
674 sub reconcile_balance {
677 my $outstanding_debits = $self->outstanding_debits;
678 my $outstanding_credits = $self->outstanding_credits;
680 while ( $outstanding_debits->total_outstanding > 0
681 and my $credit = $outstanding_credits->next )
683 # there's both outstanding debits and credits
684 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
686 $outstanding_debits = $self->outstanding_debits;
702 'credit' => 'Manual Credit',
703 'forgiven' => 'Writeoff',
704 'lost_item_return' => 'Lost Item',
705 'payment' => 'Payment',
706 'writeoff' => 'Writeoff',
707 'account' => 'Account Fee',
708 'reserve' => 'Reserve Fee',
709 'processing' => 'Processing Fee',
710 'lost_item' => 'Lost Item',
711 'rent' => 'Rental Fee',
712 'overdue' => 'OVERDUE',
713 'manual_debit' => 'Manual Debit',
714 'hold_expired' => 'Hold Expired'
717 =head3 $account_type_credit
721 our $account_type_credit = {
724 'lost_item_return' => 'CR',
729 =head3 $account_type_debit
733 our $account_type_debit = {
735 'overdue' => 'OVERDUE',
736 'lost_item' => 'LOST',
739 'processing' => 'PF',
742 'manual_debit' => 'M',
743 'hold_expired' => 'HE'
750 Kyle M Hall <kyle.m.hall@gmail.com>
751 Tomás Cohen Arazi <tomascohen@gmail.com>
752 Martin Renvoize <martin.renvoize@ptfs-europe.com>