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;
84 $library_id ||= $userenv ? $userenv->{branch} : undef;
86 my $patron = Koha::Patrons->find( $self->{patron_id} );
88 # We should remove accountno, it is no longer needed
89 my $last = $self->lines->search(
91 { order_by => 'accountno' } )->next();
92 my $accountno = $last ? $last->accountno + 1 : 1;
94 my $manager_id = $userenv ? $userenv->{number} : 0;
96 my @fines_paid; # List of account lines paid on with this payment
98 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
99 $balance_remaining ||= 0;
103 # We were passed a specific line to pay
104 foreach my $fine ( @$lines ) {
106 $fine->amountoutstanding > $balance_remaining
108 : $fine->amountoutstanding;
110 my $old_amountoutstanding = $fine->amountoutstanding;
111 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
112 $fine->amountoutstanding($new_amountoutstanding)->store();
113 $balance_remaining = $balance_remaining - $amount_to_pay;
115 if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
117 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
120 my $account_offset = Koha::Account::Offset->new(
122 debit_id => $fine->id,
123 type => $offset_type,
124 amount => $amount_to_pay * -1,
127 push( @account_offsets, $account_offset );
129 if ( C4::Context->preference("FinesLog") ) {
135 action => 'fee_payment',
136 borrowernumber => $fine->borrowernumber,
137 old_amountoutstanding => $old_amountoutstanding,
138 new_amountoutstanding => 0,
139 amount_paid => $old_amountoutstanding,
140 accountlines_id => $fine->id,
141 accountno => $fine->accountno,
142 manager_id => $manager_id,
147 push( @fines_paid, $fine->id );
151 # Were not passed a specific line to pay, or the payment was for more
152 # than the what was owed on the given line. In that case pay down other
153 # lines with remaining balance.
154 my @outstanding_fines;
155 @outstanding_fines = $self->lines->search(
157 amountoutstanding => { '>' => 0 },
159 ) if $balance_remaining > 0;
161 foreach my $fine (@outstanding_fines) {
163 $fine->amountoutstanding > $balance_remaining
165 : $fine->amountoutstanding;
167 my $old_amountoutstanding = $fine->amountoutstanding;
168 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
171 my $account_offset = Koha::Account::Offset->new(
173 debit_id => $fine->id,
174 type => $offset_type,
175 amount => $amount_to_pay * -1,
178 push( @account_offsets, $account_offset );
180 if ( C4::Context->preference("FinesLog") ) {
186 action => "fee_$type",
187 borrowernumber => $fine->borrowernumber,
188 old_amountoutstanding => $old_amountoutstanding,
189 new_amountoutstanding => $fine->amountoutstanding,
190 amount_paid => $amount_to_pay,
191 accountlines_id => $fine->id,
192 accountno => $fine->accountno,
193 manager_id => $manager_id,
198 push( @fines_paid, $fine->id );
201 $balance_remaining = $balance_remaining - $amount_to_pay;
202 last unless $balance_remaining > 0;
206 $type eq 'writeoff' ? 'W'
207 : defined($sip) ? "Pay$sip"
210 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
212 my $payment = Koha::Account::Line->new(
214 borrowernumber => $self->{patron_id},
215 accountno => $accountno,
216 date => dt_from_string(),
217 amount => 0 - $amount,
218 description => $description,
219 accounttype => $account_type,
220 payment_type => $payment_type,
221 amountoutstanding => 0 - $balance_remaining,
222 manager_id => $manager_id,
223 branchcode => $library_id,
228 foreach my $o ( @account_offsets ) {
229 $o->credit_id( $payment->id() );
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},
355 accountno => $accountno,
358 description => $description,
359 accounttype => $account_type,
360 amountoutstanding => $amount,
361 payment_type => $payment_type,
363 manager_id => $user_id,
364 branchcode => $library_id,
365 itemnumber => $item_id,
366 lastincrement => undef,
370 # Record the account offset
371 my $account_offset = Koha::Account::Offset->new(
372 { credit_id => $line->id,
373 type => $Koha::Account::offset_type->{$type},
379 { branch => $library_id,
382 borrowernumber => $self->{patron_id},
383 accountno => $accountno,
385 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
387 if ( C4::Context->preference("FinesLog") ) {
392 { action => "create_$type",
393 borrowernumber => $self->{patron_id},
394 accountno => $accountno,
396 description => $description,
397 amountoutstanding => $amount,
398 accounttype => $account_type,
400 itemnumber => $item_id,
401 manager_id => $user_id,
402 branchcode => $library_id,
415 my $balance = $self->balance
417 Return the balance (sum of amountoutstanding columns)
423 return $self->lines->total_outstanding;
426 =head3 outstanding_debits
428 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
430 It returns the debit lines with outstanding amounts for the patron.
432 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
433 return a list of Koha::Account::Line objects.
437 sub outstanding_debits {
440 return $self->lines->search(
442 amount => { '>' => 0 },
443 amountoutstanding => { '>' => 0 }
448 =head3 outstanding_credits
450 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
452 It returns the credit lines with outstanding amounts for the patron.
454 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
455 return a list of Koha::Account::Line objects.
459 sub outstanding_credits {
462 return $self->lines->search(
464 amount => { '<' => 0 },
465 amountoutstanding => { '<' => 0 }
470 =head3 non_issues_charges
472 my $non_issues_charges = $self->non_issues_charges
474 Calculates amount immediately owing by the patron - non-issue charges.
476 Charges exempt from non-issue are:
477 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
478 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
479 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
483 sub non_issues_charges {
486 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
487 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
490 push @not_fines, 'Res'
491 unless C4::Context->preference('HoldsInNoissuesCharge');
492 push @not_fines, 'Rent'
493 unless C4::Context->preference('RentalsInNoissuesCharge');
494 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
495 my $dbh = C4::Context->dbh;
498 $dbh->selectcol_arrayref(q|
499 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
503 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
505 return $self->lines->search(
507 accounttype => { -not_in => \@not_fines }
509 )->total_outstanding;
514 my $lines = $self->lines;
516 Return all credits and debits for the user, outstanding or otherwise
523 return Koha::Account::Lines->search(
525 borrowernumber => $self->{patron_id},
530 =head3 reconcile_balance
532 $account->reconcile_balance();
534 Find outstanding credits and use them to pay outstanding debits.
535 Currently, this implicitly uses the 'First In First Out' rule for
536 applying credits against debits.
540 sub reconcile_balance {
543 my $outstanding_debits = $self->outstanding_debits;
544 my $outstanding_credits = $self->outstanding_credits;
546 while ( $outstanding_debits->total_outstanding > 0
547 and my $credit = $outstanding_credits->next )
549 # there's both outstanding debits and credits
550 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
552 $outstanding_debits = $self->outstanding_debits;
568 'credit' => 'Manual Credit',
569 'forgiven' => 'Writeoff',
570 'lost_item_return' => 'Lost Item',
571 'payment' => 'Payment',
572 'writeoff' => 'Writeoff'
579 our $account_type = {
582 'lost_item_return' => 'CR',
589 Kyle M Hall <kyle.m.hall@gmail.com>