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:
425 my ( $self, $params ) = @_;
427 # amount should always be a positive value
428 my $amount = $params->{amount};
430 unless ( $amount > 0 ) {
431 Koha::Exceptions::Account::AmountNotPositive->throw(
432 error => 'Debit amount passed is not positive'
436 my $description = $params->{description} // q{};
437 my $note = $params->{note} // q{};
438 my $user_id = $params->{user_id};
439 my $library_id = $params->{library_id};
440 my $type = $params->{type};
441 my $item_id = $params->{item_id};
442 my $issue_id = $params->{issue_id};
444 my $schema = Koha::Database->new->schema;
446 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
447 Koha::Exceptions::Account::UnrecognisedType->throw(
448 error => 'Type of debit not recognised'
452 my $account_type = $Koha::Account::account_type_debit->{$type};
459 # Insert the account line
460 $line = Koha::Account::Line->new(
461 { borrowernumber => $self->{patron_id},
464 description => $description,
465 accounttype => $account_type,
466 amountoutstanding => $amount,
467 payment_type => undef,
469 manager_id => $user_id,
470 itemnumber => $item_id,
471 issue_id => $issue_id,
472 branchcode => $library_id,
476 # Record the account offset
477 my $account_offset = Koha::Account::Offset->new(
478 { debit_id => $line->id,
479 type => $Koha::Account::offset_type->{$type},
484 if ( C4::Context->preference("FinesLog") ) {
489 { action => "create_$type",
490 borrowernumber => $self->{patron_id},
492 description => $description,
493 amountoutstanding => $amount,
494 accounttype => $account_type,
496 itemnumber => $item_id,
497 manager_id => $user_id,
510 my $balance = $self->balance
512 Return the balance (sum of amountoutstanding columns)
518 return $self->lines->total_outstanding;
521 =head3 outstanding_debits
523 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
525 It returns the debit lines with outstanding amounts for the patron.
527 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
528 return a list of Koha::Account::Line objects.
532 sub outstanding_debits {
535 return $self->lines->search(
537 amount => { '>' => 0 },
538 amountoutstanding => { '>' => 0 }
543 =head3 outstanding_credits
545 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
547 It returns the credit lines with outstanding amounts for the patron.
549 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
550 return a list of Koha::Account::Line objects.
554 sub outstanding_credits {
557 return $self->lines->search(
559 amount => { '<' => 0 },
560 amountoutstanding => { '<' => 0 }
565 =head3 non_issues_charges
567 my $non_issues_charges = $self->non_issues_charges
569 Calculates amount immediately owing by the patron - non-issue charges.
571 Charges exempt from non-issue are:
572 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
573 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
574 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
578 sub non_issues_charges {
581 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
582 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
585 push @not_fines, 'Res'
586 unless C4::Context->preference('HoldsInNoissuesCharge');
587 push @not_fines, 'Rent'
588 unless C4::Context->preference('RentalsInNoissuesCharge');
589 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
590 my $dbh = C4::Context->dbh;
593 $dbh->selectcol_arrayref(q|
594 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
598 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
600 return $self->lines->search(
602 accounttype => { -not_in => \@not_fines }
604 )->total_outstanding;
609 my $lines = $self->lines;
611 Return all credits and debits for the user, outstanding or otherwise
618 return Koha::Account::Lines->search(
620 borrowernumber => $self->{patron_id},
625 =head3 reconcile_balance
627 $account->reconcile_balance();
629 Find outstanding credits and use them to pay outstanding debits.
630 Currently, this implicitly uses the 'First In First Out' rule for
631 applying credits against debits.
635 sub reconcile_balance {
638 my $outstanding_debits = $self->outstanding_debits;
639 my $outstanding_credits = $self->outstanding_credits;
641 while ( $outstanding_debits->total_outstanding > 0
642 and my $credit = $outstanding_credits->next )
644 # there's both outstanding debits and credits
645 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
647 $outstanding_debits = $self->outstanding_debits;
663 'credit' => 'Manual Credit',
664 'forgiven' => 'Writeoff',
665 'lost_item_return' => 'Lost Item',
666 'payment' => 'Payment',
667 'writeoff' => 'Writeoff',
668 'account' => 'Account Fee',
669 'reserve' => 'Reserve Fee',
670 'processing' => 'Processing Fee',
671 'lost_item' => 'Lost Item',
672 'rent' => 'Rental Fee',
674 'manual_debit' => 'Manual Debit',
675 'hold_expired' => 'Hold Expired'
678 =head3 $account_type_credit
682 our $account_type_credit = {
685 'lost_item_return' => 'CR',
690 =head3 $account_type_debit
694 our $account_type_debit = {
700 'processing' => 'PF',
703 'manual_debit' => 'M',
704 'hold_expired' => 'HE'
711 Kyle M Hall <kyle.m.hall@gmail.com>
712 Tomás Cohen Arazi <tomascohen@gmail.com>
713 Martin Renvoize <martin.renvoize@ptfs-europe.com>