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,
349 lastincrement => undef,
353 # Record the account offset
354 my $account_offset = Koha::Account::Offset->new(
355 { credit_id => $line->id,
356 type => $Koha::Account::offset_type->{$type},
362 { branch => $library_id,
365 borrowernumber => $self->{patron_id},
367 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
369 if ( C4::Context->preference("FinesLog") ) {
374 { action => "create_$type",
375 borrowernumber => $self->{patron_id},
377 description => $description,
378 amountoutstanding => $amount,
379 accounttype => $account_type,
381 itemnumber => $item_id,
382 manager_id => $user_id,
383 branchcode => $library_id,
396 This method allows adding debits to a patron's account
398 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
401 description => $description,
404 library_id => $library_id,
407 issue_id => $issue_id
411 $debit_type can be any of:
427 my ( $self, $params ) = @_;
429 # amount should always be a positive value
430 my $amount = $params->{amount};
432 unless ( $amount > 0 ) {
433 Koha::Exceptions::Account::AmountNotPositive->throw(
434 error => 'Debit amount passed is not positive'
438 my $description = $params->{description} // q{};
439 my $note = $params->{note} // q{};
440 my $user_id = $params->{user_id};
441 my $library_id = $params->{library_id};
442 my $type = $params->{type};
443 my $item_id = $params->{item_id};
444 my $issue_id = $params->{issue_id};
446 my $schema = Koha::Database->new->schema;
448 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
449 Koha::Exceptions::Account::UnrecognisedType->throw(
450 error => 'Type of debit not recognised'
454 my $account_type = $Koha::Account::account_type_debit->{$type};
461 # Insert the account line
462 $line = Koha::Account::Line->new(
463 { borrowernumber => $self->{patron_id},
466 description => $description,
467 accounttype => $account_type,
468 amountoutstanding => $amount,
469 payment_type => undef,
471 manager_id => $user_id,
472 itemnumber => $item_id,
473 issue_id => $issue_id,
474 branchcode => $library_id,
475 ( $type eq 'fine' ? ( lastincrement => $amount ) : ()),
479 # Record the account offset
480 my $account_offset = Koha::Account::Offset->new(
481 { debit_id => $line->id,
482 type => $Koha::Account::offset_type->{$type},
487 if ( C4::Context->preference("FinesLog") ) {
492 { action => "create_$type",
493 borrowernumber => $self->{patron_id},
495 description => $description,
496 amountoutstanding => $amount,
497 accounttype => $account_type,
499 itemnumber => $item_id,
500 manager_id => $user_id,
513 my $balance = $self->balance
515 Return the balance (sum of amountoutstanding columns)
521 return $self->lines->total_outstanding;
524 =head3 outstanding_debits
526 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
528 It returns the debit lines with outstanding amounts for the patron.
530 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
531 return a list of Koha::Account::Line objects.
535 sub outstanding_debits {
538 return $self->lines->search(
540 amount => { '>' => 0 },
541 amountoutstanding => { '>' => 0 }
546 =head3 outstanding_credits
548 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
550 It returns the credit lines with outstanding amounts for the patron.
552 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
553 return a list of Koha::Account::Line objects.
557 sub outstanding_credits {
560 return $self->lines->search(
562 amount => { '<' => 0 },
563 amountoutstanding => { '<' => 0 }
568 =head3 non_issues_charges
570 my $non_issues_charges = $self->non_issues_charges
572 Calculates amount immediately owing by the patron - non-issue charges.
574 Charges exempt from non-issue are:
575 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
576 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
577 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
581 sub non_issues_charges {
584 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
585 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
588 push @not_fines, 'Res'
589 unless C4::Context->preference('HoldsInNoissuesCharge');
590 push @not_fines, 'Rent'
591 unless C4::Context->preference('RentalsInNoissuesCharge');
592 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
593 my $dbh = C4::Context->dbh;
596 $dbh->selectcol_arrayref(q|
597 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
601 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
603 return $self->lines->search(
605 accounttype => { -not_in => \@not_fines }
607 )->total_outstanding;
612 my $lines = $self->lines;
614 Return all credits and debits for the user, outstanding or otherwise
621 return Koha::Account::Lines->search(
623 borrowernumber => $self->{patron_id},
628 =head3 reconcile_balance
630 $account->reconcile_balance();
632 Find outstanding credits and use them to pay outstanding debits.
633 Currently, this implicitly uses the 'First In First Out' rule for
634 applying credits against debits.
638 sub reconcile_balance {
641 my $outstanding_debits = $self->outstanding_debits;
642 my $outstanding_credits = $self->outstanding_credits;
644 while ( $outstanding_debits->total_outstanding > 0
645 and my $credit = $outstanding_credits->next )
647 # there's both outstanding debits and credits
648 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
650 $outstanding_debits = $self->outstanding_debits;
666 'credit' => 'Manual Credit',
667 'forgiven' => 'Writeoff',
668 'lost_item_return' => 'Lost Item',
669 'payment' => 'Payment',
670 'writeoff' => 'Writeoff',
671 'account' => 'Account Fee',
672 'reserve' => 'Reserve Fee',
673 'processing' => 'Processing Fee',
674 'lost_item' => 'Lost Item',
675 'rent' => 'Rental Fee',
677 'manual_debit' => 'Manual Debit',
678 'hold_expired' => 'Hold Expired'
681 =head3 $account_type_credit
685 our $account_type_credit = {
688 'lost_item_return' => 'CR',
693 =head3 $account_type_debit
697 our $account_type_debit = {
703 'processing' => 'PF',
707 'manual_debit' => 'M',
708 'hold_expired' => 'HE'
715 Kyle M Hall <kyle.m.hall@gmail.com>
716 Tomás Cohen Arazi <tomascohen@gmail.com>
717 Martin Renvoize <martin.renvoize@ptfs-europe.com>