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 );
38 Koha::Accounts - Module for managing payments and fees for patrons
43 my ( $class, $params ) = @_;
45 Carp::croak("No patron id passed in!") unless $params->{patron_id};
47 return bless( $params, $class );
52 This method allows payments to be made against fees/fines
54 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
59 description => $description,
60 library_id => $branchcode,
61 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
62 account_type => $type, # accounttype code
63 offset_type => $offset_type, # offset type code
70 my ( $self, $params ) = @_;
72 my $amount = $params->{amount};
73 my $sip = $params->{sip};
74 my $description = $params->{description};
75 my $note = $params->{note} || q{};
76 my $library_id = $params->{library_id};
77 my $lines = $params->{lines};
78 my $type = $params->{type} || 'payment';
79 my $payment_type = $params->{payment_type} || undef;
80 my $account_type = $params->{account_type};
81 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
83 my $userenv = C4::Context->userenv;
85 my $patron = Koha::Patrons->find( $self->{patron_id} );
87 # We should remove accountno, it is no longer needed
88 my $last = $self->lines->search(
90 { order_by => 'accountno' } )->next();
91 my $accountno = $last ? $last->accountno + 1 : 1;
93 my $manager_id = $userenv ? $userenv->{number} : 0;
95 my @fines_paid; # List of account lines paid on with this payment
97 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
98 $balance_remaining ||= 0;
102 # We were passed a specific line to pay
103 foreach my $fine ( @$lines ) {
105 $fine->amountoutstanding > $balance_remaining
107 : $fine->amountoutstanding;
109 my $old_amountoutstanding = $fine->amountoutstanding;
110 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
111 $fine->amountoutstanding($new_amountoutstanding)->store();
112 $balance_remaining = $balance_remaining - $amount_to_pay;
114 if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
116 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
119 my $account_offset = Koha::Account::Offset->new(
121 debit_id => $fine->id,
122 type => $offset_type,
123 amount => $amount_to_pay * -1,
126 push( @account_offsets, $account_offset );
128 if ( C4::Context->preference("FinesLog") ) {
134 action => 'fee_payment',
135 borrowernumber => $fine->borrowernumber,
136 old_amountoutstanding => $old_amountoutstanding,
137 new_amountoutstanding => 0,
138 amount_paid => $old_amountoutstanding,
139 accountlines_id => $fine->id,
140 accountno => $fine->accountno,
141 manager_id => $manager_id,
146 push( @fines_paid, $fine->id );
150 # Were not passed a specific line to pay, or the payment was for more
151 # than the what was owed on the given line. In that case pay down other
152 # lines with remaining balance.
153 my @outstanding_fines;
154 @outstanding_fines = $self->lines->search(
156 amountoutstanding => { '>' => 0 },
158 ) if $balance_remaining > 0;
160 foreach my $fine (@outstanding_fines) {
162 $fine->amountoutstanding > $balance_remaining
164 : $fine->amountoutstanding;
166 my $old_amountoutstanding = $fine->amountoutstanding;
167 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
170 my $account_offset = Koha::Account::Offset->new(
172 debit_id => $fine->id,
173 type => $offset_type,
174 amount => $amount_to_pay * -1,
177 push( @account_offsets, $account_offset );
179 if ( C4::Context->preference("FinesLog") ) {
185 action => "fee_$type",
186 borrowernumber => $fine->borrowernumber,
187 old_amountoutstanding => $old_amountoutstanding,
188 new_amountoutstanding => $fine->amountoutstanding,
189 amount_paid => $amount_to_pay,
190 accountlines_id => $fine->id,
191 accountno => $fine->accountno,
192 manager_id => $manager_id,
197 push( @fines_paid, $fine->id );
200 $balance_remaining = $balance_remaining - $amount_to_pay;
201 last unless $balance_remaining > 0;
205 $type eq 'writeoff' ? 'W'
206 : defined($sip) ? "Pay$sip"
209 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
211 my $payment = Koha::Account::Line->new(
213 borrowernumber => $self->{patron_id},
214 accountno => $accountno,
215 date => dt_from_string(),
216 amount => 0 - $amount,
217 description => $description,
218 accounttype => $account_type,
219 payment_type => $payment_type,
220 amountoutstanding => 0 - $balance_remaining,
221 manager_id => $manager_id,
222 branchcode => $library_id,
227 foreach my $o ( @account_offsets ) {
228 $o->credit_id( $payment->id() );
234 branch => $library_id,
237 borrowernumber => $self->{patron_id},
238 accountno => $accountno,
242 if ( C4::Context->preference("FinesLog") ) {
248 action => "create_$type",
249 borrowernumber => $self->{patron_id},
250 accountno => $accountno,
251 amount => 0 - $amount,
252 amountoutstanding => 0 - $balance_remaining,
253 accounttype => $account_type,
254 accountlines_paid => \@fines_paid,
255 manager_id => $manager_id,
261 if ( C4::Context->preference('UseEmailReceipts') ) {
263 my $letter = C4::Letters::GetPreparedLetter(
264 module => 'circulation',
265 letter_code => uc("ACCOUNT_$type"),
266 message_transport_type => 'email',
267 lang => $patron->lang,
269 borrowers => $self->{patron_id},
270 branches => $self->{library_id},
274 offsets => \@account_offsets,
279 C4::Letters::EnqueueLetter(
282 borrowernumber => $self->{patron_id},
283 message_transport_type => 'email',
285 ) or warn "can't enqueue letter $letter";
294 This method allows adding credits to a patron's account
296 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
299 description => $description,
302 library_id => $library_id,
304 payment_type => $payment_type,
305 type => $credit_type,
310 $credit_type can be any of:
321 my ( $self, $params ) = @_;
323 # amount is passed as a positive value, but we store credit as negative values
324 my $amount = $params->{amount} * -1;
325 my $description = $params->{description} // q{};
326 my $note = $params->{note} // q{};
327 my $user_id = $params->{user_id};
328 my $library_id = $params->{library_id};
329 my $sip = $params->{sip};
330 my $payment_type = $params->{payment_type};
331 my $type = $params->{type} || 'payment';
332 my $item_id = $params->{item_id};
334 my $schema = Koha::Database->new->schema;
336 my $account_type = $Koha::Account::account_type->{$type};
337 $account_type .= $sip
345 # We should remove accountno, it is no longer needed
346 my $last = $self->lines->search(
348 { order_by => 'accountno' } )->next();
349 my $accountno = $last ? $last->accountno + 1 : 1;
351 # Insert the account line
352 $line = Koha::Account::Line->new(
353 { borrowernumber => $self->{patron_id},
354 accountno => $accountno,
357 description => $description,
358 accounttype => $account_type,
359 amountoutstanding => $amount,
360 payment_type => $payment_type,
362 manager_id => $user_id,
363 branchcode => $library_id,
364 itemnumber => $item_id,
365 lastincrement => undef,
369 # Record the account offset
370 my $account_offset = Koha::Account::Offset->new(
371 { credit_id => $line->id,
372 type => $Koha::Account::offset_type->{$type},
378 { branch => $library_id,
381 borrowernumber => $self->{patron_id},
382 accountno => $accountno,
384 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
386 if ( C4::Context->preference("FinesLog") ) {
391 { action => "create_$type",
392 borrowernumber => $self->{patron_id},
393 accountno => $accountno,
395 description => $description,
396 amountoutstanding => $amount,
397 accounttype => $account_type,
399 itemnumber => $item_id,
400 manager_id => $user_id,
401 branchcode => $library_id,
414 my $balance = $self->balance
416 Return the balance (sum of amountoutstanding columns)
422 return $self->lines->total_outstanding;
425 =head3 outstanding_debits
427 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
429 It returns the debit lines with outstanding amounts for the patron.
431 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
432 return a list of Koha::Account::Line objects.
436 sub outstanding_debits {
439 return $self->lines->search(
441 amount => { '>' => 0 },
442 amountoutstanding => { '>' => 0 }
447 =head3 outstanding_credits
449 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
451 It returns the credit lines with outstanding amounts for the patron.
453 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
454 return a list of Koha::Account::Line objects.
458 sub outstanding_credits {
461 return $self->lines->search(
463 amount => { '<' => 0 },
464 amountoutstanding => { '<' => 0 }
469 =head3 non_issues_charges
471 my $non_issues_charges = $self->non_issues_charges
473 Calculates amount immediately owing by the patron - non-issue charges.
475 Charges exempt from non-issue are:
476 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
477 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
478 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
482 sub non_issues_charges {
485 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
486 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
489 push @not_fines, 'Res'
490 unless C4::Context->preference('HoldsInNoissuesCharge');
491 push @not_fines, 'Rent'
492 unless C4::Context->preference('RentalsInNoissuesCharge');
493 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
494 my $dbh = C4::Context->dbh;
497 $dbh->selectcol_arrayref(q|
498 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
502 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
504 return $self->lines->search(
506 accounttype => { -not_in => \@not_fines }
508 )->total_outstanding;
513 my $lines = $self->lines;
515 Return all credits and debits for the user, outstanding or otherwise
522 return Koha::Account::Lines->search(
524 borrowernumber => $self->{patron_id},
529 =head3 reconcile_balance
531 $account->reconcile_balance();
533 Find outstanding credits and use them to pay outstanding debits.
534 Currently, this implicitly uses the 'First In First Out' rule for
535 applying credits against debits.
539 sub reconcile_balance {
542 my $outstanding_debits = $self->outstanding_debits;
543 my $outstanding_credits = $self->outstanding_credits;
545 while ( $outstanding_debits->total_outstanding > 0
546 and my $credit = $outstanding_credits->next )
548 # there's both outstanding debits and credits
549 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
551 $outstanding_debits = $self->outstanding_debits;
567 'credit' => 'Manual Credit',
568 'forgiven' => 'Writeoff',
569 'lost_item_return' => 'Lost Item',
570 'payment' => 'Payment',
571 'writeoff' => 'Writeoff'
578 our $account_type = {
581 'lost_item_return' => 'CR',
588 Kyle M Hall <kyle.m.hall@gmail.com>