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 unless ( $type eq 'WRITEOFF' ) {
91 Koha::Exceptions::Account::PaymentTypeRequired->throw()
92 if ( C4::Context->preference("RequirePaymentType")
93 && !defined($payment_type) );
95 my $av = Koha::AuthorisedValues->search_with_library_limits(
96 { category => 'PAYMENT_TYPE', authorised_value => $payment_type } );
98 if ( !$av->count && C4::Context->preference("RequirePaymentType") ) {
99 Koha::Exceptions::Account::InvalidPaymentType->throw( 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' );
329 my $credit = $account->payin_amount(
332 type => $credit_type,
333 payment_type => $payment_type,
334 cash_register => $register_id,
335 interface => $interface,
336 library_id => $branchcode,
337 user_id => $staff_id,
338 debits => $debit_lines,
339 description => $description,
344 This method allows an amount to be paid into a patrons account and immediately applied against debts.
346 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
348 $credit_type can be any of:
356 my ( $self, $params ) = @_;
358 # check for mandatory params
359 my @mandatory = ( 'interface', 'amount', 'type' );
360 for my $param (@mandatory) {
361 unless ( defined( $params->{$param} ) ) {
362 Koha::Exceptions::MissingParameter->throw(
363 error => "The $param parameter is mandatory" );
367 # Check for mandatory register
368 Koha::Exceptions::Account::RegisterRequired->throw()
369 if ( C4::Context->preference("UseCashRegisters")
370 && defined( $params->{payment_type} )
371 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
372 && !defined($params->{cash_register}) );
374 # amount should always be passed as a positive value
375 my $amount = $params->{amount};
376 unless ( $amount > 0 ) {
377 Koha::Exceptions::Account::AmountNotPositive->throw(
378 error => 'Payin amount passed is not positive' );
382 my $schema = Koha::Database->new->schema;
387 $credit = $self->add_credit($params);
389 # Offset debts passed first
390 if ( exists( $params->{debits} ) ) {
391 $credit = $credit->apply(
393 debits => $params->{debits}
398 # Offset against remaining balance if AutoReconcile
399 if ( C4::Context->preference("AccountAutoReconcile")
400 && $credit->amountoutstanding != 0 )
402 $credit = $credit->apply(
404 debits => [ $self->outstanding_debits->as_list ]
416 This method allows adding debits to a patron's account
418 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
421 description => $description,
424 interface => $interface,
425 library_id => $library_id,
427 transaction_type => $transaction_type,
428 cash_register => $register_id,
430 issue_id => $issue_id
434 $debit_type can be any of:
454 my ( $self, $params ) = @_;
456 # check for mandatory params
457 my @mandatory = ( 'interface', 'type', 'amount' );
458 for my $param (@mandatory) {
459 unless ( defined( $params->{$param} ) ) {
460 Koha::Exceptions::MissingParameter->throw(
461 error => "The $param parameter is mandatory" );
465 # check for cash register if using cash
466 Koha::Exceptions::Account::RegisterRequired->throw()
467 if ( C4::Context->preference("UseCashRegisters")
468 && defined( $params->{transaction_type} )
469 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
470 && !defined( $params->{cash_register} ) );
472 # amount should always be a positive value
473 my $amount = $params->{amount};
474 unless ( $amount > 0 ) {
475 Koha::Exceptions::Account::AmountNotPositive->throw(
476 error => 'Debit amount passed is not positive' );
479 my $description = $params->{description} // q{};
480 my $note = $params->{note} // q{};
481 my $user_id = $params->{user_id};
482 my $interface = $params->{interface};
483 my $library_id = $params->{library_id};
484 my $cash_register = $params->{cash_register};
485 my $debit_type = $params->{type};
486 my $transaction_type = $params->{transaction_type};
487 my $item_id = $params->{item_id};
488 my $issue_id = $params->{issue_id};
491 my $schema = Koha::Database->new->schema;
496 # Insert the account line
497 $line = Koha::Account::Line->new(
499 borrowernumber => $self->{patron_id},
502 description => $description,
503 debit_type_code => $debit_type,
504 amountoutstanding => $amount,
505 payment_type => $transaction_type,
507 manager_id => $user_id,
508 interface => $interface,
509 itemnumber => $item_id,
510 issue_id => $issue_id,
511 branchcode => $library_id,
512 register_id => $cash_register,
514 $debit_type eq 'OVERDUE'
515 ? ( status => 'UNRETURNED' )
521 # Record the account offset
522 my $account_offset = Koha::Account::Offset->new(
524 debit_id => $line->id,
530 if ( C4::Context->preference("FinesLog") ) {
536 action => "create_$debit_type",
537 borrowernumber => $self->{patron_id},
539 description => $description,
540 amountoutstanding => $amount,
541 debit_type_code => $debit_type,
543 itemnumber => $item_id,
544 manager_id => $user_id,
554 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
555 if ( $_->broken_fk eq 'debit_type_code' ) {
556 Koha::Exceptions::Account::UnrecognisedType->throw(
557 error => 'Type of debit not recognised' );
570 my $debit = $account->payout_amount(
572 payout_type => $payout_type,
573 register_id => $register_id,
574 staff_id => $staff_id,
575 interface => 'intranet',
577 credits => $credit_lines
581 This method allows an amount to be paid out from a patrons account against outstanding credits.
583 $payout_type can be any of the defined payment_types:
588 my ( $self, $params ) = @_;
590 # Check for mandatory parameters
592 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
593 for my $param (@mandatory) {
594 unless ( defined( $params->{$param} ) ) {
595 Koha::Exceptions::MissingParameter->throw(
596 error => "The $param parameter is mandatory" );
600 # Check for mandatory register
601 Koha::Exceptions::Account::RegisterRequired->throw()
602 if ( C4::Context->preference("UseCashRegisters")
603 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
604 && !defined($params->{cash_register}) );
606 # Amount should always be passed as a positive value
607 my $amount = $params->{amount};
608 unless ( $amount > 0 ) {
609 Koha::Exceptions::Account::AmountNotPositive->throw(
610 error => 'Payout amount passed is not positive' );
613 # Amount should always be less than or equal to outstanding credit
615 my $outstanding_credits =
616 exists( $params->{credits} )
618 : $self->outstanding_credits->as_list;
619 for my $credit ( @{$outstanding_credits} ) {
620 $outstanding += $credit->amountoutstanding;
622 $outstanding = $outstanding * -1;
623 Koha::Exceptions::ParameterTooHigh->throw( error =>
624 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
625 ) unless ( $outstanding >= $amount );
628 my $schema = Koha::Database->new->schema;
632 # A 'payout' is a 'debit'
633 $payout = $self->add_debit(
635 amount => $params->{amount},
637 transaction_type => $params->{payout_type},
638 amountoutstanding => $params->{amount},
639 user_id => $params->{staff_id},
640 interface => $params->{interface},
641 branchcode => $params->{branch},
642 cash_register => $params->{cash_register}
646 # Offset against credits
647 for my $credit ( @{$outstanding_credits} ) {
648 $credit->apply( { debits => [$payout] } );
649 $payout->discard_changes;
650 last if $payout->amountoutstanding == 0;
654 $payout->status('PAID')->store;
663 my $balance = $self->balance
665 Return the balance (sum of amountoutstanding columns)
671 return $self->lines->total_outstanding;
674 =head3 outstanding_debits
676 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
678 It returns the debit lines with outstanding amounts for the patron.
680 It returns a Koha::Account::Lines iterator.
684 sub outstanding_debits {
687 return $self->lines->search(
689 amount => { '>' => 0 },
690 amountoutstanding => { '>' => 0 }
695 =head3 outstanding_credits
697 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
699 It returns the credit lines with outstanding amounts for the patron.
701 It returns a Koha::Account::Lines iterator.
705 sub outstanding_credits {
708 return $self->lines->search(
710 amount => { '<' => 0 },
711 amountoutstanding => { '<' => 0 }
716 =head3 non_issues_charges
718 my $non_issues_charges = $self->non_issues_charges
720 Calculates amount immediately owing by the patron - non-issue charges.
722 Charges can be set as exempt from non-issue by editing the debit type in the Debit Types area of System Preferences.
726 sub non_issues_charges {
729 my @blocking_debit_types = Koha::Account::DebitTypes->search({ restricts_checkouts => 1 }, { columns => 'code' })->get_column('code');
731 return $self->lines->search(
733 debit_type_code => { -in => \@blocking_debit_types }
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},
759 my $credits = $self->credits;
761 Return all credits for the user
768 return Koha::Account::Credits->search(
770 borrowernumber => $self->{patron_id}
777 my $debits = $self->debits;
779 Return all debits for the user
786 return Koha::Account::Debits->search(
788 borrowernumber => $self->{patron_id},
793 =head3 reconcile_balance
795 $account->reconcile_balance();
797 Find outstanding credits and use them to pay outstanding debits.
798 Currently, this implicitly uses the 'First In First Out' rule for
799 applying credits against debits.
803 sub reconcile_balance {
806 my $outstanding_debits = $self->outstanding_debits;
807 my $outstanding_credits = $self->outstanding_credits;
809 while ( $outstanding_debits->total_outstanding > 0
810 and my $credit = $outstanding_credits->next )
812 # there's both outstanding debits and credits
813 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
815 $outstanding_debits = $self->outstanding_debits;
828 Kyle M Hall <kyle.m.hall@gmail.com>
829 Tomás Cohen Arazi <tomascohen@gmail.com>
830 Martin Renvoize <martin.renvoize@ptfs-europe.com>