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>.
24 use List::MoreUtils qw( uniq );
27 use C4::Circulation qw( ReturnLostItem CanBookBeRenewed AddRenewal );
29 use C4::Log qw( logaction );
30 use C4::Stats qw( UpdateStats );
31 use C4::Overdues qw(GetFine);
34 use Koha::Account::Lines;
35 use Koha::Account::Offsets;
36 use Koha::Account::DebitTypes;
37 use Koha::DateUtils qw( dt_from_string );
39 use Koha::Exceptions::Account;
43 Koha::Accounts - Module for managing payments and fees for patrons
48 my ( $class, $params ) = @_;
50 Carp::croak("No patron id passed in!") unless $params->{patron_id};
52 return bless( $params, $class );
57 This method allows payments to be made against fees/fines
59 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
63 description => $description,
64 library_id => $branchcode,
65 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
66 credit_type => $type, # credit_type_code code
67 offset_type => $offset_type, # offset type 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 my $manager_id = $userenv ? $userenv->{number} : undef;
90 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
91 my $payment = $self->payin_amount(
93 interface => $interface,
96 payment_type => $payment_type,
97 cash_register => $cash_register,
98 user_id => $manager_id,
99 library_id => $library_id,
101 description => $description,
107 # NOTE: Pay historically always applied as much credit as it could to all
108 # existing outstanding debits, whether passed specific debits or otherwise.
109 if ( $payment->amountoutstanding ) {
112 { debits => [ $self->outstanding_debits->as_list ] } );
115 my $patron = Koha::Patrons->find( $self->{patron_id} );
116 my @account_offsets = $payment->debit_offsets;
117 if ( C4::Context->preference('UseEmailReceipts') ) {
119 my $letter = C4::Letters::GetPreparedLetter(
120 module => 'circulation',
121 letter_code => uc("ACCOUNT_$type"),
122 message_transport_type => 'email',
123 lang => $patron->lang,
125 borrowers => $self->{patron_id},
126 branches => $library_id,
130 offsets => \@account_offsets,
135 C4::Letters::EnqueueLetter(
138 borrowernumber => $self->{patron_id},
139 message_transport_type => 'email',
141 ) or warn "can't enqueue letter $letter";
145 my $renew_outcomes = [];
146 for my $message ( @{$payment->messages} ) {
147 push @{$renew_outcomes}, $message->payload;
150 return { payment_id => $payment->id, renew_result => $renew_outcomes };
155 This method allows adding credits to a patron's account
157 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
160 description => $description,
163 interface => $interface,
164 library_id => $library_id,
165 payment_type => $payment_type,
166 type => $credit_type,
171 $credit_type can be any of:
184 my ( $self, $params ) = @_;
186 # check for mandatory params
187 my @mandatory = ( 'interface', 'amount' );
188 for my $param (@mandatory) {
189 unless ( defined( $params->{$param} ) ) {
190 Koha::Exceptions::MissingParameter->throw(
191 error => "The $param parameter is mandatory" );
195 # amount should always be passed as a positive value
196 my $amount = $params->{amount} * -1;
197 unless ( $amount < 0 ) {
198 Koha::Exceptions::Account::AmountNotPositive->throw(
199 error => 'Debit amount passed is not positive' );
202 my $description = $params->{description} // q{};
203 my $note = $params->{note} // q{};
204 my $user_id = $params->{user_id};
205 my $interface = $params->{interface};
206 my $library_id = $params->{library_id};
207 my $cash_register = $params->{cash_register};
208 my $payment_type = $params->{payment_type};
209 my $credit_type = $params->{type} || 'PAYMENT';
210 my $item_id = $params->{item_id};
212 Koha::Exceptions::Account::RegisterRequired->throw()
213 if ( C4::Context->preference("UseCashRegisters")
214 && defined($payment_type)
215 && ( $payment_type eq 'CASH' )
216 && !defined($cash_register) );
219 my $schema = Koha::Database->new->schema;
224 # Insert the account line
225 $line = Koha::Account::Line->new(
227 borrowernumber => $self->{patron_id},
230 description => $description,
231 credit_type_code => $credit_type,
232 amountoutstanding => $amount,
233 payment_type => $payment_type,
235 manager_id => $user_id,
236 interface => $interface,
237 branchcode => $library_id,
238 register_id => $cash_register,
239 itemnumber => $item_id,
243 # Record the account offset
244 my $account_offset = Koha::Account::Offset->new(
246 credit_id => $line->id,
247 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
252 C4::Stats::UpdateStats(
254 branch => $library_id,
255 type => lc($credit_type),
257 borrowernumber => $self->{patron_id},
259 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
261 if ( C4::Context->preference("FinesLog") ) {
267 action => "create_$credit_type",
268 borrowernumber => $self->{patron_id},
270 description => $description,
271 amountoutstanding => $amount,
272 credit_type_code => $credit_type,
274 itemnumber => $item_id,
275 manager_id => $user_id,
276 branchcode => $library_id,
286 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
287 if ( $_->broken_fk eq 'credit_type_code' ) {
288 Koha::Exceptions::Account::UnrecognisedType->throw(
289 error => 'Type of credit not recognised' );
302 my $credit = $account->payin_amount(
305 type => $credit_type,
306 payment_type => $payment_type,
307 cash_register => $register_id,
308 interface => $interface,
309 library_id => $branchcode,
310 user_id => $staff_id,
311 debits => $debit_lines,
312 description => $description,
317 This method allows an amount to be paid into a patrons account and immediately applied against debts.
319 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
321 $credit_type can be any of:
329 my ( $self, $params ) = @_;
331 # check for mandatory params
332 my @mandatory = ( 'interface', 'amount', 'type' );
333 for my $param (@mandatory) {
334 unless ( defined( $params->{$param} ) ) {
335 Koha::Exceptions::MissingParameter->throw(
336 error => "The $param parameter is mandatory" );
340 # Check for mandatory register
341 Koha::Exceptions::Account::RegisterRequired->throw()
342 if ( C4::Context->preference("UseCashRegisters")
343 && defined( $params->{payment_type} )
344 && ( $params->{payment_type} eq 'CASH' )
345 && !defined($params->{cash_register}) );
347 # amount should always be passed as a positive value
348 my $amount = $params->{amount};
349 unless ( $amount > 0 ) {
350 Koha::Exceptions::Account::AmountNotPositive->throw(
351 error => 'Payin amount passed is not positive' );
355 my $schema = Koha::Database->new->schema;
360 $credit = $self->add_credit($params);
362 # Offset debts passed first
363 if ( exists( $params->{debits} ) ) {
364 $credit = $credit->apply(
366 debits => $params->{debits},
367 offset_type => $Koha::Account::offset_type->{$params->{type}}
372 # Offset against remaining balance if AutoReconcile
373 if ( C4::Context->preference("AccountAutoReconcile")
374 && $credit->amountoutstanding != 0 )
376 $credit = $credit->apply(
378 debits => [ $self->outstanding_debits->as_list ],
379 offset_type => $Koha::Account::offset_type->{$params->{type}}
391 This method allows adding debits to a patron's account
393 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
396 description => $description,
399 interface => $interface,
400 library_id => $library_id,
402 transaction_type => $transaction_type,
403 cash_register => $register_id,
405 issue_id => $issue_id
409 $debit_type can be any of:
429 my ( $self, $params ) = @_;
431 # check for mandatory params
432 my @mandatory = ( 'interface', 'type', 'amount' );
433 for my $param (@mandatory) {
434 unless ( defined( $params->{$param} ) ) {
435 Koha::Exceptions::MissingParameter->throw(
436 error => "The $param parameter is mandatory" );
440 # check for cash register if using cash
441 Koha::Exceptions::Account::RegisterRequired->throw()
442 if ( C4::Context->preference("UseCashRegisters")
443 && defined( $params->{transaction_type} )
444 && ( $params->{transaction_type} eq 'CASH' )
445 && !defined( $params->{cash_register} ) );
447 # amount should always be a positive value
448 my $amount = $params->{amount};
449 unless ( $amount > 0 ) {
450 Koha::Exceptions::Account::AmountNotPositive->throw(
451 error => 'Debit amount passed is not positive' );
454 my $description = $params->{description} // q{};
455 my $note = $params->{note} // q{};
456 my $user_id = $params->{user_id};
457 my $interface = $params->{interface};
458 my $library_id = $params->{library_id};
459 my $cash_register = $params->{cash_register};
460 my $debit_type = $params->{type};
461 my $transaction_type = $params->{transaction_type};
462 my $item_id = $params->{item_id};
463 my $issue_id = $params->{issue_id};
464 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
467 my $schema = Koha::Database->new->schema;
472 # Insert the account line
473 $line = Koha::Account::Line->new(
475 borrowernumber => $self->{patron_id},
478 description => $description,
479 debit_type_code => $debit_type,
480 amountoutstanding => $amount,
481 payment_type => $transaction_type,
483 manager_id => $user_id,
484 interface => $interface,
485 itemnumber => $item_id,
486 issue_id => $issue_id,
487 branchcode => $library_id,
488 register_id => $cash_register,
490 $debit_type eq 'OVERDUE'
491 ? ( status => 'UNRETURNED' )
497 # Record the account offset
498 my $account_offset = Koha::Account::Offset->new(
500 debit_id => $line->id,
501 type => $offset_type,
506 if ( C4::Context->preference("FinesLog") ) {
512 action => "create_$debit_type",
513 borrowernumber => $self->{patron_id},
515 description => $description,
516 amountoutstanding => $amount,
517 debit_type_code => $debit_type,
519 itemnumber => $item_id,
520 manager_id => $user_id,
530 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
531 if ( $_->broken_fk eq 'debit_type_code' ) {
532 Koha::Exceptions::Account::UnrecognisedType->throw(
533 error => 'Type of debit not recognised' );
546 my $debit = $account->payout_amount(
548 payout_type => $payout_type,
549 register_id => $register_id,
550 staff_id => $staff_id,
551 interface => 'intranet',
553 credits => $credit_lines
557 This method allows an amount to be paid out from a patrons account against outstanding credits.
559 $payout_type can be any of the defined payment_types:
564 my ( $self, $params ) = @_;
566 # Check for mandatory parameters
568 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
569 for my $param (@mandatory) {
570 unless ( defined( $params->{$param} ) ) {
571 Koha::Exceptions::MissingParameter->throw(
572 error => "The $param parameter is mandatory" );
576 # Check for mandatory register
577 Koha::Exceptions::Account::RegisterRequired->throw()
578 if ( C4::Context->preference("UseCashRegisters")
579 && ( $params->{payout_type} eq 'CASH' )
580 && !defined($params->{cash_register}) );
582 # Amount should always be passed as a positive value
583 my $amount = $params->{amount};
584 unless ( $amount > 0 ) {
585 Koha::Exceptions::Account::AmountNotPositive->throw(
586 error => 'Payout amount passed is not positive' );
589 # Amount should always be less than or equal to outstanding credit
591 my $outstanding_credits =
592 exists( $params->{credits} )
594 : $self->outstanding_credits->as_list;
595 for my $credit ( @{$outstanding_credits} ) {
596 $outstanding += $credit->amountoutstanding;
598 $outstanding = $outstanding * -1;
599 Koha::Exceptions::ParameterTooHigh->throw( error =>
600 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
601 ) unless ( $outstanding >= $amount );
604 my $schema = Koha::Database->new->schema;
608 # A 'payout' is a 'debit'
609 $payout = $self->add_debit(
611 amount => $params->{amount},
613 transaction_type => $params->{payout_type},
614 amountoutstanding => $params->{amount},
615 manager_id => $params->{staff_id},
616 interface => $params->{interface},
617 branchcode => $params->{branch},
618 cash_register => $params->{cash_register}
622 # Offset against credits
623 for my $credit ( @{$outstanding_credits} ) {
625 { debits => [$payout], offset_type => 'PAYOUT' } );
626 $payout->discard_changes;
627 last if $payout->amountoutstanding == 0;
631 $payout->status('PAID')->store;
640 my $balance = $self->balance
642 Return the balance (sum of amountoutstanding columns)
648 return $self->lines->total_outstanding;
651 =head3 outstanding_debits
653 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
655 It returns the debit lines with outstanding amounts for the patron.
657 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
658 return a list of Koha::Account::Line objects.
662 sub outstanding_debits {
665 return $self->lines->search(
667 amount => { '>' => 0 },
668 amountoutstanding => { '>' => 0 }
673 =head3 outstanding_credits
675 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
677 It returns the credit lines with outstanding amounts for the patron.
679 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
680 return a list of Koha::Account::Line objects.
684 sub outstanding_credits {
687 return $self->lines->search(
689 amount => { '<' => 0 },
690 amountoutstanding => { '<' => 0 }
695 =head3 non_issues_charges
697 my $non_issues_charges = $self->non_issues_charges
699 Calculates amount immediately owing by the patron - non-issue charges.
701 Charges exempt from non-issue are:
702 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
703 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
704 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
708 sub non_issues_charges {
711 #NOTE: With bug 23049 these preferences could be moved to being attached
712 #to individual debit types to give more flexability and specificity.
714 push @not_fines, 'RESERVE'
715 unless C4::Context->preference('HoldsInNoissuesCharge');
716 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
717 unless C4::Context->preference('RentalsInNoissuesCharge');
718 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
719 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
720 push @not_fines, @man_inv;
723 return $self->lines->search(
725 debit_type_code => { -not_in => \@not_fines }
727 )->total_outstanding;
732 my $lines = $self->lines;
734 Return all credits and debits for the user, outstanding or otherwise
741 return Koha::Account::Lines->search(
743 borrowernumber => $self->{patron_id},
748 =head3 reconcile_balance
750 $account->reconcile_balance();
752 Find outstanding credits and use them to pay outstanding debits.
753 Currently, this implicitly uses the 'First In First Out' rule for
754 applying credits against debits.
758 sub reconcile_balance {
761 my $outstanding_debits = $self->outstanding_debits;
762 my $outstanding_credits = $self->outstanding_credits;
764 while ( $outstanding_debits->total_outstanding > 0
765 and my $credit = $outstanding_credits->next )
767 # there's both outstanding debits and credits
768 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
770 $outstanding_debits = $self->outstanding_debits;
786 'CREDIT' => 'Manual Credit',
787 'FORGIVEN' => 'Writeoff',
788 'LOST_FOUND' => 'Lost Item Found',
789 'OVERPAYMENT' => 'Overpayment',
790 'PAYMENT' => 'Payment',
791 'WRITEOFF' => 'Writeoff',
792 'ACCOUNT' => 'Account Fee',
793 'ACCOUNT_RENEW' => 'Account Fee',
794 'RESERVE' => 'Reserve Fee',
795 'PROCESSING' => 'Processing Fee',
796 'LOST' => 'Lost Item',
797 'RENT' => 'Rental Fee',
798 'RENT_DAILY' => 'Rental Fee',
799 'RENT_RENEW' => 'Rental Fee',
800 'RENT_DAILY_RENEW' => 'Rental Fee',
801 'OVERDUE' => 'OVERDUE',
802 'RESERVE_EXPIRED' => 'Hold Expired',
803 'PAYOUT' => 'PAYOUT',
810 Kyle M Hall <kyle.m.hall@gmail.com>
811 Tomás Cohen Arazi <tomascohen@gmail.com>
812 Martin Renvoize <martin.renvoize@ptfs-europe.com>