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,
164 interface => $interface,
165 library_id => $library_id,
166 payment_type => $payment_type,
167 type => $credit_type,
172 $credit_type can be any of:
186 my ( $self, $params ) = @_;
188 # check for mandatory params
189 my @mandatory = ( 'interface', 'amount' );
190 for my $param (@mandatory) {
191 unless ( defined( $params->{$param} ) ) {
192 Koha::Exceptions::MissingParameter->throw(
193 error => "The $param parameter is mandatory" );
197 # amount should always be passed as a positive value
198 my $amount = $params->{amount} * -1;
199 unless ( $amount < 0 ) {
200 Koha::Exceptions::Account::AmountNotPositive->throw(
201 error => 'Debit amount passed is not positive' );
204 my $description = $params->{description} // q{};
205 my $note = $params->{note} // q{};
206 my $user_id = $params->{user_id};
207 my $interface = $params->{interface};
208 my $library_id = $params->{library_id};
209 my $cash_register = $params->{cash_register};
210 my $payment_type = $params->{payment_type};
211 my $credit_type = $params->{type} || 'PAYMENT';
212 my $item_id = $params->{item_id};
213 my $issue_id = $params->{issue_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,
243 issue_id => $issue_id,
247 # Record the account offset
248 my $account_offset = Koha::Account::Offset->new(
250 credit_id => $line->id,
252 amount => $amount * -1
256 C4::Stats::UpdateStats(
258 branch => $library_id,
259 type => lc($credit_type),
261 borrowernumber => $self->{patron_id},
262 interface => $interface,
264 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
267 'after_account_action',
269 action => "add_credit",
271 type => lc($credit_type),
272 line => $line->get_from_storage, #TODO Seems unneeded
277 if ( C4::Context->preference("FinesLog") ) {
283 action => "create_$credit_type",
284 borrowernumber => $self->{patron_id},
286 description => $description,
287 amountoutstanding => $amount,
288 credit_type_code => $credit_type,
290 itemnumber => $item_id,
291 manager_id => $user_id,
292 branchcode => $library_id,
302 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
303 if ( $_->broken_fk eq 'credit_type_code' ) {
304 Koha::Exceptions::Account::UnrecognisedType->throw(
305 error => 'Type of credit not recognised' );
318 my $credit = $account->payin_amount(
321 type => $credit_type,
322 payment_type => $payment_type,
323 cash_register => $register_id,
324 interface => $interface,
325 library_id => $branchcode,
326 user_id => $staff_id,
327 debits => $debit_lines,
328 description => $description,
333 This method allows an amount to be paid into a patrons account and immediately applied against debts.
335 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
337 $credit_type can be any of:
345 my ( $self, $params ) = @_;
347 # check for mandatory params
348 my @mandatory = ( 'interface', 'amount', 'type' );
349 for my $param (@mandatory) {
350 unless ( defined( $params->{$param} ) ) {
351 Koha::Exceptions::MissingParameter->throw(
352 error => "The $param parameter is mandatory" );
356 # Check for mandatory register
357 Koha::Exceptions::Account::RegisterRequired->throw()
358 if ( C4::Context->preference("UseCashRegisters")
359 && defined( $params->{payment_type} )
360 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
361 && !defined($params->{cash_register}) );
363 # amount should always be passed as a positive value
364 my $amount = $params->{amount};
365 unless ( $amount > 0 ) {
366 Koha::Exceptions::Account::AmountNotPositive->throw(
367 error => 'Payin amount passed is not positive' );
371 my $schema = Koha::Database->new->schema;
376 $credit = $self->add_credit($params);
378 # Offset debts passed first
379 if ( exists( $params->{debits} ) ) {
380 $credit = $credit->apply(
382 debits => $params->{debits}
387 # Offset against remaining balance if AutoReconcile
388 if ( C4::Context->preference("AccountAutoReconcile")
389 && $credit->amountoutstanding != 0 )
391 $credit = $credit->apply(
393 debits => [ $self->outstanding_debits->as_list ]
405 This method allows adding debits to a patron's account
407 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
410 description => $description,
413 interface => $interface,
414 library_id => $library_id,
416 transaction_type => $transaction_type,
417 cash_register => $register_id,
419 issue_id => $issue_id
423 $debit_type can be any of:
443 my ( $self, $params ) = @_;
445 # check for mandatory params
446 my @mandatory = ( 'interface', 'type', 'amount' );
447 for my $param (@mandatory) {
448 unless ( defined( $params->{$param} ) ) {
449 Koha::Exceptions::MissingParameter->throw(
450 error => "The $param parameter is mandatory" );
454 # check for cash register if using cash
455 Koha::Exceptions::Account::RegisterRequired->throw()
456 if ( C4::Context->preference("UseCashRegisters")
457 && defined( $params->{transaction_type} )
458 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
459 && !defined( $params->{cash_register} ) );
461 # amount should always be a positive value
462 my $amount = $params->{amount};
463 unless ( $amount > 0 ) {
464 Koha::Exceptions::Account::AmountNotPositive->throw(
465 error => 'Debit amount passed is not positive' );
468 my $description = $params->{description} // q{};
469 my $note = $params->{note} // q{};
470 my $user_id = $params->{user_id};
471 my $interface = $params->{interface};
472 my $library_id = $params->{library_id};
473 my $cash_register = $params->{cash_register};
474 my $debit_type = $params->{type};
475 my $transaction_type = $params->{transaction_type};
476 my $item_id = $params->{item_id};
477 my $issue_id = $params->{issue_id};
480 my $schema = Koha::Database->new->schema;
485 # Insert the account line
486 $line = Koha::Account::Line->new(
488 borrowernumber => $self->{patron_id},
491 description => $description,
492 debit_type_code => $debit_type,
493 amountoutstanding => $amount,
494 payment_type => $transaction_type,
496 manager_id => $user_id,
497 interface => $interface,
498 itemnumber => $item_id,
499 issue_id => $issue_id,
500 branchcode => $library_id,
501 register_id => $cash_register,
503 $debit_type eq 'OVERDUE'
504 ? ( status => 'UNRETURNED' )
510 # Record the account offset
511 my $account_offset = Koha::Account::Offset->new(
513 debit_id => $line->id,
519 if ( C4::Context->preference("FinesLog") ) {
525 action => "create_$debit_type",
526 borrowernumber => $self->{patron_id},
528 description => $description,
529 amountoutstanding => $amount,
530 debit_type_code => $debit_type,
532 itemnumber => $item_id,
533 manager_id => $user_id,
543 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
544 if ( $_->broken_fk eq 'debit_type_code' ) {
545 Koha::Exceptions::Account::UnrecognisedType->throw(
546 error => 'Type of debit not recognised' );
559 my $debit = $account->payout_amount(
561 payout_type => $payout_type,
562 register_id => $register_id,
563 staff_id => $staff_id,
564 interface => 'intranet',
566 credits => $credit_lines
570 This method allows an amount to be paid out from a patrons account against outstanding credits.
572 $payout_type can be any of the defined payment_types:
577 my ( $self, $params ) = @_;
579 # Check for mandatory parameters
581 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
582 for my $param (@mandatory) {
583 unless ( defined( $params->{$param} ) ) {
584 Koha::Exceptions::MissingParameter->throw(
585 error => "The $param parameter is mandatory" );
589 # Check for mandatory register
590 Koha::Exceptions::Account::RegisterRequired->throw()
591 if ( C4::Context->preference("UseCashRegisters")
592 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
593 && !defined($params->{cash_register}) );
595 # Amount should always be passed as a positive value
596 my $amount = $params->{amount};
597 unless ( $amount > 0 ) {
598 Koha::Exceptions::Account::AmountNotPositive->throw(
599 error => 'Payout amount passed is not positive' );
602 # Amount should always be less than or equal to outstanding credit
604 my $outstanding_credits =
605 exists( $params->{credits} )
607 : $self->outstanding_credits->as_list;
608 for my $credit ( @{$outstanding_credits} ) {
609 $outstanding += $credit->amountoutstanding;
611 $outstanding = $outstanding * -1;
612 Koha::Exceptions::ParameterTooHigh->throw( error =>
613 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
614 ) unless ( $outstanding >= $amount );
617 my $schema = Koha::Database->new->schema;
621 # A 'payout' is a 'debit'
622 $payout = $self->add_debit(
624 amount => $params->{amount},
626 transaction_type => $params->{payout_type},
627 amountoutstanding => $params->{amount},
628 user_id => $params->{staff_id},
629 interface => $params->{interface},
630 branchcode => $params->{branch},
631 cash_register => $params->{cash_register}
635 # Offset against credits
636 for my $credit ( @{$outstanding_credits} ) {
637 $credit->apply( { debits => [$payout] } );
638 $payout->discard_changes;
639 last if $payout->amountoutstanding == 0;
643 $payout->status('PAID')->store;
652 my $balance = $self->balance
654 Return the balance (sum of amountoutstanding columns)
660 return $self->lines->total_outstanding;
663 =head3 outstanding_debits
665 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
667 It returns the debit lines with outstanding amounts for the patron.
669 It returns a Koha::Account::Lines iterator.
673 sub outstanding_debits {
676 return $self->lines->search(
678 amount => { '>' => 0 },
679 amountoutstanding => { '>' => 0 }
684 =head3 outstanding_credits
686 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
688 It returns the credit lines with outstanding amounts for the patron.
690 It returns a Koha::Account::Lines iterator.
694 sub outstanding_credits {
697 return $self->lines->search(
699 amount => { '<' => 0 },
700 amountoutstanding => { '<' => 0 }
705 =head3 non_issues_charges
707 my $non_issues_charges = $self->non_issues_charges
709 Calculates amount immediately owing by the patron - non-issue charges.
711 Charges can be set as exempt from non-issue by editing the debit type in the Debit Types area of System Preferences.
715 sub non_issues_charges {
718 my @blocking_debit_types = Koha::Account::DebitTypes->search({ restricts_checkouts => 1 }, { columns => 'code' })->get_column('code');
720 return $self->lines->search(
722 debit_type_code => { -in => \@blocking_debit_types }
724 )->total_outstanding;
729 my $lines = $self->lines;
731 Return all credits and debits for the user, outstanding or otherwise
738 return Koha::Account::Lines->search(
740 borrowernumber => $self->{patron_id},
748 my $credits = $self->credits;
750 Return all credits for the user
757 return Koha::Account::Credits->search(
759 borrowernumber => $self->{patron_id}
766 my $debits = $self->debits;
768 Return all debits for the user
775 return Koha::Account::Debits->search(
777 borrowernumber => $self->{patron_id},
782 =head3 reconcile_balance
784 $account->reconcile_balance();
786 Find outstanding credits and use them to pay outstanding debits.
787 Currently, this implicitly uses the 'First In First Out' rule for
788 applying credits against debits.
792 sub reconcile_balance {
795 my $outstanding_debits = $self->outstanding_debits;
796 my $outstanding_credits = $self->outstanding_credits;
798 while ( $outstanding_debits->total_outstanding > 0
799 and my $credit = $outstanding_credits->next )
801 # there's both outstanding debits and credits
802 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
804 $outstanding_debits = $self->outstanding_debits;
817 Kyle M Hall <kyle.m.hall@gmail.com>
818 Tomás Cohen Arazi <tomascohen@gmail.com>
819 Martin Renvoize <martin.renvoize@ptfs-europe.com>