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 );
35 use Koha::Exceptions::Account;
39 Koha::Accounts - Module for managing payments and fees for patrons
44 my ( $class, $params ) = @_;
46 Carp::croak("No patron id passed in!") unless $params->{patron_id};
48 return bless( $params, $class );
53 This method allows payments to be made against fees/fines
55 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 $sip = $params->{sip};
75 my $description = $params->{description};
76 my $note = $params->{note} || q{};
77 my $library_id = $params->{library_id};
78 my $lines = $params->{lines};
79 my $type = $params->{type} || 'payment';
80 my $payment_type = $params->{payment_type} || undef;
81 my $account_type = $params->{account_type};
82 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
84 my $userenv = C4::Context->userenv;
86 my $patron = Koha::Patrons->find( $self->{patron_id} );
88 my $manager_id = $userenv ? $userenv->{number} : 0;
90 my @fines_paid; # List of account lines paid on with this payment
92 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
93 $balance_remaining ||= 0;
97 # We were passed a specific line to pay
98 foreach my $fine ( @$lines ) {
100 $fine->amountoutstanding > $balance_remaining
102 : $fine->amountoutstanding;
104 my $old_amountoutstanding = $fine->amountoutstanding;
105 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
106 $fine->amountoutstanding($new_amountoutstanding)->store();
107 $balance_remaining = $balance_remaining - $amount_to_pay;
109 if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
111 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
114 my $account_offset = Koha::Account::Offset->new(
116 debit_id => $fine->id,
117 type => $offset_type,
118 amount => $amount_to_pay * -1,
121 push( @account_offsets, $account_offset );
123 if ( C4::Context->preference("FinesLog") ) {
129 action => 'fee_payment',
130 borrowernumber => $fine->borrowernumber,
131 old_amountoutstanding => $old_amountoutstanding,
132 new_amountoutstanding => 0,
133 amount_paid => $old_amountoutstanding,
134 accountlines_id => $fine->id,
135 manager_id => $manager_id,
140 push( @fines_paid, $fine->id );
144 # Were not passed a specific line to pay, or the payment was for more
145 # than the what was owed on the given line. In that case pay down other
146 # lines with remaining balance.
147 my @outstanding_fines;
148 @outstanding_fines = $self->lines->search(
150 amountoutstanding => { '>' => 0 },
152 ) if $balance_remaining > 0;
154 foreach my $fine (@outstanding_fines) {
156 $fine->amountoutstanding > $balance_remaining
158 : $fine->amountoutstanding;
160 my $old_amountoutstanding = $fine->amountoutstanding;
161 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
164 my $account_offset = Koha::Account::Offset->new(
166 debit_id => $fine->id,
167 type => $offset_type,
168 amount => $amount_to_pay * -1,
171 push( @account_offsets, $account_offset );
173 if ( C4::Context->preference("FinesLog") ) {
179 action => "fee_$type",
180 borrowernumber => $fine->borrowernumber,
181 old_amountoutstanding => $old_amountoutstanding,
182 new_amountoutstanding => $fine->amountoutstanding,
183 amount_paid => $amount_to_pay,
184 accountlines_id => $fine->id,
185 manager_id => $manager_id,
190 push( @fines_paid, $fine->id );
193 $balance_remaining = $balance_remaining - $amount_to_pay;
194 last unless $balance_remaining > 0;
198 $type eq 'writeoff' ? 'W'
199 : defined($sip) ? "Pay$sip"
202 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
204 my $payment = Koha::Account::Line->new(
206 borrowernumber => $self->{patron_id},
207 date => dt_from_string(),
208 amount => 0 - $amount,
209 description => $description,
210 accounttype => $account_type,
211 payment_type => $payment_type,
212 amountoutstanding => 0 - $balance_remaining,
213 manager_id => $manager_id,
214 branchcode => $library_id,
219 foreach my $o ( @account_offsets ) {
220 $o->credit_id( $payment->id() );
226 branch => $library_id,
229 borrowernumber => $self->{patron_id},
233 if ( C4::Context->preference("FinesLog") ) {
239 action => "create_$type",
240 borrowernumber => $self->{patron_id},
241 amount => 0 - $amount,
242 amountoutstanding => 0 - $balance_remaining,
243 accounttype => $account_type,
244 accountlines_paid => \@fines_paid,
245 manager_id => $manager_id,
251 if ( C4::Context->preference('UseEmailReceipts') ) {
253 my $letter = C4::Letters::GetPreparedLetter(
254 module => 'circulation',
255 letter_code => uc("ACCOUNT_$type"),
256 message_transport_type => 'email',
257 lang => $patron->lang,
259 borrowers => $self->{patron_id},
260 branches => $self->{library_id},
264 offsets => \@account_offsets,
269 C4::Letters::EnqueueLetter(
272 borrowernumber => $self->{patron_id},
273 message_transport_type => 'email',
275 ) or warn "can't enqueue letter $letter";
284 This method allows adding credits to a patron's account
286 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
289 description => $description,
292 library_id => $library_id,
294 payment_type => $payment_type,
295 type => $credit_type,
300 $credit_type can be any of:
311 my ( $self, $params ) = @_;
313 # amount is passed as a positive value, but we store credit as negative values
314 my $amount = $params->{amount} * -1;
315 my $description = $params->{description} // q{};
316 my $note = $params->{note} // q{};
317 my $user_id = $params->{user_id};
318 my $library_id = $params->{library_id};
319 my $sip = $params->{sip};
320 my $payment_type = $params->{payment_type};
321 my $type = $params->{type} || 'payment';
322 my $item_id = $params->{item_id};
324 my $schema = Koha::Database->new->schema;
326 my $account_type = $Koha::Account::account_type_credit->{$type};
327 $account_type .= $sip
336 # Insert the account line
337 $line = Koha::Account::Line->new(
338 { borrowernumber => $self->{patron_id},
341 description => $description,
342 accounttype => $account_type,
343 amountoutstanding => $amount,
344 payment_type => $payment_type,
346 manager_id => $user_id,
347 branchcode => $library_id,
348 itemnumber => $item_id,
352 # Record the account offset
353 my $account_offset = Koha::Account::Offset->new(
354 { credit_id => $line->id,
355 type => $Koha::Account::offset_type->{$type},
361 { branch => $library_id,
364 borrowernumber => $self->{patron_id},
366 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
368 if ( C4::Context->preference("FinesLog") ) {
373 { action => "create_$type",
374 borrowernumber => $self->{patron_id},
376 description => $description,
377 amountoutstanding => $amount,
378 accounttype => $account_type,
380 itemnumber => $item_id,
381 manager_id => $user_id,
382 branchcode => $library_id,
395 This method allows adding debits to a patron's account
397 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
400 description => $description,
403 library_id => $library_id,
406 issue_id => $issue_id
410 $debit_type can be any of:
426 my ( $self, $params ) = @_;
428 # amount should always be a positive value
429 my $amount = $params->{amount};
431 unless ( $amount > 0 ) {
432 Koha::Exceptions::Account::AmountNotPositive->throw(
433 error => 'Debit amount passed is not positive'
437 my $description = $params->{description} // q{};
438 my $note = $params->{note} // q{};
439 my $user_id = $params->{user_id};
440 my $library_id = $params->{library_id};
441 my $type = $params->{type};
442 my $item_id = $params->{item_id};
443 my $issue_id = $params->{issue_id};
445 my $schema = Koha::Database->new->schema;
447 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
448 Koha::Exceptions::Account::UnrecognisedType->throw(
449 error => 'Type of debit not recognised'
453 my $account_type = $Koha::Account::account_type_debit->{$type};
460 # Insert the account line
461 $line = Koha::Account::Line->new(
462 { borrowernumber => $self->{patron_id},
465 description => $description,
466 accounttype => $account_type,
467 amountoutstanding => $amount,
468 payment_type => undef,
470 manager_id => $user_id,
471 itemnumber => $item_id,
472 issue_id => $issue_id,
473 branchcode => $library_id,
477 # Record the account offset
478 my $account_offset = Koha::Account::Offset->new(
479 { debit_id => $line->id,
480 type => $Koha::Account::offset_type->{$type},
485 if ( C4::Context->preference("FinesLog") ) {
490 { action => "create_$type",
491 borrowernumber => $self->{patron_id},
493 description => $description,
494 amountoutstanding => $amount,
495 accounttype => $account_type,
497 itemnumber => $item_id,
498 manager_id => $user_id,
511 my $balance = $self->balance
513 Return the balance (sum of amountoutstanding columns)
519 return $self->lines->total_outstanding;
522 =head3 outstanding_debits
524 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
526 It returns the debit lines with outstanding amounts for the patron.
528 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
529 return a list of Koha::Account::Line objects.
533 sub outstanding_debits {
536 return $self->lines->search(
538 amount => { '>' => 0 },
539 amountoutstanding => { '>' => 0 }
544 =head3 outstanding_credits
546 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
548 It returns the credit lines with outstanding amounts for the patron.
550 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
551 return a list of Koha::Account::Line objects.
555 sub outstanding_credits {
558 return $self->lines->search(
560 amount => { '<' => 0 },
561 amountoutstanding => { '<' => 0 }
566 =head3 non_issues_charges
568 my $non_issues_charges = $self->non_issues_charges
570 Calculates amount immediately owing by the patron - non-issue charges.
572 Charges exempt from non-issue are:
573 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
574 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
575 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
579 sub non_issues_charges {
582 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
583 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
586 push @not_fines, 'Res'
587 unless C4::Context->preference('HoldsInNoissuesCharge');
588 push @not_fines, 'Rent'
589 unless C4::Context->preference('RentalsInNoissuesCharge');
590 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
591 my $dbh = C4::Context->dbh;
594 $dbh->selectcol_arrayref(q|
595 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
599 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
601 return $self->lines->search(
603 accounttype => { -not_in => \@not_fines }
605 )->total_outstanding;
610 my $lines = $self->lines;
612 Return all credits and debits for the user, outstanding or otherwise
619 return Koha::Account::Lines->search(
621 borrowernumber => $self->{patron_id},
626 =head3 reconcile_balance
628 $account->reconcile_balance();
630 Find outstanding credits and use them to pay outstanding debits.
631 Currently, this implicitly uses the 'First In First Out' rule for
632 applying credits against debits.
636 sub reconcile_balance {
639 my $outstanding_debits = $self->outstanding_debits;
640 my $outstanding_credits = $self->outstanding_credits;
642 while ( $outstanding_debits->total_outstanding > 0
643 and my $credit = $outstanding_credits->next )
645 # there's both outstanding debits and credits
646 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
648 $outstanding_debits = $self->outstanding_debits;
664 'credit' => 'Manual Credit',
665 'forgiven' => 'Writeoff',
666 'lost_item_return' => 'Lost Item',
667 'payment' => 'Payment',
668 'writeoff' => 'Writeoff',
669 'account' => 'Account Fee',
670 'reserve' => 'Reserve Fee',
671 'processing' => 'Processing Fee',
672 'lost_item' => 'Lost Item',
673 'rent' => 'Rental Fee',
675 'manual_debit' => 'Manual Debit',
676 'hold_expired' => 'Hold Expired'
679 =head3 $account_type_credit
683 our $account_type_credit = {
686 'lost_item_return' => 'CR',
691 =head3 $account_type_debit
695 our $account_type_debit = {
701 'processing' => 'PF',
705 'manual_debit' => 'M',
706 'hold_expired' => 'HE'
713 Kyle M Hall <kyle.m.hall@gmail.com>
714 Tomás Cohen Arazi <tomascohen@gmail.com>
715 Martin Renvoize <martin.renvoize@ptfs-europe.com>