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(
60 description => $description,
61 library_id => $branchcode,
62 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
63 account_type => $type, # accounttype code
64 offset_type => $offset_type, # offset type code
71 my ( $self, $params ) = @_;
73 my $amount = $params->{amount};
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';
82 my $cash_register = $params->{cash_register};
84 my $userenv = C4::Context->userenv;
86 my $patron = Koha::Patrons->find( $self->{patron_id} );
88 my $manager_id = $userenv ? $userenv->{number} : 0;
89 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
90 Koha::Exceptions::Account::RegisterRequired->throw()
91 if ( C4::Context->preference("UseCashRegisters")
92 && defined($payment_type)
93 && ( $payment_type eq 'CASH' )
94 && !defined($cash_register) );
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 # Same logic exists in Koha::Account::Line::apply
116 if ( $new_amountoutstanding == 0
118 && $fine->debit_type_code
119 && ( $fine->debit_type_code eq 'LOST' ) )
121 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
124 my $account_offset = Koha::Account::Offset->new(
126 debit_id => $fine->id,
127 type => $offset_type,
128 amount => $amount_to_pay * -1,
131 push( @account_offsets, $account_offset );
133 if ( C4::Context->preference("FinesLog") ) {
139 action => 'fee_payment',
140 borrowernumber => $fine->borrowernumber,
141 old_amountoutstanding => $old_amountoutstanding,
142 new_amountoutstanding => 0,
143 amount_paid => $old_amountoutstanding,
144 accountlines_id => $fine->id,
145 manager_id => $manager_id,
151 push( @fines_paid, $fine->id );
155 # Were not passed a specific line to pay, or the payment was for more
156 # than the what was owed on the given line. In that case pay down other
157 # lines with remaining balance.
158 my @outstanding_fines;
159 @outstanding_fines = $self->lines->search(
161 amountoutstanding => { '>' => 0 },
163 ) if $balance_remaining > 0;
165 foreach my $fine (@outstanding_fines) {
167 $fine->amountoutstanding > $balance_remaining
169 : $fine->amountoutstanding;
171 my $old_amountoutstanding = $fine->amountoutstanding;
172 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
175 if ( $fine->amountoutstanding == 0
177 && $fine->debit_type_code
178 && ( $fine->debit_type_code eq 'LOST' ) )
180 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
183 my $account_offset = Koha::Account::Offset->new(
185 debit_id => $fine->id,
186 type => $offset_type,
187 amount => $amount_to_pay * -1,
190 push( @account_offsets, $account_offset );
192 if ( C4::Context->preference("FinesLog") ) {
198 action => "fee_$type",
199 borrowernumber => $fine->borrowernumber,
200 old_amountoutstanding => $old_amountoutstanding,
201 new_amountoutstanding => $fine->amountoutstanding,
202 amount_paid => $amount_to_pay,
203 accountlines_id => $fine->id,
204 manager_id => $manager_id,
210 push( @fines_paid, $fine->id );
213 $balance_remaining = $balance_remaining - $amount_to_pay;
214 last unless $balance_remaining > 0;
222 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
224 my $payment = Koha::Account::Line->new(
226 borrowernumber => $self->{patron_id},
227 date => dt_from_string(),
228 amount => 0 - $amount,
229 description => $description,
230 accounttype => $account_type,
231 payment_type => $payment_type,
232 amountoutstanding => 0 - $balance_remaining,
233 manager_id => $manager_id,
234 interface => $interface,
235 branchcode => $library_id,
236 register_id => $cash_register,
241 foreach my $o ( @account_offsets ) {
242 $o->credit_id( $payment->id() );
248 branch => $library_id,
251 borrowernumber => $self->{patron_id},
255 if ( C4::Context->preference("FinesLog") ) {
261 action => "create_$type",
262 borrowernumber => $self->{patron_id},
263 amount => 0 - $amount,
264 amountoutstanding => 0 - $balance_remaining,
265 accounttype => $account_type,
266 accountlines_paid => \@fines_paid,
267 manager_id => $manager_id,
274 if ( C4::Context->preference('UseEmailReceipts') ) {
276 my $letter = C4::Letters::GetPreparedLetter(
277 module => 'circulation',
278 letter_code => uc("ACCOUNT_$type"),
279 message_transport_type => 'email',
280 lang => $patron->lang,
282 borrowers => $self->{patron_id},
283 branches => $self->{library_id},
287 offsets => \@account_offsets,
292 C4::Letters::EnqueueLetter(
295 borrowernumber => $self->{patron_id},
296 message_transport_type => 'email',
298 ) or warn "can't enqueue letter $letter";
307 This method allows adding credits to a patron's account
309 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
312 description => $description,
315 interface => $interface,
316 library_id => $library_id,
317 payment_type => $payment_type,
318 type => $credit_type,
323 $credit_type can be any of:
334 my ( $self, $params ) = @_;
336 # amount is passed as a positive value, but we store credit as negative values
337 my $amount = $params->{amount} * -1;
338 my $description = $params->{description} // q{};
339 my $note = $params->{note} // q{};
340 my $user_id = $params->{user_id};
341 my $interface = $params->{interface};
342 my $library_id = $params->{library_id};
343 my $cash_register = $params->{cash_register};
344 my $payment_type = $params->{payment_type};
345 my $type = $params->{type} || 'payment';
346 my $item_id = $params->{item_id};
348 unless ( $interface ) {
349 Koha::Exceptions::MissingParameter->throw(
350 error => 'The interface parameter is mandatory'
354 Koha::Exceptions::Account::RegisterRequired->throw()
355 if ( C4::Context->preference("UseCashRegisters")
356 && defined($payment_type)
357 && ( $payment_type eq 'CASH' )
358 && !defined($cash_register) );
360 my $schema = Koha::Database->new->schema;
362 my $account_type = $Koha::Account::account_type_credit->{$type};
368 # Insert the account line
369 $line = Koha::Account::Line->new(
370 { borrowernumber => $self->{patron_id},
373 description => $description,
374 accounttype => $account_type,
375 amountoutstanding => $amount,
376 payment_type => $payment_type,
378 manager_id => $user_id,
379 interface => $interface,
380 branchcode => $library_id,
381 register_id => $cash_register,
382 itemnumber => $item_id,
386 # Record the account offset
387 my $account_offset = Koha::Account::Offset->new(
388 { credit_id => $line->id,
389 type => $Koha::Account::offset_type->{$type},
395 { branch => $library_id,
398 borrowernumber => $self->{patron_id},
400 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
402 if ( C4::Context->preference("FinesLog") ) {
407 { action => "create_$type",
408 borrowernumber => $self->{patron_id},
410 description => $description,
411 amountoutstanding => $amount,
412 accounttype => $account_type,
414 itemnumber => $item_id,
415 manager_id => $user_id,
416 branchcode => $library_id,
430 This method allows adding debits to a patron's account
432 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
435 description => $description,
438 interface => $interface,
439 library_id => $library_id,
442 issue_id => $issue_id
446 $debit_type can be any of:
466 my ( $self, $params ) = @_;
468 # amount should always be a positive value
469 my $amount = $params->{amount};
471 unless ( $amount > 0 ) {
472 Koha::Exceptions::Account::AmountNotPositive->throw(
473 error => 'Debit amount passed is not positive' );
476 my $description = $params->{description} // q{};
477 my $note = $params->{note} // q{};
478 my $user_id = $params->{user_id};
479 my $interface = $params->{interface};
480 my $library_id = $params->{library_id};
481 my $type = $params->{type};
482 my $item_id = $params->{item_id};
483 my $issue_id = $params->{issue_id};
485 unless ($interface) {
486 Koha::Exceptions::MissingParameter->throw(
487 error => 'The interface parameter is mandatory' );
490 my $schema = Koha::Database->new->schema;
492 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
493 Koha::Exceptions::Account::UnrecognisedType->throw(
494 error => 'Type of debit not recognised'
498 my $debit_type_code = $Koha::Account::account_type_debit->{$type};
504 # Insert the account line
505 $line = Koha::Account::Line->new(
507 borrowernumber => $self->{patron_id},
510 description => $description,
511 debit_type_code => $debit_type_code,
512 amountoutstanding => $amount,
513 payment_type => undef,
515 manager_id => $user_id,
516 interface => $interface,
517 itemnumber => $item_id,
518 issue_id => $issue_id,
519 branchcode => $library_id,
520 ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : () ),
524 # Record the account offset
525 my $account_offset = Koha::Account::Offset->new(
527 debit_id => $line->id,
528 type => $Koha::Account::offset_type->{$type},
533 if ( C4::Context->preference("FinesLog") ) {
539 action => "create_$type",
540 borrowernumber => $self->{patron_id},
542 description => $description,
543 amountoutstanding => $amount,
544 debit_type_code => $debit_type_code,
546 itemnumber => $item_id,
547 manager_id => $user_id,
561 my $balance = $self->balance
563 Return the balance (sum of amountoutstanding columns)
569 return $self->lines->total_outstanding;
572 =head3 outstanding_debits
574 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
576 It returns the debit lines with outstanding amounts for the patron.
578 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
579 return a list of Koha::Account::Line objects.
583 sub outstanding_debits {
586 return $self->lines->search(
588 amount => { '>' => 0 },
589 amountoutstanding => { '>' => 0 }
594 =head3 outstanding_credits
596 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
598 It returns the credit lines with outstanding amounts for the patron.
600 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
601 return a list of Koha::Account::Line objects.
605 sub outstanding_credits {
608 return $self->lines->search(
610 amount => { '<' => 0 },
611 amountoutstanding => { '<' => 0 }
616 =head3 non_issues_charges
618 my $non_issues_charges = $self->non_issues_charges
620 Calculates amount immediately owing by the patron - non-issue charges.
622 Charges exempt from non-issue are:
623 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
624 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
625 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
629 sub non_issues_charges {
632 #NOTE: With bug 23049 these preferences could be moved to being attached
633 #to individual debit types to give more flexability and specificity.
635 push @not_fines, 'RESERVE'
636 unless C4::Context->preference('HoldsInNoissuesCharge');
637 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
638 unless C4::Context->preference('RentalsInNoissuesCharge');
639 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
640 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
641 push @not_fines, @man_inv;
644 return $self->lines->search(
646 debit_type_code => { -not_in => \@not_fines }
648 )->total_outstanding;
653 my $lines = $self->lines;
655 Return all credits and debits for the user, outstanding or otherwise
662 return Koha::Account::Lines->search(
664 borrowernumber => $self->{patron_id},
669 =head3 reconcile_balance
671 $account->reconcile_balance();
673 Find outstanding credits and use them to pay outstanding debits.
674 Currently, this implicitly uses the 'First In First Out' rule for
675 applying credits against debits.
679 sub reconcile_balance {
682 my $outstanding_debits = $self->outstanding_debits;
683 my $outstanding_credits = $self->outstanding_credits;
685 while ( $outstanding_debits->total_outstanding > 0
686 and my $credit = $outstanding_credits->next )
688 # there's both outstanding debits and credits
689 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
691 $outstanding_debits = $self->outstanding_debits;
707 'credit' => 'Manual Credit',
708 'forgiven' => 'Writeoff',
709 'lost_item_return' => 'Lost Item',
710 'payment' => 'Payment',
711 'writeoff' => 'Writeoff',
712 'account' => 'Account Fee',
713 'account_renew' => 'Account Fee',
714 'RESERVE' => 'Reserve Fee',
715 'processing' => 'Processing Fee',
716 'lost_item' => 'Lost Item',
717 'RENT' => 'Rental Fee',
718 'RENT_DAILY' => 'Rental Fee',
719 'RENT_RENEW' => 'Rental Fee',
720 'RENT_DAILY_RENEW' => 'Rental Fee',
721 'overdue' => 'OVERDUE',
722 'manual_debit' => 'Manual Debit',
723 'hold_expired' => 'Hold Expired'
726 =head3 $account_type_credit
730 our $account_type_credit = {
733 'lost_item_return' => 'LOST_RETURN',
738 =head3 $account_type_debit
742 our $account_type_debit = {
743 'account' => 'ACCOUNT',
744 'account_renew' => 'ACCOUNT_RENEW',
745 'hold_expired' => 'HE',
746 'lost_item' => 'LOST',
749 'overdue' => 'OVERDUE',
750 'processing' => 'PF',
752 'RENT_DAILY' => 'RENT_DAILY',
753 'RENT_RENEW' => 'RENT_RENEW',
754 'RENT_DAILY_RENEW' => 'RENT_DAILY_RENEW',
755 'RESERVE' => 'RESERVE',
756 'manual_debit' => 'M'
763 Kyle M Hall <kyle.m.hall@gmail.com>
764 Tomás Cohen Arazi <tomascohen@gmail.com>
765 Martin Renvoize <martin.renvoize@ptfs-europe.com>