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);
33 use Koha::Account::Lines;
34 use Koha::Account::Offsets;
35 use Koha::Account::DebitTypes;
37 use Koha::Exceptions::Account;
41 Koha::Accounts - Module for managing payments and fees for patrons
46 my ( $class, $params ) = @_;
48 Carp::croak("No patron id passed in!") unless $params->{patron_id};
50 return bless( $params, $class );
55 This method allows payments to be made against fees/fines
57 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
61 description => $description,
62 library_id => $branchcode,
63 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
64 credit_type => $type, # credit_type_code code
65 item_id => $itemnumber, # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
72 my ( $self, $params ) = @_;
74 my $amount = $params->{amount};
75 my $description = $params->{description};
76 my $note = $params->{note} || q{};
77 my $library_id = $params->{library_id};
78 my $lines = $params->{lines};
79 my $type = $params->{type} || 'PAYMENT';
80 my $payment_type = $params->{payment_type} || undef;
81 my $cash_register = $params->{cash_register};
82 my $item_id = $params->{item_id};
84 my $userenv = C4::Context->userenv;
86 my $manager_id = $userenv ? $userenv->{number} : undef;
87 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
88 my $payment = $self->payin_amount(
90 interface => $interface,
93 payment_type => $payment_type,
94 cash_register => $cash_register,
95 user_id => $manager_id,
96 library_id => $library_id,
98 description => $description,
104 # NOTE: Pay historically always applied as much credit as it could to all
105 # existing outstanding debits, whether passed specific debits or otherwise.
106 if ( $payment->amountoutstanding ) {
109 { debits => [ $self->outstanding_debits->as_list ] } );
112 my $patron = Koha::Patrons->find( $self->{patron_id} );
113 my @account_offsets = $payment->debit_offsets;
114 if ( C4::Context->preference('UseEmailReceipts') ) {
116 my $letter = C4::Letters::GetPreparedLetter(
117 module => 'circulation',
118 letter_code => uc("ACCOUNT_$type"),
119 message_transport_type => 'email',
120 lang => $patron->lang,
122 borrowers => $self->{patron_id},
123 branches => $library_id,
127 offsets => \@account_offsets,
132 C4::Letters::EnqueueLetter(
135 borrowernumber => $self->{patron_id},
136 message_transport_type => 'email',
138 ) or warn "can't enqueue letter $letter";
142 my $renew_outcomes = [];
143 for my $message ( @{$payment->messages} ) {
144 push @{$renew_outcomes}, $message->payload;
147 return { payment_id => $payment->id, renew_result => $renew_outcomes };
152 This method allows adding credits to a patron's account
154 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
157 description => $description,
160 interface => $interface,
161 library_id => $library_id,
162 payment_type => $payment_type,
163 type => $credit_type,
168 $credit_type can be any of:
181 my ( $self, $params ) = @_;
183 # check for mandatory params
184 my @mandatory = ( 'interface', 'amount' );
185 for my $param (@mandatory) {
186 unless ( defined( $params->{$param} ) ) {
187 Koha::Exceptions::MissingParameter->throw(
188 error => "The $param parameter is mandatory" );
192 # amount should always be passed as a positive value
193 my $amount = $params->{amount} * -1;
194 unless ( $amount < 0 ) {
195 Koha::Exceptions::Account::AmountNotPositive->throw(
196 error => 'Debit amount passed is not positive' );
199 my $description = $params->{description} // q{};
200 my $note = $params->{note} // q{};
201 my $user_id = $params->{user_id};
202 my $interface = $params->{interface};
203 my $library_id = $params->{library_id};
204 my $cash_register = $params->{cash_register};
205 my $payment_type = $params->{payment_type};
206 my $credit_type = $params->{type} || 'PAYMENT';
207 my $item_id = $params->{item_id};
209 Koha::Exceptions::Account::RegisterRequired->throw()
210 if ( C4::Context->preference("UseCashRegisters")
211 && defined($payment_type)
212 && ( $payment_type eq 'CASH' )
213 && !defined($cash_register) );
216 my $schema = Koha::Database->new->schema;
221 # Insert the account line
222 $line = Koha::Account::Line->new(
224 borrowernumber => $self->{patron_id},
227 description => $description,
228 credit_type_code => $credit_type,
229 amountoutstanding => $amount,
230 payment_type => $payment_type,
232 manager_id => $user_id,
233 interface => $interface,
234 branchcode => $library_id,
235 register_id => $cash_register,
236 itemnumber => $item_id,
240 # Record the account offset
241 my $account_offset = Koha::Account::Offset->new(
243 credit_id => $line->id,
249 C4::Stats::UpdateStats(
251 branch => $library_id,
252 type => lc($credit_type),
254 borrowernumber => $self->{patron_id},
256 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
258 if ( C4::Context->preference("FinesLog") ) {
264 action => "create_$credit_type",
265 borrowernumber => $self->{patron_id},
267 description => $description,
268 amountoutstanding => $amount,
269 credit_type_code => $credit_type,
271 itemnumber => $item_id,
272 manager_id => $user_id,
273 branchcode => $library_id,
283 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
284 if ( $_->broken_fk eq 'credit_type_code' ) {
285 Koha::Exceptions::Account::UnrecognisedType->throw(
286 error => 'Type of credit not recognised' );
299 my $credit = $account->payin_amount(
302 type => $credit_type,
303 payment_type => $payment_type,
304 cash_register => $register_id,
305 interface => $interface,
306 library_id => $branchcode,
307 user_id => $staff_id,
308 debits => $debit_lines,
309 description => $description,
314 This method allows an amount to be paid into a patrons account and immediately applied against debts.
316 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
318 $credit_type can be any of:
326 my ( $self, $params ) = @_;
328 # check for mandatory params
329 my @mandatory = ( 'interface', 'amount', 'type' );
330 for my $param (@mandatory) {
331 unless ( defined( $params->{$param} ) ) {
332 Koha::Exceptions::MissingParameter->throw(
333 error => "The $param parameter is mandatory" );
337 # Check for mandatory register
338 Koha::Exceptions::Account::RegisterRequired->throw()
339 if ( C4::Context->preference("UseCashRegisters")
340 && defined( $params->{payment_type} )
341 && ( $params->{payment_type} eq 'CASH' )
342 && !defined($params->{cash_register}) );
344 # amount should always be passed as a positive value
345 my $amount = $params->{amount};
346 unless ( $amount > 0 ) {
347 Koha::Exceptions::Account::AmountNotPositive->throw(
348 error => 'Payin amount passed is not positive' );
352 my $schema = Koha::Database->new->schema;
357 $credit = $self->add_credit($params);
359 # Offset debts passed first
360 if ( exists( $params->{debits} ) ) {
361 $credit = $credit->apply(
363 debits => $params->{debits},
364 offset_type => $Koha::Account::offset_type->{$params->{type}}
369 # Offset against remaining balance if AutoReconcile
370 if ( C4::Context->preference("AccountAutoReconcile")
371 && $credit->amountoutstanding != 0 )
373 $credit = $credit->apply(
375 debits => [ $self->outstanding_debits->as_list ],
376 offset_type => $Koha::Account::offset_type->{$params->{type}}
388 This method allows adding debits to a patron's account
390 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
393 description => $description,
396 interface => $interface,
397 library_id => $library_id,
399 transaction_type => $transaction_type,
400 cash_register => $register_id,
402 issue_id => $issue_id
406 $debit_type can be any of:
426 my ( $self, $params ) = @_;
428 # check for mandatory params
429 my @mandatory = ( 'interface', 'type', 'amount' );
430 for my $param (@mandatory) {
431 unless ( defined( $params->{$param} ) ) {
432 Koha::Exceptions::MissingParameter->throw(
433 error => "The $param parameter is mandatory" );
437 # check for cash register if using cash
438 Koha::Exceptions::Account::RegisterRequired->throw()
439 if ( C4::Context->preference("UseCashRegisters")
440 && defined( $params->{transaction_type} )
441 && ( $params->{transaction_type} eq 'CASH' )
442 && !defined( $params->{cash_register} ) );
444 # amount should always be a positive value
445 my $amount = $params->{amount};
446 unless ( $amount > 0 ) {
447 Koha::Exceptions::Account::AmountNotPositive->throw(
448 error => 'Debit amount passed is not positive' );
451 my $description = $params->{description} // q{};
452 my $note = $params->{note} // q{};
453 my $user_id = $params->{user_id};
454 my $interface = $params->{interface};
455 my $library_id = $params->{library_id};
456 my $cash_register = $params->{cash_register};
457 my $debit_type = $params->{type};
458 my $transaction_type = $params->{transaction_type};
459 my $item_id = $params->{item_id};
460 my $issue_id = $params->{issue_id};
463 my $schema = Koha::Database->new->schema;
468 # Insert the account line
469 $line = Koha::Account::Line->new(
471 borrowernumber => $self->{patron_id},
474 description => $description,
475 debit_type_code => $debit_type,
476 amountoutstanding => $amount,
477 payment_type => $transaction_type,
479 manager_id => $user_id,
480 interface => $interface,
481 itemnumber => $item_id,
482 issue_id => $issue_id,
483 branchcode => $library_id,
484 register_id => $cash_register,
486 $debit_type eq 'OVERDUE'
487 ? ( status => 'UNRETURNED' )
493 # Record the account offset
494 my $account_offset = Koha::Account::Offset->new(
496 debit_id => $line->id,
502 if ( C4::Context->preference("FinesLog") ) {
508 action => "create_$debit_type",
509 borrowernumber => $self->{patron_id},
511 description => $description,
512 amountoutstanding => $amount,
513 debit_type_code => $debit_type,
515 itemnumber => $item_id,
516 manager_id => $user_id,
526 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
527 if ( $_->broken_fk eq 'debit_type_code' ) {
528 Koha::Exceptions::Account::UnrecognisedType->throw(
529 error => 'Type of debit not recognised' );
542 my $debit = $account->payout_amount(
544 payout_type => $payout_type,
545 register_id => $register_id,
546 staff_id => $staff_id,
547 interface => 'intranet',
549 credits => $credit_lines
553 This method allows an amount to be paid out from a patrons account against outstanding credits.
555 $payout_type can be any of the defined payment_types:
560 my ( $self, $params ) = @_;
562 # Check for mandatory parameters
564 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
565 for my $param (@mandatory) {
566 unless ( defined( $params->{$param} ) ) {
567 Koha::Exceptions::MissingParameter->throw(
568 error => "The $param parameter is mandatory" );
572 # Check for mandatory register
573 Koha::Exceptions::Account::RegisterRequired->throw()
574 if ( C4::Context->preference("UseCashRegisters")
575 && ( $params->{payout_type} eq 'CASH' )
576 && !defined($params->{cash_register}) );
578 # Amount should always be passed as a positive value
579 my $amount = $params->{amount};
580 unless ( $amount > 0 ) {
581 Koha::Exceptions::Account::AmountNotPositive->throw(
582 error => 'Payout amount passed is not positive' );
585 # Amount should always be less than or equal to outstanding credit
587 my $outstanding_credits =
588 exists( $params->{credits} )
590 : $self->outstanding_credits->as_list;
591 for my $credit ( @{$outstanding_credits} ) {
592 $outstanding += $credit->amountoutstanding;
594 $outstanding = $outstanding * -1;
595 Koha::Exceptions::ParameterTooHigh->throw( error =>
596 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
597 ) unless ( $outstanding >= $amount );
600 my $schema = Koha::Database->new->schema;
604 # A 'payout' is a 'debit'
605 $payout = $self->add_debit(
607 amount => $params->{amount},
609 transaction_type => $params->{payout_type},
610 amountoutstanding => $params->{amount},
611 manager_id => $params->{staff_id},
612 interface => $params->{interface},
613 branchcode => $params->{branch},
614 cash_register => $params->{cash_register}
618 # Offset against credits
619 for my $credit ( @{$outstanding_credits} ) {
621 { debits => [$payout], offset_type => 'PAYOUT' } );
622 $payout->discard_changes;
623 last if $payout->amountoutstanding == 0;
627 $payout->status('PAID')->store;
636 my $balance = $self->balance
638 Return the balance (sum of amountoutstanding columns)
644 return $self->lines->total_outstanding;
647 =head3 outstanding_debits
649 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
651 It returns the debit lines with outstanding amounts for the patron.
653 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
654 return a list of Koha::Account::Line objects.
658 sub outstanding_debits {
661 return $self->lines->search(
663 amount => { '>' => 0 },
664 amountoutstanding => { '>' => 0 }
669 =head3 outstanding_credits
671 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
673 It returns the credit lines with outstanding amounts for the patron.
675 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
676 return a list of Koha::Account::Line objects.
680 sub outstanding_credits {
683 return $self->lines->search(
685 amount => { '<' => 0 },
686 amountoutstanding => { '<' => 0 }
691 =head3 non_issues_charges
693 my $non_issues_charges = $self->non_issues_charges
695 Calculates amount immediately owing by the patron - non-issue charges.
697 Charges exempt from non-issue are:
698 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
699 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
700 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
704 sub non_issues_charges {
707 #NOTE: With bug 23049 these preferences could be moved to being attached
708 #to individual debit types to give more flexability and specificity.
710 push @not_fines, 'RESERVE'
711 unless C4::Context->preference('HoldsInNoissuesCharge');
712 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
713 unless C4::Context->preference('RentalsInNoissuesCharge');
714 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
715 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
716 push @not_fines, @man_inv;
719 return $self->lines->search(
721 debit_type_code => { -not_in => \@not_fines }
723 )->total_outstanding;
728 my $lines = $self->lines;
730 Return all credits and debits for the user, outstanding or otherwise
737 return Koha::Account::Lines->search(
739 borrowernumber => $self->{patron_id},
744 =head3 reconcile_balance
746 $account->reconcile_balance();
748 Find outstanding credits and use them to pay outstanding debits.
749 Currently, this implicitly uses the 'First In First Out' rule for
750 applying credits against debits.
754 sub reconcile_balance {
757 my $outstanding_debits = $self->outstanding_debits;
758 my $outstanding_credits = $self->outstanding_credits;
760 while ( $outstanding_debits->total_outstanding > 0
761 and my $credit = $outstanding_credits->next )
763 # there's both outstanding debits and credits
764 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
766 $outstanding_debits = $self->outstanding_debits;
779 Kyle M Hall <kyle.m.hall@gmail.com>
780 Tomás Cohen Arazi <tomascohen@gmail.com>
781 Martin Renvoize <martin.renvoize@ptfs-europe.com>