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 );
27 use C4::Log qw( logaction );
28 use C4::Stats qw( UpdateStats );
29 use C4::Overdues qw(GetFine);
33 use Koha::Account::Credits;
34 use Koha::Account::Debits;
35 use Koha::Account::Lines;
36 use Koha::Account::Offsets;
37 use Koha::Account::DebitTypes;
39 use Koha::Exceptions::Account;
44 Koha::Accounts - Module for managing payments and fees for patrons
49 my ( $class, $params ) = @_;
51 Carp::croak("No patron id passed in!") unless $params->{patron_id};
53 return bless( $params, $class );
58 This method allows payments to be made against fees/fines
60 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
64 description => $description,
65 library_id => $branchcode,
66 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
67 credit_type => $type, # credit_type_code code
68 item_id => $itemnumber, # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
75 my ( $self, $params ) = @_;
77 my $amount = $params->{amount};
78 my $description = $params->{description};
79 my $note = $params->{note} || q{};
80 my $library_id = $params->{library_id};
81 my $lines = $params->{lines};
82 my $type = $params->{type} || 'PAYMENT';
83 my $payment_type = $params->{payment_type} || undef;
84 my $cash_register = $params->{cash_register};
85 my $item_id = $params->{item_id};
87 my $userenv = C4::Context->userenv;
89 unless ( $type eq 'WRITEOFF' ) {
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(
95 { category => 'PAYMENT_TYPE', authorised_value => $payment_type } );
97 if ( !$av->count && C4::Context->preference("RequirePaymentType") ) {
98 Koha::Exceptions::Account::InvalidPaymentType->throw( error => 'Invalid payment type' );
101 my $manager_id = $userenv ? $userenv->{number} : undef;
102 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
103 my $payment = $self->payin_amount(
105 interface => $interface,
108 payment_type => $payment_type,
109 cash_register => $cash_register,
110 user_id => $manager_id,
111 library_id => $library_id,
113 description => $description,
119 # NOTE: Pay historically always applied as much credit as it could to all
120 # existing outstanding debits, whether passed specific debits or otherwise.
121 if ( $payment->amountoutstanding ) {
124 { debits => [ $self->outstanding_debits->as_list ] } );
127 my $patron = Koha::Patrons->find( $self->{patron_id} );
128 my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
129 if ( C4::Context->preference('UseEmailReceipts') ) {
131 my $letter = C4::Letters::GetPreparedLetter(
132 module => 'circulation',
133 letter_code => uc("ACCOUNT_$type"),
134 message_transport_type => 'email',
135 lang => $patron->lang,
137 borrowers => $self->{patron_id},
138 branches => $library_id,
142 offsets => \@account_offsets,
147 C4::Letters::EnqueueLetter(
150 borrowernumber => $self->{patron_id},
151 message_transport_type => 'email',
153 ) or warn "can't enqueue letter $letter";
157 my $renew_outcomes = [];
158 for my $message ( @{$payment->object_messages} ) {
159 push @{$renew_outcomes}, $message->payload;
162 return { payment_id => $payment->id, renew_result => $renew_outcomes };
167 This method allows adding credits to a patron's account
169 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
172 description => $description,
173 interface => $interface,
174 issue_id => $checkout->id,
176 library_id => $library_id,
178 payment_type => $payment_type,
179 type => $credit_type,
184 $credit_type can be any of:
198 my ( $self, $params ) = @_;
200 # check for mandatory params
201 my @mandatory = ( 'interface', 'amount' );
202 for my $param (@mandatory) {
203 unless ( defined( $params->{$param} ) ) {
204 Koha::Exceptions::MissingParameter->throw(
205 error => "The $param parameter is mandatory" );
209 # amount should always be passed as a positive value
210 my $amount = $params->{amount} * -1;
211 unless ( $amount < 0 ) {
212 Koha::Exceptions::Account::AmountNotPositive->throw(
213 error => 'Debit amount passed is not positive' );
216 my $description = $params->{description} // q{};
217 my $note = $params->{note} // q{};
218 my $user_id = $params->{user_id};
219 my $interface = $params->{interface};
220 my $library_id = $params->{library_id};
221 my $cash_register = $params->{cash_register};
222 my $payment_type = $params->{payment_type};
223 my $credit_type = $params->{type} || 'PAYMENT';
224 my $item_id = $params->{item_id};
225 my $issue_id = $params->{issue_id};
227 Koha::Exceptions::Account::RegisterRequired->throw()
228 if ( C4::Context->preference("UseCashRegisters")
229 && defined($payment_type)
230 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
231 && !defined($cash_register) );
234 my $schema = Koha::Database->new->schema;
239 # Insert the account line
240 $line = Koha::Account::Line->new(
242 borrowernumber => $self->{patron_id},
245 description => $description,
246 credit_type_code => $credit_type,
247 amountoutstanding => $amount,
248 payment_type => $payment_type,
250 manager_id => $user_id,
251 interface => $interface,
252 branchcode => $library_id,
253 register_id => $cash_register,
254 itemnumber => $item_id,
255 issue_id => $issue_id,
259 # Record the account offset
260 my $account_offset = Koha::Account::Offset->new(
262 credit_id => $line->id,
264 amount => $amount * -1
268 C4::Stats::UpdateStats(
270 branch => $library_id,
271 type => lc($credit_type),
273 borrowernumber => $self->{patron_id},
274 interface => $interface,
276 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
279 'after_account_action',
281 action => "add_credit",
283 type => lc($credit_type),
284 line => $line->get_from_storage, #TODO Seems unneeded
289 if ( C4::Context->preference("FinesLog") ) {
295 action => "create_$credit_type",
296 borrowernumber => $self->{patron_id},
298 description => $description,
299 amountoutstanding => $amount,
300 credit_type_code => $credit_type,
302 itemnumber => $item_id,
303 manager_id => $user_id,
304 branchcode => $library_id,
314 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
315 if ( $_->broken_fk eq 'credit_type_code' ) {
316 Koha::Exceptions::Account::UnrecognisedType->throw(
317 error => 'Type of credit not recognised' );
328 my $credit = $account->payin_amount(
331 type => $credit_type,
332 payment_type => $payment_type,
333 cash_register => $register_id,
334 interface => $interface,
335 library_id => $branchcode,
336 user_id => $staff_id,
337 debits => $debit_lines,
338 description => $description,
343 This method allows an amount to be paid into a patrons account and immediately applied against debts.
345 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
347 $credit_type can be any of:
355 my ( $self, $params ) = @_;
357 # check for mandatory params
358 my @mandatory = ( 'interface', 'amount', 'type' );
359 for my $param (@mandatory) {
360 unless ( defined( $params->{$param} ) ) {
361 Koha::Exceptions::MissingParameter->throw(
362 error => "The $param parameter is mandatory" );
366 # Check for mandatory register
367 Koha::Exceptions::Account::RegisterRequired->throw()
368 if ( C4::Context->preference("UseCashRegisters")
369 && defined( $params->{payment_type} )
370 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
371 && !defined($params->{cash_register}) );
373 # amount should always be passed as a positive value
374 my $amount = $params->{amount};
375 unless ( $amount > 0 ) {
376 Koha::Exceptions::Account::AmountNotPositive->throw(
377 error => 'Payin amount passed is not positive' );
381 my $schema = Koha::Database->new->schema;
386 $credit = $self->add_credit($params);
388 # Offset debts passed first
389 if ( exists( $params->{debits} ) ) {
390 $credit = $credit->apply(
392 debits => $params->{debits}
397 # Offset against remaining balance if AutoReconcile
398 if ( C4::Context->preference("AccountAutoReconcile")
399 && $credit->amountoutstanding != 0 )
401 $credit = $credit->apply(
403 debits => [ $self->outstanding_debits->as_list ]
415 This method allows adding debits to a patron's account
417 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
420 description => $description,
423 interface => $interface,
424 library_id => $library_id,
426 transaction_type => $transaction_type,
427 cash_register => $register_id,
429 issue_id => $issue_id
433 $debit_type can be any of:
453 my ( $self, $params ) = @_;
455 # check for mandatory params
456 my @mandatory = ( 'interface', 'type', 'amount' );
457 for my $param (@mandatory) {
458 unless ( defined( $params->{$param} ) ) {
459 Koha::Exceptions::MissingParameter->throw(
460 error => "The $param parameter is mandatory" );
464 # check for cash register if using cash
465 Koha::Exceptions::Account::RegisterRequired->throw()
466 if ( C4::Context->preference("UseCashRegisters")
467 && defined( $params->{transaction_type} )
468 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
469 && !defined( $params->{cash_register} ) );
471 # amount should always be a positive value
472 my $amount = $params->{amount};
473 unless ( $amount > 0 ) {
474 Koha::Exceptions::Account::AmountNotPositive->throw(
475 error => 'Debit amount passed is not positive' );
478 my $description = $params->{description} // q{};
479 my $note = $params->{note} // q{};
480 my $user_id = $params->{user_id};
481 my $interface = $params->{interface};
482 my $library_id = $params->{library_id};
483 my $cash_register = $params->{cash_register};
484 my $debit_type = $params->{type};
485 my $transaction_type = $params->{transaction_type};
486 my $item_id = $params->{item_id};
487 my $issue_id = $params->{issue_id};
490 my $schema = Koha::Database->new->schema;
495 # Insert the account line
496 $line = Koha::Account::Line->new(
498 borrowernumber => $self->{patron_id},
501 description => $description,
502 debit_type_code => $debit_type,
503 amountoutstanding => $amount,
504 payment_type => $transaction_type,
506 manager_id => $user_id,
507 interface => $interface,
508 itemnumber => $item_id,
509 issue_id => $issue_id,
510 branchcode => $library_id,
511 register_id => $cash_register,
513 $debit_type eq 'OVERDUE'
514 ? ( status => 'UNRETURNED' )
520 # Record the account offset
521 my $account_offset = Koha::Account::Offset->new(
523 debit_id => $line->id,
529 if ( C4::Context->preference("FinesLog") ) {
535 action => "create_$debit_type",
536 borrowernumber => $self->{patron_id},
538 description => $description,
539 amountoutstanding => $amount,
540 debit_type_code => $debit_type,
542 itemnumber => $item_id,
543 manager_id => $user_id,
553 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
554 if ( $_->broken_fk eq 'debit_type_code' ) {
555 Koha::Exceptions::Account::UnrecognisedType->throw(
556 error => 'Type of debit not recognised' );
569 my $debit = $account->payout_amount(
571 payout_type => $payout_type,
572 register_id => $register_id,
573 staff_id => $staff_id,
574 interface => 'intranet',
576 credits => $credit_lines
580 This method allows an amount to be paid out from a patrons account against outstanding credits.
582 $payout_type can be any of the defined payment_types:
587 my ( $self, $params ) = @_;
589 # Check for mandatory parameters
591 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
592 for my $param (@mandatory) {
593 unless ( defined( $params->{$param} ) ) {
594 Koha::Exceptions::MissingParameter->throw(
595 error => "The $param parameter is mandatory" );
599 # Check for mandatory register
600 Koha::Exceptions::Account::RegisterRequired->throw()
601 if ( C4::Context->preference("UseCashRegisters")
602 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
603 && !defined($params->{cash_register}) );
605 # Amount should always be passed as a positive value
606 my $amount = $params->{amount};
607 unless ( $amount > 0 ) {
608 Koha::Exceptions::Account::AmountNotPositive->throw(
609 error => 'Payout amount passed is not positive' );
612 # Amount should always be less than or equal to outstanding credit
614 my $outstanding_credits =
615 exists( $params->{credits} )
617 : $self->outstanding_credits->as_list;
618 for my $credit ( @{$outstanding_credits} ) {
619 $outstanding += $credit->amountoutstanding;
621 $outstanding = $outstanding * -1;
622 Koha::Exceptions::ParameterTooHigh->throw( error =>
623 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
624 ) unless ( $outstanding >= $amount );
627 my $schema = Koha::Database->new->schema;
631 # A 'payout' is a 'debit'
632 $payout = $self->add_debit(
634 amount => $params->{amount},
636 transaction_type => $params->{payout_type},
637 amountoutstanding => $params->{amount},
638 user_id => $params->{staff_id},
639 interface => $params->{interface},
640 branchcode => $params->{branch},
641 cash_register => $params->{cash_register}
645 # Offset against credits
646 for my $credit ( @{$outstanding_credits} ) {
647 $credit->apply( { debits => [$payout] } );
648 $payout->discard_changes;
649 last if $payout->amountoutstanding == 0;
653 $payout->status('PAID')->store;
662 my $balance = $self->balance
664 Return the balance (sum of amountoutstanding columns)
670 return $self->lines->total_outstanding;
673 =head3 outstanding_debits
675 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
677 It returns the debit lines with outstanding amounts for the patron.
679 It returns a Koha::Account::Lines iterator.
683 sub outstanding_debits {
686 return $self->lines->search(
688 amount => { '>' => 0 },
689 amountoutstanding => { '>' => 0 }
694 =head3 outstanding_credits
696 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
698 It returns the credit lines with outstanding amounts for the patron.
700 It returns a Koha::Account::Lines iterator.
704 sub outstanding_credits {
707 return $self->lines->search(
709 amount => { '<' => 0 },
710 amountoutstanding => { '<' => 0 }
715 =head3 non_issues_charges
717 my $non_issues_charges = $self->non_issues_charges
719 Calculates amount immediately owing by the patron - non-issue charges.
721 Charges can be set as exempt from non-issue by editing the debit type in the Debit Types area of System Preferences.
725 sub non_issues_charges {
728 my @blocking_debit_types = Koha::Account::DebitTypes->search({ restricts_checkouts => 1 }, { columns => 'code' })->get_column('code');
730 return $self->lines->search(
732 debit_type_code => { -in => \@blocking_debit_types }
734 )->total_outstanding;
739 my $lines = $self->lines;
741 Return all credits and debits for the user, outstanding or otherwise
748 return Koha::Account::Lines->search(
750 borrowernumber => $self->{patron_id},
758 my $credits = $self->credits;
760 Return all credits for the user
767 return Koha::Account::Credits->search(
769 borrowernumber => $self->{patron_id}
776 my $debits = $self->debits;
778 Return all debits for the user
785 return Koha::Account::Debits->search(
787 borrowernumber => $self->{patron_id},
792 =head3 reconcile_balance
794 $account->reconcile_balance();
796 Find outstanding credits and use them to pay outstanding debits.
797 Currently, this implicitly uses the 'First In First Out' rule for
798 applying credits against debits.
802 sub reconcile_balance {
805 my $outstanding_debits = $self->outstanding_debits;
806 my $outstanding_credits = $self->outstanding_credits;
808 while ( $outstanding_debits->total_outstanding > 0
809 and my $credit = $outstanding_credits->next )
811 # there's both outstanding debits and credits
812 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
814 $outstanding_debits = $self->outstanding_debits;
827 Kyle M Hall <kyle.m.hall@gmail.com>
828 Tomás Cohen Arazi <tomascohen@gmail.com>
829 Martin Renvoize <martin.renvoize@ptfs-europe.com>