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 account_type => $type, # accounttype 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 $account_type = $params->{account_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 accounttype => $account_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 accounttype => $account_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 $account_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 accounttype => $account_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 accounttype => $account_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 # 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 $debit_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 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
499 # Insert the account line
500 $line = Koha::Account::Line->new(
502 borrowernumber => $self->{patron_id},
505 description => $description,
506 debit_type_code => $debit_type,
507 amountoutstanding => $amount,
508 payment_type => undef,
510 manager_id => $user_id,
511 interface => $interface,
512 itemnumber => $item_id,
513 issue_id => $issue_id,
514 branchcode => $library_id,
516 $debit_type eq 'OVERDUE'
517 ? ( status => 'UNRETURNED' )
523 # Record the account offset
524 my $account_offset = Koha::Account::Offset->new(
526 debit_id => $line->id,
527 type => $offset_type,
532 if ( C4::Context->preference("FinesLog") ) {
538 action => "create_$debit_type",
539 borrowernumber => $self->{patron_id},
541 description => $description,
542 amountoutstanding => $amount,
543 debit_type_code => $debit_type,
545 itemnumber => $item_id,
546 manager_id => $user_id,
556 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
557 if ( $_->broken_fk eq 'debit_type_code' ) {
558 Koha::Exceptions::Account::UnrecognisedType->throw(
559 error => 'Type of debit not recognised' );
572 my $balance = $self->balance
574 Return the balance (sum of amountoutstanding columns)
580 return $self->lines->total_outstanding;
583 =head3 outstanding_debits
585 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
587 It returns the debit lines with outstanding amounts for the patron.
589 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
590 return a list of Koha::Account::Line objects.
594 sub outstanding_debits {
597 return $self->lines->search(
599 amount => { '>' => 0 },
600 amountoutstanding => { '>' => 0 }
605 =head3 outstanding_credits
607 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
609 It returns the credit lines with outstanding amounts for the patron.
611 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
612 return a list of Koha::Account::Line objects.
616 sub outstanding_credits {
619 return $self->lines->search(
621 amount => { '<' => 0 },
622 amountoutstanding => { '<' => 0 }
627 =head3 non_issues_charges
629 my $non_issues_charges = $self->non_issues_charges
631 Calculates amount immediately owing by the patron - non-issue charges.
633 Charges exempt from non-issue are:
634 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
635 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
636 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
640 sub non_issues_charges {
643 #NOTE: With bug 23049 these preferences could be moved to being attached
644 #to individual debit types to give more flexability and specificity.
646 push @not_fines, 'RESERVE'
647 unless C4::Context->preference('HoldsInNoissuesCharge');
648 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
649 unless C4::Context->preference('RentalsInNoissuesCharge');
650 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
651 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
652 push @not_fines, @man_inv;
655 return $self->lines->search(
657 debit_type_code => { -not_in => \@not_fines }
659 )->total_outstanding;
664 my $lines = $self->lines;
666 Return all credits and debits for the user, outstanding or otherwise
673 return Koha::Account::Lines->search(
675 borrowernumber => $self->{patron_id},
680 =head3 reconcile_balance
682 $account->reconcile_balance();
684 Find outstanding credits and use them to pay outstanding debits.
685 Currently, this implicitly uses the 'First In First Out' rule for
686 applying credits against debits.
690 sub reconcile_balance {
693 my $outstanding_debits = $self->outstanding_debits;
694 my $outstanding_credits = $self->outstanding_credits;
696 while ( $outstanding_debits->total_outstanding > 0
697 and my $credit = $outstanding_credits->next )
699 # there's both outstanding debits and credits
700 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
702 $outstanding_debits = $self->outstanding_debits;
718 'credit' => 'Manual Credit',
719 'forgiven' => 'Writeoff',
720 'lost_item_return' => 'Lost Item',
721 'payment' => 'Payment',
722 'writeoff' => 'Writeoff',
723 'ACCOUNT' => 'Account Fee',
724 'ACCOUNT_RENEW' => 'Account Fee',
725 'RESERVE' => 'Reserve Fee',
726 'PROCESSING' => 'Processing Fee',
727 'LOST' => 'Lost Item',
728 'RENT' => 'Rental Fee',
729 'RENT_DAILY' => 'Rental Fee',
730 'RENT_RENEW' => 'Rental Fee',
731 'RENT_DAILY_RENEW' => 'Rental Fee',
732 'OVERDUE' => 'OVERDUE',
733 'RESERVE_EXPIRED' => 'Hold Expired'
736 =head3 $account_type_credit
740 our $account_type_credit = {
743 'lost_item_return' => 'LOST_RETURN',
752 Kyle M Hall <kyle.m.hall@gmail.com>
753 Tomás Cohen Arazi <tomascohen@gmail.com>
754 Martin Renvoize <martin.renvoize@ptfs-europe.com>