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 );
27 use C4::Circulation qw( ReturnLostItem );
29 use C4::Log qw( logaction );
30 use C4::Stats qw( UpdateStats );
33 use Koha::Account::Lines;
34 use Koha::Account::Offsets;
35 use Koha::DateUtils qw( dt_from_string );
37 use Koha::Exceptions::Account;
41 Koha::Accounts - Module for managing payments and fees for patrons
46 my ( $class, $params ) = @_;
48 Carp::croak("No patron id passed in!") unless $params->{patron_id};
50 return bless( $params, $class );
55 This method allows payments to be made against fees/fines
57 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 credit_type => $type, # credit_type_code code
65 offset_type => $offset_type, # offset type code
72 my ( $self, $params ) = @_;
74 my $amount = $params->{amount};
75 my $description = $params->{description};
76 my $note = $params->{note} || q{};
77 my $library_id = $params->{library_id};
78 my $lines = $params->{lines};
79 my $type = $params->{type} || 'payment';
80 my $payment_type = $params->{payment_type} || undef;
81 my $credit_type = $params->{credit_type};
82 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
83 my $cash_register = $params->{cash_register};
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;
91 Koha::Exceptions::Account::RegisterRequired->throw()
92 if ( C4::Context->preference("UseCashRegisters")
93 && defined($payment_type)
94 && ( $payment_type eq 'CASH' )
95 && !defined($cash_register) );
97 my @fines_paid; # List of account lines paid on with this payment
99 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
100 $balance_remaining ||= 0;
104 # We were passed a specific line to pay
105 foreach my $fine ( @$lines ) {
107 $fine->amountoutstanding > $balance_remaining
109 : $fine->amountoutstanding;
111 my $old_amountoutstanding = $fine->amountoutstanding;
112 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
113 $fine->amountoutstanding($new_amountoutstanding)->store();
114 $balance_remaining = $balance_remaining - $amount_to_pay;
116 # Same logic exists in Koha::Account::Line::apply
117 if ( $new_amountoutstanding == 0
119 && $fine->debit_type_code
120 && ( $fine->debit_type_code eq 'LOST' ) )
122 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
125 my $account_offset = Koha::Account::Offset->new(
127 debit_id => $fine->id,
128 type => $offset_type,
129 amount => $amount_to_pay * -1,
132 push( @account_offsets, $account_offset );
134 if ( C4::Context->preference("FinesLog") ) {
140 action => 'fee_payment',
141 borrowernumber => $fine->borrowernumber,
142 old_amountoutstanding => $old_amountoutstanding,
143 new_amountoutstanding => 0,
144 amount_paid => $old_amountoutstanding,
145 accountlines_id => $fine->id,
146 manager_id => $manager_id,
152 push( @fines_paid, $fine->id );
156 # Were not passed a specific line to pay, or the payment was for more
157 # than the what was owed on the given line. In that case pay down other
158 # lines with remaining balance.
159 my @outstanding_fines;
160 @outstanding_fines = $self->lines->search(
162 amountoutstanding => { '>' => 0 },
164 ) if $balance_remaining > 0;
166 foreach my $fine (@outstanding_fines) {
168 $fine->amountoutstanding > $balance_remaining
170 : $fine->amountoutstanding;
172 my $old_amountoutstanding = $fine->amountoutstanding;
173 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
176 if ( $fine->amountoutstanding == 0
178 && $fine->debit_type_code
179 && ( $fine->debit_type_code eq 'LOST' ) )
181 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
184 my $account_offset = Koha::Account::Offset->new(
186 debit_id => $fine->id,
187 type => $offset_type,
188 amount => $amount_to_pay * -1,
191 push( @account_offsets, $account_offset );
193 if ( C4::Context->preference("FinesLog") ) {
199 action => "fee_$type",
200 borrowernumber => $fine->borrowernumber,
201 old_amountoutstanding => $old_amountoutstanding,
202 new_amountoutstanding => $fine->amountoutstanding,
203 amount_paid => $amount_to_pay,
204 accountlines_id => $fine->id,
205 manager_id => $manager_id,
211 push( @fines_paid, $fine->id );
214 $balance_remaining = $balance_remaining - $amount_to_pay;
215 last unless $balance_remaining > 0;
223 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
225 my $payment = Koha::Account::Line->new(
227 borrowernumber => $self->{patron_id},
228 date => dt_from_string(),
229 amount => 0 - $amount,
230 description => $description,
231 credit_type_code => $credit_type,
232 payment_type => $payment_type,
233 amountoutstanding => 0 - $balance_remaining,
234 manager_id => $manager_id,
235 interface => $interface,
236 branchcode => $library_id,
237 register_id => $cash_register,
242 foreach my $o ( @account_offsets ) {
243 $o->credit_id( $payment->id() );
249 branch => $library_id,
252 borrowernumber => $self->{patron_id},
256 if ( C4::Context->preference("FinesLog") ) {
262 action => "create_$type",
263 borrowernumber => $self->{patron_id},
264 amount => 0 - $amount,
265 amountoutstanding => 0 - $balance_remaining,
266 credit_type_code => $credit_type,
267 accountlines_paid => \@fines_paid,
268 manager_id => $manager_id,
275 if ( C4::Context->preference('UseEmailReceipts') ) {
277 my $letter = C4::Letters::GetPreparedLetter(
278 module => 'circulation',
279 letter_code => uc("ACCOUNT_$type"),
280 message_transport_type => 'email',
281 lang => $patron->lang,
283 borrowers => $self->{patron_id},
284 branches => $self->{library_id},
288 offsets => \@account_offsets,
293 C4::Letters::EnqueueLetter(
296 borrowernumber => $self->{patron_id},
297 message_transport_type => 'email',
299 ) or warn "can't enqueue letter $letter";
308 This method allows adding credits to a patron's account
310 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
313 description => $description,
316 interface => $interface,
317 library_id => $library_id,
318 payment_type => $payment_type,
319 type => $credit_type,
324 $credit_type can be any of:
335 my ( $self, $params ) = @_;
337 # amount is passed as a positive value, but we store credit as negative values
338 my $amount = $params->{amount} * -1;
339 my $description = $params->{description} // q{};
340 my $note = $params->{note} // q{};
341 my $user_id = $params->{user_id};
342 my $interface = $params->{interface};
343 my $library_id = $params->{library_id};
344 my $cash_register = $params->{cash_register};
345 my $payment_type = $params->{payment_type};
346 my $type = $params->{type} || 'payment';
347 my $item_id = $params->{item_id};
349 unless ( $interface ) {
350 Koha::Exceptions::MissingParameter->throw(
351 error => 'The interface parameter is mandatory'
355 Koha::Exceptions::Account::RegisterRequired->throw()
356 if ( C4::Context->preference("UseCashRegisters")
357 && defined($payment_type)
358 && ( $payment_type eq 'CASH' )
359 && !defined($cash_register) );
361 my $schema = Koha::Database->new->schema;
363 my $credit_type = $Koha::Account::account_type_credit->{$type};
369 # Insert the account line
370 $line = Koha::Account::Line->new(
371 { borrowernumber => $self->{patron_id},
374 description => $description,
375 credit_type_code => $credit_type,
376 amountoutstanding => $amount,
377 payment_type => $payment_type,
379 manager_id => $user_id,
380 interface => $interface,
381 branchcode => $library_id,
382 register_id => $cash_register,
383 itemnumber => $item_id,
387 # Record the account offset
388 my $account_offset = Koha::Account::Offset->new(
389 { credit_id => $line->id,
390 type => $Koha::Account::offset_type->{$type},
396 { branch => $library_id,
399 borrowernumber => $self->{patron_id},
401 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
403 if ( C4::Context->preference("FinesLog") ) {
408 { action => "create_$type",
409 borrowernumber => $self->{patron_id},
411 description => $description,
412 amountoutstanding => $amount,
413 credit_type_code => $credit_type,
415 itemnumber => $item_id,
416 manager_id => $user_id,
417 branchcode => $library_id,
431 This method allows adding debits to a patron's account
433 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
436 description => $description,
439 interface => $interface,
440 library_id => $library_id,
443 issue_id => $issue_id
447 $debit_type can be any of:
466 my ( $self, $params ) = @_;
468 # check for mandatory params
469 my @mandatory = ( 'interface', 'type', 'amount' );
470 for my $param (@mandatory) {
471 unless ( defined( $params->{$param} ) ) {
472 Koha::Exceptions::MissingParameter->throw(
473 error => "The $param parameter is mandatory" );
477 # amount should always be a positive value
478 my $amount = $params->{amount};
479 unless ( $amount > 0 ) {
480 Koha::Exceptions::Account::AmountNotPositive->throw(
481 error => 'Debit amount passed is not positive' );
484 my $description = $params->{description} // q{};
485 my $note = $params->{note} // q{};
486 my $user_id = $params->{user_id};
487 my $interface = $params->{interface};
488 my $library_id = $params->{library_id};
489 my $debit_type = $params->{type};
490 my $item_id = $params->{item_id};
491 my $issue_id = $params->{issue_id};
492 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
495 my $schema = Koha::Database->new->schema;
500 # Insert the account line
501 $line = Koha::Account::Line->new(
503 borrowernumber => $self->{patron_id},
506 description => $description,
507 debit_type_code => $debit_type,
508 amountoutstanding => $amount,
509 payment_type => undef,
511 manager_id => $user_id,
512 interface => $interface,
513 itemnumber => $item_id,
514 issue_id => $issue_id,
515 branchcode => $library_id,
517 $debit_type eq 'OVERDUE'
518 ? ( status => 'UNRETURNED' )
524 # Record the account offset
525 my $account_offset = Koha::Account::Offset->new(
527 debit_id => $line->id,
528 type => $offset_type,
533 if ( C4::Context->preference("FinesLog") ) {
539 action => "create_$debit_type",
540 borrowernumber => $self->{patron_id},
542 description => $description,
543 amountoutstanding => $amount,
544 debit_type_code => $debit_type,
546 itemnumber => $item_id,
547 manager_id => $user_id,
557 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
558 if ( $_->broken_fk eq 'debit_type_code' ) {
559 Koha::Exceptions::Account::UnrecognisedType->throw(
560 error => 'Type of debit not recognised' );
573 my $balance = $self->balance
575 Return the balance (sum of amountoutstanding columns)
581 return $self->lines->total_outstanding;
584 =head3 outstanding_debits
586 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
588 It returns the debit lines with outstanding amounts for the patron.
590 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
591 return a list of Koha::Account::Line objects.
595 sub outstanding_debits {
598 return $self->lines->search(
600 amount => { '>' => 0 },
601 amountoutstanding => { '>' => 0 }
606 =head3 outstanding_credits
608 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
610 It returns the credit lines with outstanding amounts for the patron.
612 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
613 return a list of Koha::Account::Line objects.
617 sub outstanding_credits {
620 return $self->lines->search(
622 amount => { '<' => 0 },
623 amountoutstanding => { '<' => 0 }
628 =head3 non_issues_charges
630 my $non_issues_charges = $self->non_issues_charges
632 Calculates amount immediately owing by the patron - non-issue charges.
634 Charges exempt from non-issue are:
635 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
636 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
637 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
641 sub non_issues_charges {
644 #NOTE: With bug 23049 these preferences could be moved to being attached
645 #to individual debit types to give more flexability and specificity.
647 push @not_fines, 'RESERVE'
648 unless C4::Context->preference('HoldsInNoissuesCharge');
649 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
650 unless C4::Context->preference('RentalsInNoissuesCharge');
651 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
652 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
653 push @not_fines, @man_inv;
656 return $self->lines->search(
658 debit_type_code => { -not_in => \@not_fines }
660 )->total_outstanding;
665 my $lines = $self->lines;
667 Return all credits and debits for the user, outstanding or otherwise
674 return Koha::Account::Lines->search(
676 borrowernumber => $self->{patron_id},
681 =head3 reconcile_balance
683 $account->reconcile_balance();
685 Find outstanding credits and use them to pay outstanding debits.
686 Currently, this implicitly uses the 'First In First Out' rule for
687 applying credits against debits.
691 sub reconcile_balance {
694 my $outstanding_debits = $self->outstanding_debits;
695 my $outstanding_credits = $self->outstanding_credits;
697 while ( $outstanding_debits->total_outstanding > 0
698 and my $credit = $outstanding_credits->next )
700 # there's both outstanding debits and credits
701 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
703 $outstanding_debits = $self->outstanding_debits;
719 'CREDIT' => 'Manual Credit',
720 'forgiven' => 'Writeoff',
721 'lost_item_return' => 'Lost Item',
722 'payment' => 'Payment',
723 'writeoff' => 'Writeoff',
724 'ACCOUNT' => 'Account Fee',
725 'ACCOUNT_RENEW' => 'Account Fee',
726 'RESERVE' => 'Reserve Fee',
727 'PROCESSING' => 'Processing Fee',
728 'LOST' => 'Lost Item',
729 'RENT' => 'Rental Fee',
730 'RENT_DAILY' => 'Rental Fee',
731 'RENT_RENEW' => 'Rental Fee',
732 'RENT_DAILY_RENEW' => 'Rental Fee',
733 'OVERDUE' => 'OVERDUE',
734 'RESERVE_EXPIRED' => 'Hold Expired'
737 =head3 $account_type_credit
741 our $account_type_credit = {
742 'CREDIT' => 'CREDIT',
744 'lost_item_return' => 'LOST_RETURN',
753 Kyle M Hall <kyle.m.hall@gmail.com>
754 Tomás Cohen Arazi <tomascohen@gmail.com>
755 Martin Renvoize <martin.renvoize@ptfs-europe.com>