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},
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;
426 It returns the debit lines with outstanding amounts for the patron.
428 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
429 return a list of Koha::Account::Line objects.
433 sub outstanding_debits {
436 return $self->lines->search(
438 amount => { '>' => 0 },
439 amountoutstanding => { '>' => 0 }
444 =head3 outstanding_credits
446 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
448 It returns the credit lines with outstanding amounts for the patron.
450 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
451 return a list of Koha::Account::Line objects.
455 sub outstanding_credits {
458 return $self->lines->search(
460 amount => { '<' => 0 },
461 amountoutstanding => { '<' => 0 }
466 =head3 non_issues_charges
468 my $non_issues_charges = $self->non_issues_charges
470 Calculates amount immediately owing by the patron - non-issue charges.
472 Charges exempt from non-issue are:
473 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
474 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
475 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
479 sub non_issues_charges {
482 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
483 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
486 push @not_fines, 'Res'
487 unless C4::Context->preference('HoldsInNoissuesCharge');
488 push @not_fines, 'Rent'
489 unless C4::Context->preference('RentalsInNoissuesCharge');
490 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
491 my $dbh = C4::Context->dbh;
494 $dbh->selectcol_arrayref(q|
495 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
499 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
501 return $self->lines->search(
503 accounttype => { -not_in => \@not_fines }
505 )->total_outstanding;
510 my $lines = $self->lines;
512 Return all credits and debits for the user, outstanding or otherwise
519 return Koha::Account::Lines->search(
521 borrowernumber => $self->{patron_id},
526 =head3 reconcile_balance
528 $account->reconcile_balance();
530 Find outstanding credits and use them to pay outstanding debits.
531 Currently, this implicitly uses the 'First In First Out' rule for
532 applying credits against debits.
536 sub reconcile_balance {
539 my $outstanding_debits = $self->outstanding_debits;
540 my $outstanding_credits = $self->outstanding_credits;
542 while ( $outstanding_debits->total_outstanding > 0
543 and my $credit = $outstanding_credits->next )
545 # there's both outstanding debits and credits
546 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
548 $outstanding_debits = $self->outstanding_debits;
564 'credit' => 'Manual Credit',
565 'forgiven' => 'Writeoff',
566 'lost_item_return' => 'Lost Item',
567 'payment' => 'Payment',
568 'writeoff' => 'Writeoff'
575 our $account_type = {
578 'lost_item_return' => 'CR',
585 Kyle M Hall <kyle.m.hall@gmail.com>