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 unless ( $type eq 'WRITEOFF' ) {
88 Koha::Exceptions::Account::PaymentTypeRequired->throw()
89 if ( C4::Context->preference("RequirePaymentType")
90 && !defined($payment_type) );
92 my $av = Koha::AuthorisedValues->search_with_library_limits(
93 { category => 'PAYMENT_TYPE', authorised_value => $payment_type } );
95 if ( !$av->count && C4::Context->preference("RequirePaymentType") ) {
96 Koha::Exceptions::Account::InvalidPaymentType->throw( error => 'Invalid payment type' );
99 my $manager_id = $userenv ? $userenv->{number} : undef;
100 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
101 my $payment = $self->payin_amount(
103 interface => $interface,
106 payment_type => $payment_type,
107 cash_register => $cash_register,
108 user_id => $manager_id,
109 library_id => $library_id,
111 description => $description,
117 # NOTE: Pay historically always applied as much credit as it could to all
118 # existing outstanding debits, whether passed specific debits or otherwise.
119 if ( $payment->amountoutstanding ) {
122 { debits => [ $self->outstanding_debits->as_list ] } );
125 my $patron = Koha::Patrons->find( $self->{patron_id} );
126 my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
127 if ( C4::Context->preference('UseEmailReceipts') ) {
129 my $letter = C4::Letters::GetPreparedLetter(
130 module => 'circulation',
131 letter_code => uc("ACCOUNT_$type"),
132 message_transport_type => 'email',
133 lang => $patron->lang,
135 borrowers => $self->{patron_id},
136 branches => $library_id,
140 offsets => \@account_offsets,
145 C4::Letters::EnqueueLetter(
148 borrowernumber => $self->{patron_id},
149 message_transport_type => 'email',
151 ) or warn "can't enqueue letter $letter";
155 my $renew_outcomes = [];
156 for my $message ( @{$payment->object_messages} ) {
157 push @{$renew_outcomes}, $message->payload;
160 return { payment_id => $payment->id, renew_result => $renew_outcomes };
165 This method allows adding credits to a patron's account
167 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
170 description => $description,
173 interface => $interface,
174 library_id => $library_id,
175 payment_type => $payment_type,
176 type => $credit_type,
181 $credit_type can be any of:
195 my ( $self, $params ) = @_;
197 # check for mandatory params
198 my @mandatory = ( 'interface', 'amount' );
199 for my $param (@mandatory) {
200 unless ( defined( $params->{$param} ) ) {
201 Koha::Exceptions::MissingParameter->throw(
202 error => "The $param parameter is mandatory" );
206 # amount should always be passed as a positive value
207 my $amount = $params->{amount} * -1;
208 unless ( $amount < 0 ) {
209 Koha::Exceptions::Account::AmountNotPositive->throw(
210 error => 'Debit amount passed is not positive' );
213 my $description = $params->{description} // q{};
214 my $note = $params->{note} // q{};
215 my $user_id = $params->{user_id};
216 my $interface = $params->{interface};
217 my $library_id = $params->{library_id};
218 my $cash_register = $params->{cash_register};
219 my $payment_type = $params->{payment_type};
220 my $credit_type = $params->{type} || 'PAYMENT';
221 my $item_id = $params->{item_id};
223 Koha::Exceptions::Account::RegisterRequired->throw()
224 if ( C4::Context->preference("UseCashRegisters")
225 && defined($payment_type)
226 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
227 && !defined($cash_register) );
230 my $schema = Koha::Database->new->schema;
235 # Insert the account line
236 $line = Koha::Account::Line->new(
238 borrowernumber => $self->{patron_id},
241 description => $description,
242 credit_type_code => $credit_type,
243 amountoutstanding => $amount,
244 payment_type => $payment_type,
246 manager_id => $user_id,
247 interface => $interface,
248 branchcode => $library_id,
249 register_id => $cash_register,
250 itemnumber => $item_id,
254 # Record the account offset
255 my $account_offset = Koha::Account::Offset->new(
257 credit_id => $line->id,
259 amount => $amount * -1
263 C4::Stats::UpdateStats(
265 branch => $library_id,
266 type => lc($credit_type),
268 borrowernumber => $self->{patron_id},
270 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
273 'after_account_action',
275 action => "add_credit",
277 type => lc($credit_type),
278 line => $line->get_from_storage, #TODO Seems unneeded
283 if ( C4::Context->preference("FinesLog") ) {
289 action => "create_$credit_type",
290 borrowernumber => $self->{patron_id},
292 description => $description,
293 amountoutstanding => $amount,
294 credit_type_code => $credit_type,
296 itemnumber => $item_id,
297 manager_id => $user_id,
298 branchcode => $library_id,
308 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
309 if ( $_->broken_fk eq 'credit_type_code' ) {
310 Koha::Exceptions::Account::UnrecognisedType->throw(
311 error => 'Type of credit not recognised' );
322 my $credit = $account->payin_amount(
325 type => $credit_type,
326 payment_type => $payment_type,
327 cash_register => $register_id,
328 interface => $interface,
329 library_id => $branchcode,
330 user_id => $staff_id,
331 debits => $debit_lines,
332 description => $description,
337 This method allows an amount to be paid into a patrons account and immediately applied against debts.
339 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
341 $credit_type can be any of:
349 my ( $self, $params ) = @_;
351 # check for mandatory params
352 my @mandatory = ( 'interface', 'amount', 'type' );
353 for my $param (@mandatory) {
354 unless ( defined( $params->{$param} ) ) {
355 Koha::Exceptions::MissingParameter->throw(
356 error => "The $param parameter is mandatory" );
360 # Check for mandatory register
361 Koha::Exceptions::Account::RegisterRequired->throw()
362 if ( C4::Context->preference("UseCashRegisters")
363 && defined( $params->{payment_type} )
364 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
365 && !defined($params->{cash_register}) );
367 # amount should always be passed as a positive value
368 my $amount = $params->{amount};
369 unless ( $amount > 0 ) {
370 Koha::Exceptions::Account::AmountNotPositive->throw(
371 error => 'Payin amount passed is not positive' );
375 my $schema = Koha::Database->new->schema;
380 $credit = $self->add_credit($params);
382 # Offset debts passed first
383 if ( exists( $params->{debits} ) ) {
384 $credit = $credit->apply(
386 debits => $params->{debits}
391 # Offset against remaining balance if AutoReconcile
392 if ( C4::Context->preference("AccountAutoReconcile")
393 && $credit->amountoutstanding != 0 )
395 $credit = $credit->apply(
397 debits => [ $self->outstanding_debits->as_list ]
409 This method allows adding debits to a patron's account
411 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
414 description => $description,
417 interface => $interface,
418 library_id => $library_id,
420 transaction_type => $transaction_type,
421 cash_register => $register_id,
423 issue_id => $issue_id
427 $debit_type can be any of:
447 my ( $self, $params ) = @_;
449 # check for mandatory params
450 my @mandatory = ( 'interface', 'type', 'amount' );
451 for my $param (@mandatory) {
452 unless ( defined( $params->{$param} ) ) {
453 Koha::Exceptions::MissingParameter->throw(
454 error => "The $param parameter is mandatory" );
458 # check for cash register if using cash
459 Koha::Exceptions::Account::RegisterRequired->throw()
460 if ( C4::Context->preference("UseCashRegisters")
461 && defined( $params->{transaction_type} )
462 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
463 && !defined( $params->{cash_register} ) );
465 # amount should always be a positive value
466 my $amount = $params->{amount};
467 unless ( $amount > 0 ) {
468 Koha::Exceptions::Account::AmountNotPositive->throw(
469 error => 'Debit amount passed is not positive' );
472 my $description = $params->{description} // q{};
473 my $note = $params->{note} // q{};
474 my $user_id = $params->{user_id};
475 my $interface = $params->{interface};
476 my $library_id = $params->{library_id};
477 my $cash_register = $params->{cash_register};
478 my $debit_type = $params->{type};
479 my $transaction_type = $params->{transaction_type};
480 my $item_id = $params->{item_id};
481 my $issue_id = $params->{issue_id};
484 my $schema = Koha::Database->new->schema;
489 # Insert the account line
490 $line = Koha::Account::Line->new(
492 borrowernumber => $self->{patron_id},
495 description => $description,
496 debit_type_code => $debit_type,
497 amountoutstanding => $amount,
498 payment_type => $transaction_type,
500 manager_id => $user_id,
501 interface => $interface,
502 itemnumber => $item_id,
503 issue_id => $issue_id,
504 branchcode => $library_id,
505 register_id => $cash_register,
507 $debit_type eq 'OVERDUE'
508 ? ( status => 'UNRETURNED' )
514 # Record the account offset
515 my $account_offset = Koha::Account::Offset->new(
517 debit_id => $line->id,
523 if ( C4::Context->preference("FinesLog") ) {
529 action => "create_$debit_type",
530 borrowernumber => $self->{patron_id},
532 description => $description,
533 amountoutstanding => $amount,
534 debit_type_code => $debit_type,
536 itemnumber => $item_id,
537 manager_id => $user_id,
547 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
548 if ( $_->broken_fk eq 'debit_type_code' ) {
549 Koha::Exceptions::Account::UnrecognisedType->throw(
550 error => 'Type of debit not recognised' );
563 my $debit = $account->payout_amount(
565 payout_type => $payout_type,
566 register_id => $register_id,
567 staff_id => $staff_id,
568 interface => 'intranet',
570 credits => $credit_lines
574 This method allows an amount to be paid out from a patrons account against outstanding credits.
576 $payout_type can be any of the defined payment_types:
581 my ( $self, $params ) = @_;
583 # Check for mandatory parameters
585 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
586 for my $param (@mandatory) {
587 unless ( defined( $params->{$param} ) ) {
588 Koha::Exceptions::MissingParameter->throw(
589 error => "The $param parameter is mandatory" );
593 # Check for mandatory register
594 Koha::Exceptions::Account::RegisterRequired->throw()
595 if ( C4::Context->preference("UseCashRegisters")
596 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
597 && !defined($params->{cash_register}) );
599 # Amount should always be passed as a positive value
600 my $amount = $params->{amount};
601 unless ( $amount > 0 ) {
602 Koha::Exceptions::Account::AmountNotPositive->throw(
603 error => 'Payout amount passed is not positive' );
606 # Amount should always be less than or equal to outstanding credit
608 my $outstanding_credits =
609 exists( $params->{credits} )
611 : $self->outstanding_credits->as_list;
612 for my $credit ( @{$outstanding_credits} ) {
613 $outstanding += $credit->amountoutstanding;
615 $outstanding = $outstanding * -1;
616 Koha::Exceptions::ParameterTooHigh->throw( error =>
617 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
618 ) unless ( $outstanding >= $amount );
621 my $schema = Koha::Database->new->schema;
625 # A 'payout' is a 'debit'
626 $payout = $self->add_debit(
628 amount => $params->{amount},
630 transaction_type => $params->{payout_type},
631 amountoutstanding => $params->{amount},
632 user_id => $params->{staff_id},
633 interface => $params->{interface},
634 branchcode => $params->{branch},
635 cash_register => $params->{cash_register}
639 # Offset against credits
640 for my $credit ( @{$outstanding_credits} ) {
641 $credit->apply( { debits => [$payout] } );
642 $payout->discard_changes;
643 last if $payout->amountoutstanding == 0;
647 $payout->status('PAID')->store;
656 my $balance = $self->balance
658 Return the balance (sum of amountoutstanding columns)
664 return $self->lines->total_outstanding;
667 =head3 outstanding_debits
669 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
671 It returns the debit lines with outstanding amounts for the patron.
673 It returns a Koha::Account::Lines iterator.
677 sub outstanding_debits {
680 return $self->lines->search(
682 amount => { '>' => 0 },
683 amountoutstanding => { '>' => 0 }
688 =head3 outstanding_credits
690 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
692 It returns the credit lines with outstanding amounts for the patron.
694 It returns a Koha::Account::Lines iterator.
698 sub outstanding_credits {
701 return $self->lines->search(
703 amount => { '<' => 0 },
704 amountoutstanding => { '<' => 0 }
709 =head3 non_issues_charges
711 my $non_issues_charges = $self->non_issues_charges
713 Calculates amount immediately owing by the patron - non-issue charges.
715 Charges exempt from non-issue are:
716 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
717 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
718 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
722 sub non_issues_charges {
725 #NOTE: With bug 23049 these preferences could be moved to being attached
726 #to individual debit types to give more flexability and specificity.
728 push @not_fines, 'RESERVE'
729 unless C4::Context->preference('HoldsInNoissuesCharge');
730 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
731 unless C4::Context->preference('RentalsInNoissuesCharge');
732 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
733 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
734 push @not_fines, @man_inv;
737 return $self->lines->search(
739 debit_type_code => { -not_in => \@not_fines }
741 )->total_outstanding;
746 my $lines = $self->lines;
748 Return all credits and debits for the user, outstanding or otherwise
755 return Koha::Account::Lines->search(
757 borrowernumber => $self->{patron_id},
762 =head3 reconcile_balance
764 $account->reconcile_balance();
766 Find outstanding credits and use them to pay outstanding debits.
767 Currently, this implicitly uses the 'First In First Out' rule for
768 applying credits against debits.
772 sub reconcile_balance {
775 my $outstanding_debits = $self->outstanding_debits;
776 my $outstanding_credits = $self->outstanding_credits;
778 while ( $outstanding_debits->total_outstanding > 0
779 and my $credit = $outstanding_credits->next )
781 # there's both outstanding debits and credits
782 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
784 $outstanding_debits = $self->outstanding_debits;
797 Kyle M Hall <kyle.m.hall@gmail.com>
798 Tomás Cohen Arazi <tomascohen@gmail.com>
799 Martin Renvoize <martin.renvoize@ptfs-europe.com>