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 my $manager_id = $userenv ? $userenv->{number} : undef;
91 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
92 my $payment = $self->payin_amount(
94 interface => $interface,
97 payment_type => $payment_type,
98 cash_register => $cash_register,
99 user_id => $manager_id,
100 library_id => $library_id,
102 description => $description,
108 # NOTE: Pay historically always applied as much credit as it could to all
109 # existing outstanding debits, whether passed specific debits or otherwise.
110 if ( $payment->amountoutstanding ) {
113 { debits => [ $self->outstanding_debits->as_list ] } );
116 my $patron = Koha::Patrons->find( $self->{patron_id} );
117 my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
118 if ( C4::Context->preference('UseEmailReceipts') ) {
120 my $letter = C4::Letters::GetPreparedLetter(
121 module => 'circulation',
122 letter_code => uc("ACCOUNT_$type"),
123 message_transport_type => 'email',
124 lang => $patron->lang,
126 borrowers => $self->{patron_id},
127 branches => $library_id,
131 offsets => \@account_offsets,
136 C4::Letters::EnqueueLetter(
139 borrowernumber => $self->{patron_id},
140 message_transport_type => 'email',
142 ) or warn "can't enqueue letter $letter";
146 my $renew_outcomes = [];
147 for my $message ( @{$payment->object_messages} ) {
148 push @{$renew_outcomes}, $message->payload;
151 return { payment_id => $payment->id, renew_result => $renew_outcomes };
156 This method allows adding credits to a patron's account
158 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
161 description => $description,
162 interface => $interface,
163 issue_id => $checkout->id,
165 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};
214 my $issue_id = $params->{issue_id};
216 Koha::Exceptions::Account::RegisterRequired->throw()
217 if ( C4::Context->preference("UseCashRegisters")
218 && defined($payment_type)
219 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
220 && !defined($cash_register) );
223 my $schema = Koha::Database->new->schema;
228 # Insert the account line
229 $line = Koha::Account::Line->new(
231 borrowernumber => $self->{patron_id},
234 description => $description,
235 credit_type_code => $credit_type,
236 amountoutstanding => $amount,
237 payment_type => $payment_type,
239 manager_id => $user_id,
240 interface => $interface,
241 branchcode => $library_id,
242 register_id => $cash_register,
243 itemnumber => $item_id,
244 issue_id => $issue_id,
248 # Record the account offset
249 my $account_offset = Koha::Account::Offset->new(
251 credit_id => $line->id,
253 amount => $amount * -1
257 C4::Stats::UpdateStats(
259 branch => $library_id,
260 type => lc($credit_type),
262 borrowernumber => $self->{patron_id},
263 interface => $interface,
265 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
268 'after_account_action',
270 action => "add_credit",
272 type => lc($credit_type),
273 line => $line->get_from_storage, #TODO Seems unneeded
278 if ( C4::Context->preference("FinesLog") ) {
284 action => "create_$credit_type",
285 borrowernumber => $self->{patron_id},
287 description => $description,
288 amountoutstanding => $amount,
289 credit_type_code => $credit_type,
291 itemnumber => $item_id,
292 manager_id => $user_id,
293 branchcode => $library_id,
303 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
304 if ( $_->broken_fk eq 'credit_type_code' ) {
305 Koha::Exceptions::Account::UnrecognisedType->throw(
306 error => 'Type of credit not recognised' );
319 my $credit = $account->payin_amount(
322 type => $credit_type,
323 payment_type => $payment_type,
324 cash_register => $register_id,
325 interface => $interface,
326 library_id => $branchcode,
327 user_id => $staff_id,
328 debits => $debit_lines,
329 description => $description,
334 This method allows an amount to be paid into a patrons account and immediately applied against debts.
336 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
338 $credit_type can be any of:
346 my ( $self, $params ) = @_;
348 # check for mandatory params
349 my @mandatory = ( 'interface', 'amount', 'type' );
350 for my $param (@mandatory) {
351 unless ( defined( $params->{$param} ) ) {
352 Koha::Exceptions::MissingParameter->throw(
353 error => "The $param parameter is mandatory" );
357 # Check for mandatory register
358 Koha::Exceptions::Account::RegisterRequired->throw()
359 if ( C4::Context->preference("UseCashRegisters")
360 && defined( $params->{payment_type} )
361 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
362 && !defined($params->{cash_register}) );
364 # amount should always be passed as a positive value
365 my $amount = $params->{amount};
366 unless ( $amount > 0 ) {
367 Koha::Exceptions::Account::AmountNotPositive->throw(
368 error => 'Payin amount passed is not positive' );
372 my $schema = Koha::Database->new->schema;
377 $credit = $self->add_credit($params);
379 # Offset debts passed first
380 if ( exists( $params->{debits} ) ) {
381 $credit = $credit->apply(
383 debits => $params->{debits}
388 # Offset against remaining balance if AutoReconcile
389 if ( C4::Context->preference("AccountAutoReconcile")
390 && $credit->amountoutstanding != 0 )
392 $credit = $credit->apply(
394 debits => [ $self->outstanding_debits->as_list ]
406 This method allows adding debits to a patron's account
408 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
411 description => $description,
414 interface => $interface,
415 library_id => $library_id,
417 transaction_type => $transaction_type,
418 cash_register => $register_id,
420 issue_id => $issue_id
424 $debit_type can be any of:
444 my ( $self, $params ) = @_;
446 # check for mandatory params
447 my @mandatory = ( 'interface', 'type', 'amount' );
448 for my $param (@mandatory) {
449 unless ( defined( $params->{$param} ) ) {
450 Koha::Exceptions::MissingParameter->throw(
451 error => "The $param parameter is mandatory" );
455 # check for cash register if using cash
456 Koha::Exceptions::Account::RegisterRequired->throw()
457 if ( C4::Context->preference("UseCashRegisters")
458 && defined( $params->{transaction_type} )
459 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
460 && !defined( $params->{cash_register} ) );
462 # amount should always be a positive value
463 my $amount = $params->{amount};
464 unless ( $amount > 0 ) {
465 Koha::Exceptions::Account::AmountNotPositive->throw(
466 error => 'Debit amount passed is not positive' );
469 my $description = $params->{description} // q{};
470 my $note = $params->{note} // q{};
471 my $user_id = $params->{user_id};
472 my $interface = $params->{interface};
473 my $library_id = $params->{library_id};
474 my $cash_register = $params->{cash_register};
475 my $debit_type = $params->{type};
476 my $transaction_type = $params->{transaction_type};
477 my $item_id = $params->{item_id};
478 my $issue_id = $params->{issue_id};
481 my $schema = Koha::Database->new->schema;
486 # Insert the account line
487 $line = Koha::Account::Line->new(
489 borrowernumber => $self->{patron_id},
492 description => $description,
493 debit_type_code => $debit_type,
494 amountoutstanding => $amount,
495 payment_type => $transaction_type,
497 manager_id => $user_id,
498 interface => $interface,
499 itemnumber => $item_id,
500 issue_id => $issue_id,
501 branchcode => $library_id,
502 register_id => $cash_register,
504 $debit_type eq 'OVERDUE'
505 ? ( status => 'UNRETURNED' )
511 # Record the account offset
512 my $account_offset = Koha::Account::Offset->new(
514 debit_id => $line->id,
520 if ( C4::Context->preference("FinesLog") ) {
526 action => "create_$debit_type",
527 borrowernumber => $self->{patron_id},
529 description => $description,
530 amountoutstanding => $amount,
531 debit_type_code => $debit_type,
533 itemnumber => $item_id,
534 manager_id => $user_id,
544 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
545 if ( $_->broken_fk eq 'debit_type_code' ) {
546 Koha::Exceptions::Account::UnrecognisedType->throw(
547 error => 'Type of debit not recognised' );
560 my $debit = $account->payout_amount(
562 payout_type => $payout_type,
563 register_id => $register_id,
564 staff_id => $staff_id,
565 interface => 'intranet',
567 credits => $credit_lines
571 This method allows an amount to be paid out from a patrons account against outstanding credits.
573 $payout_type can be any of the defined payment_types:
578 my ( $self, $params ) = @_;
580 # Check for mandatory parameters
582 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
583 for my $param (@mandatory) {
584 unless ( defined( $params->{$param} ) ) {
585 Koha::Exceptions::MissingParameter->throw(
586 error => "The $param parameter is mandatory" );
590 # Check for mandatory register
591 Koha::Exceptions::Account::RegisterRequired->throw()
592 if ( C4::Context->preference("UseCashRegisters")
593 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
594 && !defined($params->{cash_register}) );
596 # Amount should always be passed as a positive value
597 my $amount = $params->{amount};
598 unless ( $amount > 0 ) {
599 Koha::Exceptions::Account::AmountNotPositive->throw(
600 error => 'Payout amount passed is not positive' );
603 # Amount should always be less than or equal to outstanding credit
605 my $outstanding_credits =
606 exists( $params->{credits} )
608 : $self->outstanding_credits->as_list;
609 for my $credit ( @{$outstanding_credits} ) {
610 $outstanding += $credit->amountoutstanding;
612 $outstanding = $outstanding * -1;
613 Koha::Exceptions::ParameterTooHigh->throw( error =>
614 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
615 ) unless ( $outstanding >= $amount );
618 my $schema = Koha::Database->new->schema;
622 # A 'payout' is a 'debit'
623 $payout = $self->add_debit(
625 amount => $params->{amount},
627 transaction_type => $params->{payout_type},
628 amountoutstanding => $params->{amount},
629 user_id => $params->{staff_id},
630 interface => $params->{interface},
631 branchcode => $params->{branch},
632 cash_register => $params->{cash_register}
636 # Offset against credits
637 for my $credit ( @{$outstanding_credits} ) {
638 $credit->apply( { debits => [$payout] } );
639 $payout->discard_changes;
640 last if $payout->amountoutstanding == 0;
644 $payout->status('PAID')->store;
653 my $balance = $self->balance
655 Return the balance (sum of amountoutstanding columns)
661 return $self->lines->total_outstanding;
664 =head3 outstanding_debits
666 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
668 It returns the debit lines with outstanding amounts for the patron.
670 It returns a Koha::Account::Lines iterator.
674 sub outstanding_debits {
677 return $self->lines->search(
679 amount => { '>' => 0 },
680 amountoutstanding => { '>' => 0 }
685 =head3 outstanding_credits
687 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
689 It returns the credit lines with outstanding amounts for the patron.
691 It returns a Koha::Account::Lines iterator.
695 sub outstanding_credits {
698 return $self->lines->search(
700 amount => { '<' => 0 },
701 amountoutstanding => { '<' => 0 }
706 =head3 non_issues_charges
708 my $non_issues_charges = $self->non_issues_charges
710 Calculates amount immediately owing by the patron - non-issue charges.
712 Charges can be set as exempt from non-issue by editing the debit type in the Debit Types area of System Preferences.
716 sub non_issues_charges {
719 my @blocking_debit_types = Koha::Account::DebitTypes->search({ restricts_checkouts => 1 }, { columns => 'code' })->get_column('code');
721 return $self->lines->search(
723 debit_type_code => { -in => \@blocking_debit_types }
725 )->total_outstanding;
730 my $lines = $self->lines;
732 Return all credits and debits for the user, outstanding or otherwise
739 return Koha::Account::Lines->search(
741 borrowernumber => $self->{patron_id},
749 my $credits = $self->credits;
751 Return all credits for the user
758 return Koha::Account::Credits->search(
760 borrowernumber => $self->{patron_id}
767 my $debits = $self->debits;
769 Return all debits for the user
776 return Koha::Account::Debits->search(
778 borrowernumber => $self->{patron_id},
783 =head3 reconcile_balance
785 $account->reconcile_balance();
787 Find outstanding credits and use them to pay outstanding debits.
788 Currently, this implicitly uses the 'First In First Out' rule for
789 applying credits against debits.
793 sub reconcile_balance {
796 my $outstanding_debits = $self->outstanding_debits;
797 my $outstanding_credits = $self->outstanding_credits;
799 while ( $outstanding_debits->total_outstanding > 0
800 and my $credit = $outstanding_credits->next )
802 # there's both outstanding debits and credits
803 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
805 $outstanding_debits = $self->outstanding_debits;
818 Kyle M Hall <kyle.m.hall@gmail.com>
819 Tomás Cohen Arazi <tomascohen@gmail.com>
820 Martin Renvoize <martin.renvoize@ptfs-europe.com>