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);
34 use Koha::Account::Credits;
35 use Koha::Account::Debits;
36 use Koha::Account::Lines;
37 use Koha::Account::Offsets;
38 use Koha::Account::DebitTypes;
40 use Koha::Exceptions::Account;
45 Koha::Accounts - Module for managing payments and fees for patrons
50 my ( $class, $params ) = @_;
52 Carp::croak("No patron id passed in!") unless $params->{patron_id};
54 return bless( $params, $class );
59 This method allows payments to be made against fees/fines
61 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
65 description => $description,
66 library_id => $branchcode,
67 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
68 credit_type => $type, # credit_type_code code
69 item_id => $itemnumber, # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
76 my ( $self, $params ) = @_;
78 my $amount = $params->{amount};
79 my $description = $params->{description};
80 my $note = $params->{note} || q{};
81 my $library_id = $params->{library_id};
82 my $lines = $params->{lines};
83 my $type = $params->{type} || 'PAYMENT';
84 my $payment_type = $params->{payment_type} || undef;
85 my $cash_register = $params->{cash_register};
86 my $item_id = $params->{item_id};
88 my $userenv = C4::Context->userenv;
90 Koha::Exceptions::Account::PaymentTypeRequired->throw()
91 if ( C4::Context->preference("RequirePaymentType")
92 && !defined($payment_type) );
94 my $av = Koha::AuthorisedValues->search_with_library_limits({ category => 'PAYMENT_TYPE', authorised_value => $payment_type });
96 if ( !$av->count && C4::Context->preference("RequirePaymentType")) {
97 Koha::Exceptions::Account::InvalidPaymentType->throw(
98 error => 'Invalid payment type'
102 my $manager_id = $userenv ? $userenv->{number} : undef;
103 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
104 my $payment = $self->payin_amount(
106 interface => $interface,
109 payment_type => $payment_type,
110 cash_register => $cash_register,
111 user_id => $manager_id,
112 library_id => $library_id,
114 description => $description,
120 # NOTE: Pay historically always applied as much credit as it could to all
121 # existing outstanding debits, whether passed specific debits or otherwise.
122 if ( $payment->amountoutstanding ) {
125 { debits => [ $self->outstanding_debits->as_list ] } );
128 my $patron = Koha::Patrons->find( $self->{patron_id} );
129 my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
130 if ( C4::Context->preference('UseEmailReceipts') ) {
132 my $letter = C4::Letters::GetPreparedLetter(
133 module => 'circulation',
134 letter_code => uc("ACCOUNT_$type"),
135 message_transport_type => 'email',
136 lang => $patron->lang,
138 borrowers => $self->{patron_id},
139 branches => $library_id,
143 offsets => \@account_offsets,
148 C4::Letters::EnqueueLetter(
151 borrowernumber => $self->{patron_id},
152 message_transport_type => 'email',
154 ) or warn "can't enqueue letter $letter";
158 my $renew_outcomes = [];
159 for my $message ( @{$payment->object_messages} ) {
160 push @{$renew_outcomes}, $message->payload;
163 return { payment_id => $payment->id, renew_result => $renew_outcomes };
168 This method allows adding credits to a patron's account
170 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
173 description => $description,
174 interface => $interface,
175 issue_id => $checkout->id,
177 library_id => $library_id,
179 payment_type => $payment_type,
180 type => $credit_type,
185 $credit_type can be any of:
199 my ( $self, $params ) = @_;
201 # check for mandatory params
202 my @mandatory = ( 'interface', 'amount' );
203 for my $param (@mandatory) {
204 unless ( defined( $params->{$param} ) ) {
205 Koha::Exceptions::MissingParameter->throw(
206 error => "The $param parameter is mandatory" );
210 # amount should always be passed as a positive value
211 my $amount = $params->{amount} * -1;
212 unless ( $amount < 0 ) {
213 Koha::Exceptions::Account::AmountNotPositive->throw(
214 error => 'Debit amount passed is not positive' );
217 my $description = $params->{description} // q{};
218 my $note = $params->{note} // q{};
219 my $user_id = $params->{user_id};
220 my $interface = $params->{interface};
221 my $library_id = $params->{library_id};
222 my $cash_register = $params->{cash_register};
223 my $payment_type = $params->{payment_type};
224 my $credit_type = $params->{type} || 'PAYMENT';
225 my $item_id = $params->{item_id};
226 my $issue_id = $params->{issue_id};
228 Koha::Exceptions::Account::RegisterRequired->throw()
229 if ( C4::Context->preference("UseCashRegisters")
230 && defined($payment_type)
231 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
232 && !defined($cash_register) );
235 my $schema = Koha::Database->new->schema;
240 # Insert the account line
241 $line = Koha::Account::Line->new(
243 borrowernumber => $self->{patron_id},
246 description => $description,
247 credit_type_code => $credit_type,
248 amountoutstanding => $amount,
249 payment_type => $payment_type,
251 manager_id => $user_id,
252 interface => $interface,
253 branchcode => $library_id,
254 register_id => $cash_register,
255 itemnumber => $item_id,
256 issue_id => $issue_id,
260 # Record the account offset
261 my $account_offset = Koha::Account::Offset->new(
263 credit_id => $line->id,
265 amount => $amount * -1
269 C4::Stats::UpdateStats(
271 branch => $library_id,
272 type => lc($credit_type),
274 borrowernumber => $self->{patron_id},
275 interface => $interface,
277 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
280 'after_account_action',
282 action => "add_credit",
284 type => lc($credit_type),
285 line => $line->get_from_storage, #TODO Seems unneeded
290 if ( C4::Context->preference("FinesLog") ) {
296 action => "create_$credit_type",
297 borrowernumber => $self->{patron_id},
299 description => $description,
300 amountoutstanding => $amount,
301 credit_type_code => $credit_type,
303 itemnumber => $item_id,
304 manager_id => $user_id,
305 branchcode => $library_id,
315 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
316 if ( $_->broken_fk eq 'credit_type_code' ) {
317 Koha::Exceptions::Account::UnrecognisedType->throw(
318 error => 'Type of credit not recognised' );
331 my $credit = $account->payin_amount(
334 type => $credit_type,
335 payment_type => $payment_type,
336 cash_register => $register_id,
337 interface => $interface,
338 library_id => $branchcode,
339 user_id => $staff_id,
340 debits => $debit_lines,
341 description => $description,
346 This method allows an amount to be paid into a patrons account and immediately applied against debts.
348 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
350 $credit_type can be any of:
358 my ( $self, $params ) = @_;
360 # check for mandatory params
361 my @mandatory = ( 'interface', 'amount', 'type' );
362 for my $param (@mandatory) {
363 unless ( defined( $params->{$param} ) ) {
364 Koha::Exceptions::MissingParameter->throw(
365 error => "The $param parameter is mandatory" );
369 # Check for mandatory register
370 Koha::Exceptions::Account::RegisterRequired->throw()
371 if ( C4::Context->preference("UseCashRegisters")
372 && defined( $params->{payment_type} )
373 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
374 && !defined($params->{cash_register}) );
376 # amount should always be passed as a positive value
377 my $amount = $params->{amount};
378 unless ( $amount > 0 ) {
379 Koha::Exceptions::Account::AmountNotPositive->throw(
380 error => 'Payin amount passed is not positive' );
384 my $schema = Koha::Database->new->schema;
389 $credit = $self->add_credit($params);
391 # Offset debts passed first
392 if ( exists( $params->{debits} ) ) {
393 $credit = $credit->apply(
395 debits => $params->{debits}
400 # Offset against remaining balance if AutoReconcile
401 if ( C4::Context->preference("AccountAutoReconcile")
402 && $credit->amountoutstanding != 0 )
404 $credit = $credit->apply(
406 debits => [ $self->outstanding_debits->as_list ]
418 This method allows adding debits to a patron's account
420 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
423 description => $description,
426 interface => $interface,
427 library_id => $library_id,
429 transaction_type => $transaction_type,
430 cash_register => $register_id,
432 issue_id => $issue_id
436 $debit_type can be any of:
456 my ( $self, $params ) = @_;
458 # check for mandatory params
459 my @mandatory = ( 'interface', 'type', 'amount' );
460 for my $param (@mandatory) {
461 unless ( defined( $params->{$param} ) ) {
462 Koha::Exceptions::MissingParameter->throw(
463 error => "The $param parameter is mandatory" );
467 # check for cash register if using cash
468 Koha::Exceptions::Account::RegisterRequired->throw()
469 if ( C4::Context->preference("UseCashRegisters")
470 && defined( $params->{transaction_type} )
471 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
472 && !defined( $params->{cash_register} ) );
474 # amount should always be a positive value
475 my $amount = $params->{amount};
476 unless ( $amount > 0 ) {
477 Koha::Exceptions::Account::AmountNotPositive->throw(
478 error => 'Debit amount passed is not positive' );
481 my $description = $params->{description} // q{};
482 my $note = $params->{note} // q{};
483 my $user_id = $params->{user_id};
484 my $interface = $params->{interface};
485 my $library_id = $params->{library_id};
486 my $cash_register = $params->{cash_register};
487 my $debit_type = $params->{type};
488 my $transaction_type = $params->{transaction_type};
489 my $item_id = $params->{item_id};
490 my $issue_id = $params->{issue_id};
493 my $schema = Koha::Database->new->schema;
498 # Insert the account line
499 $line = Koha::Account::Line->new(
501 borrowernumber => $self->{patron_id},
504 description => $description,
505 debit_type_code => $debit_type,
506 amountoutstanding => $amount,
507 payment_type => $transaction_type,
509 manager_id => $user_id,
510 interface => $interface,
511 itemnumber => $item_id,
512 issue_id => $issue_id,
513 branchcode => $library_id,
514 register_id => $cash_register,
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,
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 $debit = $account->payout_amount(
574 payout_type => $payout_type,
575 register_id => $register_id,
576 staff_id => $staff_id,
577 interface => 'intranet',
579 credits => $credit_lines
583 This method allows an amount to be paid out from a patrons account against outstanding credits.
585 $payout_type can be any of the defined payment_types:
590 my ( $self, $params ) = @_;
592 # Check for mandatory parameters
594 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
595 for my $param (@mandatory) {
596 unless ( defined( $params->{$param} ) ) {
597 Koha::Exceptions::MissingParameter->throw(
598 error => "The $param parameter is mandatory" );
602 # Check for mandatory register
603 Koha::Exceptions::Account::RegisterRequired->throw()
604 if ( C4::Context->preference("UseCashRegisters")
605 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
606 && !defined($params->{cash_register}) );
608 # Amount should always be passed as a positive value
609 my $amount = $params->{amount};
610 unless ( $amount > 0 ) {
611 Koha::Exceptions::Account::AmountNotPositive->throw(
612 error => 'Payout amount passed is not positive' );
615 # Amount should always be less than or equal to outstanding credit
617 my $outstanding_credits =
618 exists( $params->{credits} )
620 : $self->outstanding_credits->as_list;
621 for my $credit ( @{$outstanding_credits} ) {
622 $outstanding += $credit->amountoutstanding;
624 $outstanding = $outstanding * -1;
625 Koha::Exceptions::ParameterTooHigh->throw( error =>
626 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
627 ) unless ( $outstanding >= $amount );
630 my $schema = Koha::Database->new->schema;
634 # A 'payout' is a 'debit'
635 $payout = $self->add_debit(
637 amount => $params->{amount},
639 transaction_type => $params->{payout_type},
640 amountoutstanding => $params->{amount},
641 user_id => $params->{staff_id},
642 interface => $params->{interface},
643 branchcode => $params->{branch},
644 cash_register => $params->{cash_register}
648 # Offset against credits
649 for my $credit ( @{$outstanding_credits} ) {
650 $credit->apply( { debits => [$payout] } );
651 $payout->discard_changes;
652 last if $payout->amountoutstanding == 0;
656 $payout->status('PAID')->store;
665 my $balance = $self->balance
667 Return the balance (sum of amountoutstanding columns)
673 return $self->lines->total_outstanding;
676 =head3 outstanding_debits
678 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
680 It returns the debit lines with outstanding amounts for the patron.
682 It returns a Koha::Account::Lines iterator.
686 sub outstanding_debits {
689 return $self->lines->search(
691 amount => { '>' => 0 },
692 amountoutstanding => { '>' => 0 }
697 =head3 outstanding_credits
699 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
701 It returns the credit lines with outstanding amounts for the patron.
703 It returns a Koha::Account::Lines iterator.
707 sub outstanding_credits {
710 return $self->lines->search(
712 amount => { '<' => 0 },
713 amountoutstanding => { '<' => 0 }
718 =head3 non_issues_charges
720 my $non_issues_charges = $self->non_issues_charges
722 Calculates amount immediately owing by the patron - non-issue charges.
724 Charges can be set as exempt from non-issue by editing the debit type in the Debit Types area of System Preferences.
728 sub non_issues_charges {
731 my @blocking_debit_types = Koha::Account::DebitTypes->search({ restricts_checkouts => 1 }, { columns => 'code' })->get_column('code');
733 return $self->lines->search(
735 debit_type_code => { -in => \@blocking_debit_types }
737 )->total_outstanding;
742 my $lines = $self->lines;
744 Return all credits and debits for the user, outstanding or otherwise
751 return Koha::Account::Lines->search(
753 borrowernumber => $self->{patron_id},
761 my $credits = $self->credits;
763 Return all credits for the user
770 return Koha::Account::Credits->search(
772 borrowernumber => $self->{patron_id}
779 my $debits = $self->debits;
781 Return all debits for the user
788 return Koha::Account::Debits->search(
790 borrowernumber => $self->{patron_id},
795 =head3 reconcile_balance
797 $account->reconcile_balance();
799 Find outstanding credits and use them to pay outstanding debits.
800 Currently, this implicitly uses the 'First In First Out' rule for
801 applying credits against debits.
805 sub reconcile_balance {
808 my $outstanding_debits = $self->outstanding_debits;
809 my $outstanding_credits = $self->outstanding_credits;
811 while ( $outstanding_debits->total_outstanding > 0
812 and my $credit = $outstanding_credits->next )
814 # there's both outstanding debits and credits
815 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
817 $outstanding_debits = $self->outstanding_debits;
830 Kyle M Hall <kyle.m.hall@gmail.com>
831 Tomás Cohen Arazi <tomascohen@gmail.com>
832 Martin Renvoize <martin.renvoize@ptfs-europe.com>