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>.
23 use Data::Dumper qw( Dumper );
24 use Try::Tiny qw( catch try );
26 use C4::Circulation qw( ReturnLostItem CanBookBeRenewed AddRenewal );
28 use C4::Log qw( logaction );
29 use C4::Stats qw( UpdateStats );
30 use C4::Overdues qw(GetFine);
33 use Koha::Account::Lines;
34 use Koha::Account::Offsets;
35 use Koha::Account::DebitTypes;
37 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 item_id => $itemnumber, # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
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 $cash_register = $params->{cash_register};
83 my $item_id = $params->{item_id};
85 my $userenv = C4::Context->userenv;
87 my $manager_id = $userenv ? $userenv->{number} : undef;
88 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
89 my $payment = $self->payin_amount(
91 interface => $interface,
94 payment_type => $payment_type,
95 cash_register => $cash_register,
96 user_id => $manager_id,
97 library_id => $library_id,
99 description => $description,
105 Koha::Exceptions::Account::PaymentTypeRequired->throw()
106 if ( C4::Context->preference("RequirePaymentType")
107 && !defined($payment_type) );
109 # NOTE: Pay historically always applied as much credit as it could to all
110 # existing outstanding debits, whether passed specific debits or otherwise.
111 if ( $payment->amountoutstanding ) {
114 { debits => [ $self->outstanding_debits->as_list ] } );
117 my $patron = Koha::Patrons->find( $self->{patron_id} );
118 my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
119 if ( C4::Context->preference('UseEmailReceipts') ) {
121 my $letter = C4::Letters::GetPreparedLetter(
122 module => 'circulation',
123 letter_code => uc("ACCOUNT_$type"),
124 message_transport_type => 'email',
125 lang => $patron->lang,
127 borrowers => $self->{patron_id},
128 branches => $library_id,
132 offsets => \@account_offsets,
137 C4::Letters::EnqueueLetter(
140 borrowernumber => $self->{patron_id},
141 message_transport_type => 'email',
143 ) or warn "can't enqueue letter $letter";
147 my $renew_outcomes = [];
148 for my $message ( @{$payment->object_messages} ) {
149 push @{$renew_outcomes}, $message->payload;
152 return { payment_id => $payment->id, renew_result => $renew_outcomes };
157 This method allows adding credits to a patron's account
159 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
162 description => $description,
165 interface => $interface,
166 library_id => $library_id,
167 payment_type => $payment_type,
168 type => $credit_type,
173 $credit_type can be any of:
187 my ( $self, $params ) = @_;
189 # check for mandatory params
190 my @mandatory = ( 'interface', 'amount' );
191 for my $param (@mandatory) {
192 unless ( defined( $params->{$param} ) ) {
193 Koha::Exceptions::MissingParameter->throw(
194 error => "The $param parameter is mandatory" );
198 # amount should always be passed as a positive value
199 my $amount = $params->{amount} * -1;
200 unless ( $amount < 0 ) {
201 Koha::Exceptions::Account::AmountNotPositive->throw(
202 error => 'Debit amount passed is not positive' );
205 my $description = $params->{description} // q{};
206 my $note = $params->{note} // q{};
207 my $user_id = $params->{user_id};
208 my $interface = $params->{interface};
209 my $library_id = $params->{library_id};
210 my $cash_register = $params->{cash_register};
211 my $payment_type = $params->{payment_type};
212 my $credit_type = $params->{type} || 'PAYMENT';
213 my $item_id = $params->{item_id};
215 Koha::Exceptions::Account::RegisterRequired->throw()
216 if ( C4::Context->preference("UseCashRegisters")
217 && defined($payment_type)
218 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
219 && !defined($cash_register) );
222 my $schema = Koha::Database->new->schema;
227 # Insert the account line
228 $line = Koha::Account::Line->new(
230 borrowernumber => $self->{patron_id},
233 description => $description,
234 credit_type_code => $credit_type,
235 amountoutstanding => $amount,
236 payment_type => $payment_type,
238 manager_id => $user_id,
239 interface => $interface,
240 branchcode => $library_id,
241 register_id => $cash_register,
242 itemnumber => $item_id,
246 # Record the account offset
247 my $account_offset = Koha::Account::Offset->new(
249 credit_id => $line->id,
251 amount => $amount * -1
255 C4::Stats::UpdateStats(
257 branch => $library_id,
258 type => lc($credit_type),
260 borrowernumber => $self->{patron_id},
262 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
265 'after_account_action',
267 action => "add_credit",
269 type => lc($credit_type),
270 line => $line->get_from_storage, #TODO Seems unneeded
275 if ( C4::Context->preference("FinesLog") ) {
281 action => "create_$credit_type",
282 borrowernumber => $self->{patron_id},
284 description => $description,
285 amountoutstanding => $amount,
286 credit_type_code => $credit_type,
288 itemnumber => $item_id,
289 manager_id => $user_id,
290 branchcode => $library_id,
300 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
301 if ( $_->broken_fk eq 'credit_type_code' ) {
302 Koha::Exceptions::Account::UnrecognisedType->throw(
303 error => 'Type of credit not recognised' );
316 my $credit = $account->payin_amount(
319 type => $credit_type,
320 payment_type => $payment_type,
321 cash_register => $register_id,
322 interface => $interface,
323 library_id => $branchcode,
324 user_id => $staff_id,
325 debits => $debit_lines,
326 description => $description,
331 This method allows an amount to be paid into a patrons account and immediately applied against debts.
333 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
335 $credit_type can be any of:
343 my ( $self, $params ) = @_;
345 # check for mandatory params
346 my @mandatory = ( 'interface', 'amount', 'type' );
347 for my $param (@mandatory) {
348 unless ( defined( $params->{$param} ) ) {
349 Koha::Exceptions::MissingParameter->throw(
350 error => "The $param parameter is mandatory" );
354 # Check for mandatory register
355 Koha::Exceptions::Account::RegisterRequired->throw()
356 if ( C4::Context->preference("UseCashRegisters")
357 && defined( $params->{payment_type} )
358 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
359 && !defined($params->{cash_register}) );
361 # amount should always be passed as a positive value
362 my $amount = $params->{amount};
363 unless ( $amount > 0 ) {
364 Koha::Exceptions::Account::AmountNotPositive->throw(
365 error => 'Payin amount passed is not positive' );
369 my $schema = Koha::Database->new->schema;
374 $credit = $self->add_credit($params);
376 # Offset debts passed first
377 if ( exists( $params->{debits} ) ) {
378 $credit = $credit->apply(
380 debits => $params->{debits}
385 # Offset against remaining balance if AutoReconcile
386 if ( C4::Context->preference("AccountAutoReconcile")
387 && $credit->amountoutstanding != 0 )
389 $credit = $credit->apply(
391 debits => [ $self->outstanding_debits->as_list ]
403 This method allows adding debits to a patron's account
405 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
408 description => $description,
411 interface => $interface,
412 library_id => $library_id,
414 transaction_type => $transaction_type,
415 cash_register => $register_id,
417 issue_id => $issue_id
421 $debit_type can be any of:
441 my ( $self, $params ) = @_;
443 # check for mandatory params
444 my @mandatory = ( 'interface', 'type', 'amount' );
445 for my $param (@mandatory) {
446 unless ( defined( $params->{$param} ) ) {
447 Koha::Exceptions::MissingParameter->throw(
448 error => "The $param parameter is mandatory" );
452 # check for cash register if using cash
453 Koha::Exceptions::Account::RegisterRequired->throw()
454 if ( C4::Context->preference("UseCashRegisters")
455 && defined( $params->{transaction_type} )
456 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
457 && !defined( $params->{cash_register} ) );
459 # amount should always be a positive value
460 my $amount = $params->{amount};
461 unless ( $amount > 0 ) {
462 Koha::Exceptions::Account::AmountNotPositive->throw(
463 error => 'Debit amount passed is not positive' );
466 my $description = $params->{description} // q{};
467 my $note = $params->{note} // q{};
468 my $user_id = $params->{user_id};
469 my $interface = $params->{interface};
470 my $library_id = $params->{library_id};
471 my $cash_register = $params->{cash_register};
472 my $debit_type = $params->{type};
473 my $transaction_type = $params->{transaction_type};
474 my $item_id = $params->{item_id};
475 my $issue_id = $params->{issue_id};
478 my $schema = Koha::Database->new->schema;
483 # Insert the account line
484 $line = Koha::Account::Line->new(
486 borrowernumber => $self->{patron_id},
489 description => $description,
490 debit_type_code => $debit_type,
491 amountoutstanding => $amount,
492 payment_type => $transaction_type,
494 manager_id => $user_id,
495 interface => $interface,
496 itemnumber => $item_id,
497 issue_id => $issue_id,
498 branchcode => $library_id,
499 register_id => $cash_register,
501 $debit_type eq 'OVERDUE'
502 ? ( status => 'UNRETURNED' )
508 # Record the account offset
509 my $account_offset = Koha::Account::Offset->new(
511 debit_id => $line->id,
517 if ( C4::Context->preference("FinesLog") ) {
523 action => "create_$debit_type",
524 borrowernumber => $self->{patron_id},
526 description => $description,
527 amountoutstanding => $amount,
528 debit_type_code => $debit_type,
530 itemnumber => $item_id,
531 manager_id => $user_id,
541 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
542 if ( $_->broken_fk eq 'debit_type_code' ) {
543 Koha::Exceptions::Account::UnrecognisedType->throw(
544 error => 'Type of debit not recognised' );
557 my $debit = $account->payout_amount(
559 payout_type => $payout_type,
560 register_id => $register_id,
561 staff_id => $staff_id,
562 interface => 'intranet',
564 credits => $credit_lines
568 This method allows an amount to be paid out from a patrons account against outstanding credits.
570 $payout_type can be any of the defined payment_types:
575 my ( $self, $params ) = @_;
577 # Check for mandatory parameters
579 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
580 for my $param (@mandatory) {
581 unless ( defined( $params->{$param} ) ) {
582 Koha::Exceptions::MissingParameter->throw(
583 error => "The $param parameter is mandatory" );
587 # Check for mandatory register
588 Koha::Exceptions::Account::RegisterRequired->throw()
589 if ( C4::Context->preference("UseCashRegisters")
590 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
591 && !defined($params->{cash_register}) );
593 # Amount should always be passed as a positive value
594 my $amount = $params->{amount};
595 unless ( $amount > 0 ) {
596 Koha::Exceptions::Account::AmountNotPositive->throw(
597 error => 'Payout amount passed is not positive' );
600 # Amount should always be less than or equal to outstanding credit
602 my $outstanding_credits =
603 exists( $params->{credits} )
605 : $self->outstanding_credits->as_list;
606 for my $credit ( @{$outstanding_credits} ) {
607 $outstanding += $credit->amountoutstanding;
609 $outstanding = $outstanding * -1;
610 Koha::Exceptions::ParameterTooHigh->throw( error =>
611 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
612 ) unless ( $outstanding >= $amount );
615 my $schema = Koha::Database->new->schema;
619 # A 'payout' is a 'debit'
620 $payout = $self->add_debit(
622 amount => $params->{amount},
624 transaction_type => $params->{payout_type},
625 amountoutstanding => $params->{amount},
626 user_id => $params->{staff_id},
627 interface => $params->{interface},
628 branchcode => $params->{branch},
629 cash_register => $params->{cash_register}
633 # Offset against credits
634 for my $credit ( @{$outstanding_credits} ) {
635 $credit->apply( { debits => [$payout] } );
636 $payout->discard_changes;
637 last if $payout->amountoutstanding == 0;
641 $payout->status('PAID')->store;
650 my $balance = $self->balance
652 Return the balance (sum of amountoutstanding columns)
658 return $self->lines->total_outstanding;
661 =head3 outstanding_debits
663 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
665 It returns the debit lines with outstanding amounts for the patron.
667 It returns a Koha::Account::Lines iterator.
671 sub outstanding_debits {
674 return $self->lines->search(
676 amount => { '>' => 0 },
677 amountoutstanding => { '>' => 0 }
682 =head3 outstanding_credits
684 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
686 It returns the credit lines with outstanding amounts for the patron.
688 It returns a Koha::Account::Lines iterator.
692 sub outstanding_credits {
695 return $self->lines->search(
697 amount => { '<' => 0 },
698 amountoutstanding => { '<' => 0 }
703 =head3 non_issues_charges
705 my $non_issues_charges = $self->non_issues_charges
707 Calculates amount immediately owing by the patron - non-issue charges.
709 Charges exempt from non-issue are:
710 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
711 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
712 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
716 sub non_issues_charges {
719 #NOTE: With bug 23049 these preferences could be moved to being attached
720 #to individual debit types to give more flexability and specificity.
722 push @not_fines, 'RESERVE'
723 unless C4::Context->preference('HoldsInNoissuesCharge');
724 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
725 unless C4::Context->preference('RentalsInNoissuesCharge');
726 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
727 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
728 push @not_fines, @man_inv;
731 return $self->lines->search(
733 debit_type_code => { -not_in => \@not_fines }
735 )->total_outstanding;
740 my $lines = $self->lines;
742 Return all credits and debits for the user, outstanding or otherwise
749 return Koha::Account::Lines->search(
751 borrowernumber => $self->{patron_id},
756 =head3 reconcile_balance
758 $account->reconcile_balance();
760 Find outstanding credits and use them to pay outstanding debits.
761 Currently, this implicitly uses the 'First In First Out' rule for
762 applying credits against debits.
766 sub reconcile_balance {
769 my $outstanding_debits = $self->outstanding_debits;
770 my $outstanding_credits = $self->outstanding_credits;
772 while ( $outstanding_debits->total_outstanding > 0
773 and my $credit = $outstanding_credits->next )
775 # there's both outstanding debits and credits
776 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
778 $outstanding_debits = $self->outstanding_debits;
791 Kyle M Hall <kyle.m.hall@gmail.com>
792 Tomás Cohen Arazi <tomascohen@gmail.com>
793 Martin Renvoize <martin.renvoize@ptfs-europe.com>