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::Account::DebitTypes;
36 use Koha::DateUtils qw( dt_from_string );
38 use Koha::Exceptions::Account;
42 Koha::Accounts - Module for managing payments and fees for patrons
47 my ( $class, $params ) = @_;
49 Carp::croak("No patron id passed in!") unless $params->{patron_id};
51 return bless( $params, $class );
56 This method allows payments to be made against fees/fines
58 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
62 description => $description,
63 library_id => $branchcode,
64 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
65 credit_type => $type, # credit_type_code code
66 offset_type => $offset_type, # offset type code
73 my ( $self, $params ) = @_;
75 my $amount = $params->{amount};
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 $credit_type = $params->{credit_type};
83 my $offset_type = $params->{offset_type} || $type eq 'WRITEOFF' ? 'Writeoff' : 'Payment';
84 my $cash_register = $params->{cash_register};
86 my $userenv = C4::Context->userenv;
88 my $patron = Koha::Patrons->find( $self->{patron_id} );
90 my $manager_id = $userenv ? $userenv->{number} : 0;
91 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
92 Koha::Exceptions::Account::RegisterRequired->throw()
93 if ( C4::Context->preference("UseCashRegisters")
94 && defined($payment_type)
95 && ( $payment_type eq 'CASH' )
96 && !defined($cash_register) );
98 my @fines_paid; # List of account lines paid on with this payment
100 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
101 $balance_remaining ||= 0;
105 # We were passed a specific line to pay
106 foreach my $fine ( @$lines ) {
108 $fine->amountoutstanding > $balance_remaining
110 : $fine->amountoutstanding;
112 my $old_amountoutstanding = $fine->amountoutstanding;
113 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
114 $fine->amountoutstanding($new_amountoutstanding)->store();
115 $balance_remaining = $balance_remaining - $amount_to_pay;
117 # Same logic exists in Koha::Account::Line::apply
118 if ( $new_amountoutstanding == 0
120 && $fine->debit_type_code
121 && ( $fine->debit_type_code eq 'LOST' ) )
123 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
126 my $account_offset = Koha::Account::Offset->new(
128 debit_id => $fine->id,
129 type => $offset_type,
130 amount => $amount_to_pay * -1,
133 push( @account_offsets, $account_offset );
135 if ( C4::Context->preference("FinesLog") ) {
141 action => 'fee_payment',
142 borrowernumber => $fine->borrowernumber,
143 old_amountoutstanding => $old_amountoutstanding,
144 new_amountoutstanding => 0,
145 amount_paid => $old_amountoutstanding,
146 accountlines_id => $fine->id,
147 manager_id => $manager_id,
153 push( @fines_paid, $fine->id );
157 # Were not passed a specific line to pay, or the payment was for more
158 # than the what was owed on the given line. In that case pay down other
159 # lines with remaining balance.
160 my @outstanding_fines;
161 @outstanding_fines = $self->lines->search(
163 amountoutstanding => { '>' => 0 },
165 ) if $balance_remaining > 0;
167 foreach my $fine (@outstanding_fines) {
169 $fine->amountoutstanding > $balance_remaining
171 : $fine->amountoutstanding;
173 my $old_amountoutstanding = $fine->amountoutstanding;
174 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
177 if ( $fine->amountoutstanding == 0
179 && $fine->debit_type_code
180 && ( $fine->debit_type_code eq 'LOST' ) )
182 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
185 my $account_offset = Koha::Account::Offset->new(
187 debit_id => $fine->id,
188 type => $offset_type,
189 amount => $amount_to_pay * -1,
192 push( @account_offsets, $account_offset );
194 if ( C4::Context->preference("FinesLog") ) {
200 action => "fee_$type",
201 borrowernumber => $fine->borrowernumber,
202 old_amountoutstanding => $old_amountoutstanding,
203 new_amountoutstanding => $fine->amountoutstanding,
204 amount_paid => $amount_to_pay,
205 accountlines_id => $fine->id,
206 manager_id => $manager_id,
212 push( @fines_paid, $fine->id );
215 $balance_remaining = $balance_remaining - $amount_to_pay;
216 last unless $balance_remaining > 0;
224 $description ||= $type eq 'WRITEOFF' ? 'Writeoff' : q{};
226 my $payment = Koha::Account::Line->new(
228 borrowernumber => $self->{patron_id},
229 date => dt_from_string(),
230 amount => 0 - $amount,
231 description => $description,
232 credit_type_code => $credit_type,
233 payment_type => $payment_type,
234 amountoutstanding => 0 - $balance_remaining,
235 manager_id => $manager_id,
236 interface => $interface,
237 branchcode => $library_id,
238 register_id => $cash_register,
243 foreach my $o ( @account_offsets ) {
244 $o->credit_id( $payment->id() );
250 branch => $library_id,
253 borrowernumber => $self->{patron_id},
257 if ( C4::Context->preference("FinesLog") ) {
263 action => "create_$type",
264 borrowernumber => $self->{patron_id},
265 amount => 0 - $amount,
266 amountoutstanding => 0 - $balance_remaining,
267 credit_type_code => $credit_type,
268 accountlines_paid => \@fines_paid,
269 manager_id => $manager_id,
276 if ( C4::Context->preference('UseEmailReceipts') ) {
278 my $letter = C4::Letters::GetPreparedLetter(
279 module => 'circulation',
280 letter_code => uc("ACCOUNT_$type"),
281 message_transport_type => 'email',
282 lang => $patron->lang,
284 borrowers => $self->{patron_id},
285 branches => $library_id,
289 offsets => \@account_offsets,
294 C4::Letters::EnqueueLetter(
297 borrowernumber => $self->{patron_id},
298 message_transport_type => 'email',
300 ) or warn "can't enqueue letter $letter";
309 This method allows adding credits to a patron's account
311 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
314 description => $description,
317 interface => $interface,
318 library_id => $library_id,
319 payment_type => $payment_type,
320 type => $credit_type,
325 $credit_type can be any of:
336 my ( $self, $params ) = @_;
338 # check for mandatory params
339 my @mandatory = ( 'interface', 'amount' );
340 for my $param (@mandatory) {
341 unless ( defined( $params->{$param} ) ) {
342 Koha::Exceptions::MissingParameter->throw(
343 error => "The $param parameter is mandatory" );
347 # amount should always be passed as a positive value
348 my $amount = $params->{amount} * -1;
349 unless ( $amount < 0 ) {
350 Koha::Exceptions::Account::AmountNotPositive->throw(
351 error => 'Debit amount passed is not positive' );
354 my $description = $params->{description} // q{};
355 my $note = $params->{note} // q{};
356 my $user_id = $params->{user_id};
357 my $interface = $params->{interface};
358 my $library_id = $params->{library_id};
359 my $cash_register = $params->{cash_register};
360 my $payment_type = $params->{payment_type};
361 my $credit_type = $params->{type} || 'PAYMENT';
362 my $item_id = $params->{item_id};
364 Koha::Exceptions::Account::RegisterRequired->throw()
365 if ( C4::Context->preference("UseCashRegisters")
366 && defined($payment_type)
367 && ( $payment_type eq 'CASH' )
368 && !defined($cash_register) );
371 my $schema = Koha::Database->new->schema;
376 # Insert the account line
377 $line = Koha::Account::Line->new(
379 borrowernumber => $self->{patron_id},
382 description => $description,
383 credit_type_code => $credit_type,
384 amountoutstanding => $amount,
385 payment_type => $payment_type,
387 manager_id => $user_id,
388 interface => $interface,
389 branchcode => $library_id,
390 register_id => $cash_register,
391 itemnumber => $item_id,
395 # Record the account offset
396 my $account_offset = Koha::Account::Offset->new(
398 credit_id => $line->id,
399 type => $Koha::Account::offset_type->{$credit_type},
406 branch => $library_id,
407 type => lc($credit_type),
409 borrowernumber => $self->{patron_id},
411 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
413 if ( C4::Context->preference("FinesLog") ) {
419 action => "create_$credit_type",
420 borrowernumber => $self->{patron_id},
422 description => $description,
423 amountoutstanding => $amount,
424 credit_type_code => $credit_type,
426 itemnumber => $item_id,
427 manager_id => $user_id,
428 branchcode => $library_id,
438 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
439 if ( $_->broken_fk eq 'credit_type_code' ) {
440 Koha::Exceptions::Account::UnrecognisedType->throw(
441 error => 'Type of credit not recognised' );
454 This method allows adding debits to a patron's account
456 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
459 description => $description,
462 interface => $interface,
463 library_id => $library_id,
466 issue_id => $issue_id
470 $debit_type can be any of:
489 my ( $self, $params ) = @_;
491 # check for mandatory params
492 my @mandatory = ( 'interface', 'type', 'amount' );
493 for my $param (@mandatory) {
494 unless ( defined( $params->{$param} ) ) {
495 Koha::Exceptions::MissingParameter->throw(
496 error => "The $param parameter is mandatory" );
500 # amount should always be a positive value
501 my $amount = $params->{amount};
502 unless ( $amount > 0 ) {
503 Koha::Exceptions::Account::AmountNotPositive->throw(
504 error => 'Debit amount passed is not positive' );
507 my $description = $params->{description} // q{};
508 my $note = $params->{note} // q{};
509 my $user_id = $params->{user_id};
510 my $interface = $params->{interface};
511 my $library_id = $params->{library_id};
512 my $debit_type = $params->{type};
513 my $item_id = $params->{item_id};
514 my $issue_id = $params->{issue_id};
515 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
518 my $schema = Koha::Database->new->schema;
523 # Insert the account line
524 $line = Koha::Account::Line->new(
526 borrowernumber => $self->{patron_id},
529 description => $description,
530 debit_type_code => $debit_type,
531 amountoutstanding => $amount,
532 payment_type => undef,
534 manager_id => $user_id,
535 interface => $interface,
536 itemnumber => $item_id,
537 issue_id => $issue_id,
538 branchcode => $library_id,
540 $debit_type eq 'OVERDUE'
541 ? ( status => 'UNRETURNED' )
547 # Record the account offset
548 my $account_offset = Koha::Account::Offset->new(
550 debit_id => $line->id,
551 type => $offset_type,
556 if ( C4::Context->preference("FinesLog") ) {
562 action => "create_$debit_type",
563 borrowernumber => $self->{patron_id},
565 description => $description,
566 amountoutstanding => $amount,
567 debit_type_code => $debit_type,
569 itemnumber => $item_id,
570 manager_id => $user_id,
580 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
581 if ( $_->broken_fk eq 'debit_type_code' ) {
582 Koha::Exceptions::Account::UnrecognisedType->throw(
583 error => 'Type of debit not recognised' );
596 my $balance = $self->balance
598 Return the balance (sum of amountoutstanding columns)
604 return $self->lines->total_outstanding;
607 =head3 outstanding_debits
609 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
611 It returns the debit lines with outstanding amounts for the patron.
613 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
614 return a list of Koha::Account::Line objects.
618 sub outstanding_debits {
621 return $self->lines->search(
623 amount => { '>' => 0 },
624 amountoutstanding => { '>' => 0 }
629 =head3 outstanding_credits
631 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
633 It returns the credit lines with outstanding amounts for the patron.
635 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
636 return a list of Koha::Account::Line objects.
640 sub outstanding_credits {
643 return $self->lines->search(
645 amount => { '<' => 0 },
646 amountoutstanding => { '<' => 0 }
651 =head3 non_issues_charges
653 my $non_issues_charges = $self->non_issues_charges
655 Calculates amount immediately owing by the patron - non-issue charges.
657 Charges exempt from non-issue are:
658 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
659 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
660 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
664 sub non_issues_charges {
667 #NOTE: With bug 23049 these preferences could be moved to being attached
668 #to individual debit types to give more flexability and specificity.
670 push @not_fines, 'RESERVE'
671 unless C4::Context->preference('HoldsInNoissuesCharge');
672 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
673 unless C4::Context->preference('RentalsInNoissuesCharge');
674 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
675 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
676 push @not_fines, @man_inv;
679 return $self->lines->search(
681 debit_type_code => { -not_in => \@not_fines }
683 )->total_outstanding;
688 my $lines = $self->lines;
690 Return all credits and debits for the user, outstanding or otherwise
697 return Koha::Account::Lines->search(
699 borrowernumber => $self->{patron_id},
704 =head3 reconcile_balance
706 $account->reconcile_balance();
708 Find outstanding credits and use them to pay outstanding debits.
709 Currently, this implicitly uses the 'First In First Out' rule for
710 applying credits against debits.
714 sub reconcile_balance {
717 my $outstanding_debits = $self->outstanding_debits;
718 my $outstanding_credits = $self->outstanding_credits;
720 while ( $outstanding_debits->total_outstanding > 0
721 and my $credit = $outstanding_credits->next )
723 # there's both outstanding debits and credits
724 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
726 $outstanding_debits = $self->outstanding_debits;
742 'CREDIT' => 'Manual Credit',
743 'FORGIVEN' => 'Writeoff',
744 'LOST_RETURN' => 'Lost Item',
745 'PAYMENT' => 'Payment',
746 'WRITEOFF' => 'Writeoff',
747 'ACCOUNT' => 'Account Fee',
748 'ACCOUNT_RENEW' => 'Account Fee',
749 'RESERVE' => 'Reserve Fee',
750 'PROCESSING' => 'Processing Fee',
751 'LOST' => 'Lost Item',
752 'RENT' => 'Rental Fee',
753 'RENT_DAILY' => 'Rental Fee',
754 'RENT_RENEW' => 'Rental Fee',
755 'RENT_DAILY_RENEW' => 'Rental Fee',
756 'OVERDUE' => 'OVERDUE',
757 'RESERVE_EXPIRED' => 'Hold Expired'
764 Kyle M Hall <kyle.m.hall@gmail.com>
765 Tomás Cohen Arazi <tomascohen@gmail.com>
766 Martin Renvoize <martin.renvoize@ptfs-europe.com>