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 # check for mandatory params
338 my @mandatory = ( 'interface', 'amount' );
339 for my $param (@mandatory) {
340 unless ( defined( $params->{$param} ) ) {
341 Koha::Exceptions::MissingParameter->throw(
342 error => "The $param parameter is mandatory" );
346 # amount should always be passed as a positive value
347 my $amount = $params->{amount} * -1;
348 unless ( $amount < 0 ) {
349 Koha::Exceptions::Account::AmountNotPositive->throw(
350 error => 'Debit amount passed is not positive' );
353 my $description = $params->{description} // q{};
354 my $note = $params->{note} // q{};
355 my $user_id = $params->{user_id};
356 my $interface = $params->{interface};
357 my $library_id = $params->{library_id};
358 my $cash_register = $params->{cash_register};
359 my $payment_type = $params->{payment_type};
360 my $credit_type = $params->{type} || 'PAYMENT';
361 my $item_id = $params->{item_id};
363 Koha::Exceptions::Account::RegisterRequired->throw()
364 if ( C4::Context->preference("UseCashRegisters")
365 && defined($payment_type)
366 && ( $payment_type eq 'CASH' )
367 && !defined($cash_register) );
370 my $schema = Koha::Database->new->schema;
375 # Insert the account line
376 $line = Koha::Account::Line->new(
378 borrowernumber => $self->{patron_id},
381 description => $description,
382 credit_type_code => $credit_type,
383 amountoutstanding => $amount,
384 payment_type => $payment_type,
386 manager_id => $user_id,
387 interface => $interface,
388 branchcode => $library_id,
389 register_id => $cash_register,
390 itemnumber => $item_id,
394 # Record the account offset
395 my $account_offset = Koha::Account::Offset->new(
397 credit_id => $line->id,
398 type => $Koha::Account::offset_type->{$credit_type},
405 branch => $library_id,
406 type => lc($credit_type),
408 borrowernumber => $self->{patron_id},
410 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
412 if ( C4::Context->preference("FinesLog") ) {
418 action => "create_$credit_type",
419 borrowernumber => $self->{patron_id},
421 description => $description,
422 amountoutstanding => $amount,
423 credit_type_code => $credit_type,
425 itemnumber => $item_id,
426 manager_id => $user_id,
427 branchcode => $library_id,
437 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
438 if ( $_->broken_fk eq 'credit_type_code' ) {
439 Koha::Exceptions::Account::UnrecognisedType->throw(
440 error => 'Type of credit not recognised' );
453 This method allows adding debits to a patron's account
455 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
458 description => $description,
461 interface => $interface,
462 library_id => $library_id,
465 issue_id => $issue_id
469 $debit_type can be any of:
488 my ( $self, $params ) = @_;
490 # check for mandatory params
491 my @mandatory = ( 'interface', 'type', 'amount' );
492 for my $param (@mandatory) {
493 unless ( defined( $params->{$param} ) ) {
494 Koha::Exceptions::MissingParameter->throw(
495 error => "The $param parameter is mandatory" );
499 # amount should always be a positive value
500 my $amount = $params->{amount};
501 unless ( $amount > 0 ) {
502 Koha::Exceptions::Account::AmountNotPositive->throw(
503 error => 'Debit amount passed is not positive' );
506 my $description = $params->{description} // q{};
507 my $note = $params->{note} // q{};
508 my $user_id = $params->{user_id};
509 my $interface = $params->{interface};
510 my $library_id = $params->{library_id};
511 my $debit_type = $params->{type};
512 my $item_id = $params->{item_id};
513 my $issue_id = $params->{issue_id};
514 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
517 my $schema = Koha::Database->new->schema;
522 # Insert the account line
523 $line = Koha::Account::Line->new(
525 borrowernumber => $self->{patron_id},
528 description => $description,
529 debit_type_code => $debit_type,
530 amountoutstanding => $amount,
531 payment_type => undef,
533 manager_id => $user_id,
534 interface => $interface,
535 itemnumber => $item_id,
536 issue_id => $issue_id,
537 branchcode => $library_id,
539 $debit_type eq 'OVERDUE'
540 ? ( status => 'UNRETURNED' )
546 # Record the account offset
547 my $account_offset = Koha::Account::Offset->new(
549 debit_id => $line->id,
550 type => $offset_type,
555 if ( C4::Context->preference("FinesLog") ) {
561 action => "create_$debit_type",
562 borrowernumber => $self->{patron_id},
564 description => $description,
565 amountoutstanding => $amount,
566 debit_type_code => $debit_type,
568 itemnumber => $item_id,
569 manager_id => $user_id,
579 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
580 if ( $_->broken_fk eq 'debit_type_code' ) {
581 Koha::Exceptions::Account::UnrecognisedType->throw(
582 error => 'Type of debit not recognised' );
595 my $balance = $self->balance
597 Return the balance (sum of amountoutstanding columns)
603 return $self->lines->total_outstanding;
606 =head3 outstanding_debits
608 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
610 It returns the debit 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_debits {
620 return $self->lines->search(
622 amount => { '>' => 0 },
623 amountoutstanding => { '>' => 0 }
628 =head3 outstanding_credits
630 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
632 It returns the credit lines with outstanding amounts for the patron.
634 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
635 return a list of Koha::Account::Line objects.
639 sub outstanding_credits {
642 return $self->lines->search(
644 amount => { '<' => 0 },
645 amountoutstanding => { '<' => 0 }
650 =head3 non_issues_charges
652 my $non_issues_charges = $self->non_issues_charges
654 Calculates amount immediately owing by the patron - non-issue charges.
656 Charges exempt from non-issue are:
657 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
658 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
659 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
663 sub non_issues_charges {
666 #NOTE: With bug 23049 these preferences could be moved to being attached
667 #to individual debit types to give more flexability and specificity.
669 push @not_fines, 'RESERVE'
670 unless C4::Context->preference('HoldsInNoissuesCharge');
671 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
672 unless C4::Context->preference('RentalsInNoissuesCharge');
673 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
674 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
675 push @not_fines, @man_inv;
678 return $self->lines->search(
680 debit_type_code => { -not_in => \@not_fines }
682 )->total_outstanding;
687 my $lines = $self->lines;
689 Return all credits and debits for the user, outstanding or otherwise
696 return Koha::Account::Lines->search(
698 borrowernumber => $self->{patron_id},
703 =head3 reconcile_balance
705 $account->reconcile_balance();
707 Find outstanding credits and use them to pay outstanding debits.
708 Currently, this implicitly uses the 'First In First Out' rule for
709 applying credits against debits.
713 sub reconcile_balance {
716 my $outstanding_debits = $self->outstanding_debits;
717 my $outstanding_credits = $self->outstanding_credits;
719 while ( $outstanding_debits->total_outstanding > 0
720 and my $credit = $outstanding_credits->next )
722 # there's both outstanding debits and credits
723 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
725 $outstanding_debits = $self->outstanding_debits;
741 'CREDIT' => 'Manual Credit',
742 'FORGIVEN' => 'Writeoff',
743 'LOST_RETURN' => 'Lost Item',
744 'PAYMENT' => 'Payment',
745 'WRITEOFF' => 'Writeoff',
746 'ACCOUNT' => 'Account Fee',
747 'ACCOUNT_RENEW' => 'Account Fee',
748 'RESERVE' => 'Reserve Fee',
749 'PROCESSING' => 'Processing Fee',
750 'LOST' => 'Lost Item',
751 'RENT' => 'Rental Fee',
752 'RENT_DAILY' => 'Rental Fee',
753 'RENT_RENEW' => 'Rental Fee',
754 'RENT_DAILY_RENEW' => 'Rental Fee',
755 'OVERDUE' => 'OVERDUE',
756 'RESERVE_EXPIRED' => 'Hold Expired'
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>