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,
226 foreach my $o ( @account_offsets ) {
227 $o->credit_id( $payment->id() );
231 $library_id ||= $userenv ? $userenv->{'branch'} : undef;
235 branch => $library_id,
238 borrowernumber => $self->{patron_id},
239 accountno => $accountno,
243 if ( C4::Context->preference("FinesLog") ) {
249 action => "create_$type",
250 borrowernumber => $self->{patron_id},
251 accountno => $accountno,
252 amount => 0 - $amount,
253 amountoutstanding => 0 - $balance_remaining,
254 accounttype => $account_type,
255 accountlines_paid => \@fines_paid,
256 manager_id => $manager_id,
262 if ( C4::Context->preference('UseEmailReceipts') ) {
264 my $letter = C4::Letters::GetPreparedLetter(
265 module => 'circulation',
266 letter_code => uc("ACCOUNT_$type"),
267 message_transport_type => 'email',
268 lang => $patron->lang,
270 borrowers => $self->{patron_id},
271 branches => $self->{library_id},
275 offsets => \@account_offsets,
280 C4::Letters::EnqueueLetter(
283 borrowernumber => $self->{patron_id},
284 message_transport_type => 'email',
286 ) or warn "can't enqueue letter $letter";
295 This method allows adding credits to a patron's account
297 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
300 description => $description,
303 library_id => $library_id,
305 payment_type => $payment_type,
306 type => $credit_type,
311 $credit_type can be any of:
322 my ( $self, $params ) = @_;
324 # amount is passed as a positive value, but we store credit as negative values
325 my $amount = $params->{amount} * -1;
326 my $description = $params->{description} // q{};
327 my $note = $params->{note} // q{};
328 my $user_id = $params->{user_id};
329 my $library_id = $params->{library_id};
330 my $sip = $params->{sip};
331 my $payment_type = $params->{payment_type};
332 my $type = $params->{type} || 'payment';
333 my $item_id = $params->{item_id};
335 my $schema = Koha::Database->new->schema;
337 my $account_type = $Koha::Account::account_type->{$type};
338 $account_type .= $sip
346 # We should remove accountno, it is no longer needed
347 my $last = $self->lines->search(
349 { order_by => 'accountno' } )->next();
350 my $accountno = $last ? $last->accountno + 1 : 1;
352 # Insert the account line
353 $line = Koha::Account::Line->new(
354 { borrowernumber => $self->{patron_id},
357 description => $description,
358 accounttype => $account_type,
359 amountoutstanding => $amount,
360 payment_type => $payment_type,
362 manager_id => $user_id,
363 itemnumber => $item_id
367 # Record the account offset
368 my $account_offset = Koha::Account::Offset->new(
369 { credit_id => $line->id,
370 type => $Koha::Account::offset_type->{$type},
376 { branch => $library_id,
379 borrowernumber => $self->{patron_id},
380 accountno => $accountno,
382 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
384 if ( C4::Context->preference("FinesLog") ) {
389 { action => "create_$type",
390 borrowernumber => $self->{patron_id},
391 accountno => $accountno,
393 description => $description,
394 amountoutstanding => $amount,
395 accounttype => $account_type,
397 itemnumber => $item_id,
398 manager_id => $user_id,
411 my $balance = $self->balance
413 Return the balance (sum of amountoutstanding columns)
419 return $self->lines->total_outstanding;
422 =head3 outstanding_debits
424 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
428 sub outstanding_debits {
431 return $self->lines->search(
433 amount => { '>' => 0 },
434 amountoutstanding => { '>' => 0 }
439 =head3 outstanding_credits
441 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
445 sub outstanding_credits {
448 return $self->lines->search(
450 amount => { '<' => 0 },
451 amountoutstanding => { '<' => 0 }
456 =head3 non_issues_charges
458 my $non_issues_charges = $self->non_issues_charges
460 Calculates amount immediately owing by the patron - non-issue charges.
462 Charges exempt from non-issue are:
463 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
464 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
465 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
469 sub non_issues_charges {
472 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
473 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
476 push @not_fines, 'Res'
477 unless C4::Context->preference('HoldsInNoissuesCharge');
478 push @not_fines, 'Rent'
479 unless C4::Context->preference('RentalsInNoissuesCharge');
480 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
481 my $dbh = C4::Context->dbh;
484 $dbh->selectcol_arrayref(q|
485 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
489 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
491 return $self->lines->search(
493 accounttype => { -not_in => \@not_fines }
495 )->total_outstanding;
500 my $lines = $self->lines;
502 Return all credits and debits for the user, outstanding or otherwise
509 return Koha::Account::Lines->search(
511 borrowernumber => $self->{patron_id},
516 =head3 reconcile_balance
518 $account->reconcile_balance();
520 Find outstanding credits and use them to pay outstanding debits.
521 Currently, this implicitly uses the 'First In First Out' rule for
522 applying credits against debits.
526 sub reconcile_balance {
529 my $outstanding_debits = $self->outstanding_debits;
530 my $outstanding_credits = $self->outstanding_credits;
532 while ( $outstanding_debits->total_outstanding > 0
533 and my $credit = $outstanding_credits->next )
535 # there's both outstanding debits and credits
536 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
538 $outstanding_debits = $self->outstanding_debits;
554 'credit' => 'Manual Credit',
555 'forgiven' => 'Writeoff',
556 'lost_item_return' => 'Lost Item',
557 'payment' => 'Payment',
558 'writeoff' => 'Writeoff'
565 our $account_type = {
568 'lost_item_return' => 'CR',
575 Kyle M Hall <kyle.m.hall@gmail.com>