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 );
36 use Koha::Exceptions::Account;
40 Koha::Accounts - Module for managing payments and fees for patrons
45 my ( $class, $params ) = @_;
47 Carp::croak("No patron id passed in!") unless $params->{patron_id};
49 return bless( $params, $class );
54 This method allows payments to be made against fees/fines
56 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
61 description => $description,
62 library_id => $branchcode,
63 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
64 account_type => $type, # accounttype code
65 offset_type => $offset_type, # offset type code
72 my ( $self, $params ) = @_;
74 my $amount = $params->{amount};
75 my $sip = $params->{sip};
76 my $description = $params->{description};
77 my $note = $params->{note} || q{};
78 my $library_id = $params->{library_id};
79 my $lines = $params->{lines};
80 my $type = $params->{type} || 'payment';
81 my $payment_type = $params->{payment_type} || undef;
82 my $account_type = $params->{account_type};
83 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
85 my $userenv = C4::Context->userenv;
87 my $patron = Koha::Patrons->find( $self->{patron_id} );
89 my $manager_id = $userenv ? $userenv->{number} : 0;
90 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
92 my @fines_paid; # List of account lines paid on with this payment
94 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
95 $balance_remaining ||= 0;
99 # We were passed a specific line to pay
100 foreach my $fine ( @$lines ) {
102 $fine->amountoutstanding > $balance_remaining
104 : $fine->amountoutstanding;
106 my $old_amountoutstanding = $fine->amountoutstanding;
107 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
108 $fine->amountoutstanding($new_amountoutstanding)->store();
109 $balance_remaining = $balance_remaining - $amount_to_pay;
111 if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
113 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
116 my $account_offset = Koha::Account::Offset->new(
118 debit_id => $fine->id,
119 type => $offset_type,
120 amount => $amount_to_pay * -1,
123 push( @account_offsets, $account_offset );
125 if ( C4::Context->preference("FinesLog") ) {
131 action => 'fee_payment',
132 borrowernumber => $fine->borrowernumber,
133 old_amountoutstanding => $old_amountoutstanding,
134 new_amountoutstanding => 0,
135 amount_paid => $old_amountoutstanding,
136 accountlines_id => $fine->id,
137 manager_id => $manager_id,
143 push( @fines_paid, $fine->id );
147 # Were not passed a specific line to pay, or the payment was for more
148 # than the what was owed on the given line. In that case pay down other
149 # lines with remaining balance.
150 my @outstanding_fines;
151 @outstanding_fines = $self->lines->search(
153 amountoutstanding => { '>' => 0 },
155 ) if $balance_remaining > 0;
157 foreach my $fine (@outstanding_fines) {
159 $fine->amountoutstanding > $balance_remaining
161 : $fine->amountoutstanding;
163 my $old_amountoutstanding = $fine->amountoutstanding;
164 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
167 my $account_offset = Koha::Account::Offset->new(
169 debit_id => $fine->id,
170 type => $offset_type,
171 amount => $amount_to_pay * -1,
174 push( @account_offsets, $account_offset );
176 if ( C4::Context->preference("FinesLog") ) {
182 action => "fee_$type",
183 borrowernumber => $fine->borrowernumber,
184 old_amountoutstanding => $old_amountoutstanding,
185 new_amountoutstanding => $fine->amountoutstanding,
186 amount_paid => $amount_to_pay,
187 accountlines_id => $fine->id,
188 manager_id => $manager_id,
194 push( @fines_paid, $fine->id );
197 $balance_remaining = $balance_remaining - $amount_to_pay;
198 last unless $balance_remaining > 0;
202 $type eq 'writeoff' ? 'W'
203 : defined($sip) ? "Pay$sip"
206 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
208 my $payment = Koha::Account::Line->new(
210 borrowernumber => $self->{patron_id},
211 date => dt_from_string(),
212 amount => 0 - $amount,
213 description => $description,
214 accounttype => $account_type,
215 payment_type => $payment_type,
216 amountoutstanding => 0 - $balance_remaining,
217 manager_id => $manager_id,
218 interface => $interface,
219 branchcode => $library_id,
224 foreach my $o ( @account_offsets ) {
225 $o->credit_id( $payment->id() );
231 branch => $library_id,
234 borrowernumber => $self->{patron_id},
238 if ( C4::Context->preference("FinesLog") ) {
244 action => "create_$type",
245 borrowernumber => $self->{patron_id},
246 amount => 0 - $amount,
247 amountoutstanding => 0 - $balance_remaining,
248 accounttype => $account_type,
249 accountlines_paid => \@fines_paid,
250 manager_id => $manager_id,
257 if ( C4::Context->preference('UseEmailReceipts') ) {
259 my $letter = C4::Letters::GetPreparedLetter(
260 module => 'circulation',
261 letter_code => uc("ACCOUNT_$type"),
262 message_transport_type => 'email',
263 lang => $patron->lang,
265 borrowers => $self->{patron_id},
266 branches => $self->{library_id},
270 offsets => \@account_offsets,
275 C4::Letters::EnqueueLetter(
278 borrowernumber => $self->{patron_id},
279 message_transport_type => 'email',
281 ) or warn "can't enqueue letter $letter";
290 This method allows adding credits to a patron's account
292 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
295 description => $description,
298 interface => $interface,
299 library_id => $library_id,
301 payment_type => $payment_type,
302 type => $credit_type,
307 $credit_type can be any of:
318 my ( $self, $params ) = @_;
320 # amount is passed as a positive value, but we store credit as negative values
321 my $amount = $params->{amount} * -1;
322 my $description = $params->{description} // q{};
323 my $note = $params->{note} // q{};
324 my $user_id = $params->{user_id};
325 my $interface = $params->{interface};
326 my $library_id = $params->{library_id};
327 my $sip = $params->{sip};
328 my $payment_type = $params->{payment_type};
329 my $type = $params->{type} || 'payment';
330 my $item_id = $params->{item_id};
332 unless ( $interface ) {
333 Koha::Exceptions::MissingParameter->throw(
334 error => 'The interface parameter is mandatory'
338 my $schema = Koha::Database->new->schema;
340 my $account_type = $Koha::Account::account_type_credit->{$type};
341 $account_type .= $sip
350 # Insert the account line
351 $line = Koha::Account::Line->new(
352 { borrowernumber => $self->{patron_id},
355 description => $description,
356 accounttype => $account_type,
357 amountoutstanding => $amount,
358 payment_type => $payment_type,
360 manager_id => $user_id,
361 interface => $interface,
362 branchcode => $library_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},
381 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
383 if ( C4::Context->preference("FinesLog") ) {
388 { action => "create_$type",
389 borrowernumber => $self->{patron_id},
391 description => $description,
392 amountoutstanding => $amount,
393 accounttype => $account_type,
395 itemnumber => $item_id,
396 manager_id => $user_id,
397 branchcode => $library_id,
411 This method allows adding debits to a patron's account
413 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
416 description => $description,
419 interface => $interface,
420 library_id => $library_id,
423 issue_id => $issue_id
427 $debit_type can be any of:
442 my ( $self, $params ) = @_;
444 # amount should always be a positive value
445 my $amount = $params->{amount};
447 unless ( $amount > 0 ) {
448 Koha::Exceptions::Account::AmountNotPositive->throw(
449 error => 'Debit amount passed is not positive'
453 my $description = $params->{description} // q{};
454 my $note = $params->{note} // q{};
455 my $user_id = $params->{user_id};
456 my $interface = $params->{interface};
457 my $library_id = $params->{library_id};
458 my $type = $params->{type};
459 my $item_id = $params->{item_id};
460 my $issue_id = $params->{issue_id};
462 unless ( $interface ) {
463 Koha::Exceptions::MissingParameter->throw(
464 error => 'The interface parameter is mandatory'
468 my $schema = Koha::Database->new->schema;
470 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
471 Koha::Exceptions::Account::UnrecognisedType->throw(
472 error => 'Type of debit not recognised'
476 my $account_type = $Koha::Account::account_type_debit->{$type};
483 # Insert the account line
484 $line = Koha::Account::Line->new(
485 { borrowernumber => $self->{patron_id},
488 description => $description,
489 accounttype => $account_type,
490 amountoutstanding => $amount,
491 payment_type => undef,
493 manager_id => $user_id,
494 interface => $interface,
495 itemnumber => $item_id,
496 issue_id => $issue_id,
497 branchcode => $library_id,
501 # Record the account offset
502 my $account_offset = Koha::Account::Offset->new(
503 { debit_id => $line->id,
504 type => $Koha::Account::offset_type->{$type},
509 if ( C4::Context->preference("FinesLog") ) {
514 { action => "create_$type",
515 borrowernumber => $self->{patron_id},
517 description => $description,
518 amountoutstanding => $amount,
519 accounttype => $account_type,
521 itemnumber => $item_id,
522 manager_id => $user_id,
536 my $balance = $self->balance
538 Return the balance (sum of amountoutstanding columns)
544 return $self->lines->total_outstanding;
547 =head3 outstanding_debits
549 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
551 It returns the debit lines with outstanding amounts for the patron.
553 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
554 return a list of Koha::Account::Line objects.
558 sub outstanding_debits {
561 return $self->lines->search(
563 amount => { '>' => 0 },
564 amountoutstanding => { '>' => 0 }
569 =head3 outstanding_credits
571 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
573 It returns the credit lines with outstanding amounts for the patron.
575 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
576 return a list of Koha::Account::Line objects.
580 sub outstanding_credits {
583 return $self->lines->search(
585 amount => { '<' => 0 },
586 amountoutstanding => { '<' => 0 }
591 =head3 non_issues_charges
593 my $non_issues_charges = $self->non_issues_charges
595 Calculates amount immediately owing by the patron - non-issue charges.
597 Charges exempt from non-issue are:
598 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
599 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
600 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
604 sub non_issues_charges {
607 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
608 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
611 push @not_fines, 'Res'
612 unless C4::Context->preference('HoldsInNoissuesCharge');
613 push @not_fines, 'Rent'
614 unless C4::Context->preference('RentalsInNoissuesCharge');
615 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
616 my $dbh = C4::Context->dbh;
619 $dbh->selectcol_arrayref(q|
620 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
624 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
626 return $self->lines->search(
628 accounttype => { -not_in => \@not_fines }
630 )->total_outstanding;
635 my $lines = $self->lines;
637 Return all credits and debits for the user, outstanding or otherwise
644 return Koha::Account::Lines->search(
646 borrowernumber => $self->{patron_id},
651 =head3 reconcile_balance
653 $account->reconcile_balance();
655 Find outstanding credits and use them to pay outstanding debits.
656 Currently, this implicitly uses the 'First In First Out' rule for
657 applying credits against debits.
661 sub reconcile_balance {
664 my $outstanding_debits = $self->outstanding_debits;
665 my $outstanding_credits = $self->outstanding_credits;
667 while ( $outstanding_debits->total_outstanding > 0
668 and my $credit = $outstanding_credits->next )
670 # there's both outstanding debits and credits
671 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
673 $outstanding_debits = $self->outstanding_debits;
689 'credit' => 'Manual Credit',
690 'forgiven' => 'Writeoff',
691 'lost_item_return' => 'Lost Item',
692 'payment' => 'Payment',
693 'writeoff' => 'Writeoff',
694 'account' => 'Account Fee',
695 'reserve' => 'Reserve Fee',
696 'processing' => 'Processing Fee',
697 'lost_item' => 'Lost Item',
698 'rent' => 'Rental Fee',
700 'manual_debit' => 'Manual Debit',
701 'hold_expired' => 'Hold Expired'
704 =head3 $account_type_credit
708 our $account_type_credit = {
711 'lost_item_return' => 'CR',
716 =head3 $account_type_debit
720 our $account_type_debit = {
726 'processing' => 'PF',
729 'manual_debit' => 'M',
730 'hold_expired' => 'HE'
737 Kyle M Hall <kyle.m.hall@gmail.com>
738 Tomás Cohen Arazi <tomascohen@gmail.com>
739 Martin Renvoize <martin.renvoize@ptfs-europe.com>