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 offset_type => $offset_type, # offset type code
66 item_id => $itemnumber, # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
73 my ( $self, $params ) = @_;
75 my $amount = $params->{amount};
76 my $description = $params->{description};
77 my $note = $params->{note} || q{};
78 my $library_id = $params->{library_id};
79 my $lines = $params->{lines};
80 my $type = $params->{type} || 'PAYMENT';
81 my $payment_type = $params->{payment_type} || undef;
82 my $cash_register = $params->{cash_register};
83 my $item_id = $params->{item_id};
85 my $userenv = C4::Context->userenv;
87 my $manager_id = $userenv ? $userenv->{number} : undef;
88 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
89 my $payment = $self->payin_amount(
91 interface => $interface,
94 payment_type => $payment_type,
95 cash_register => $cash_register,
96 user_id => $manager_id,
97 library_id => $library_id,
99 description => $description,
105 # NOTE: Pay historically always applied as much credit as it could to all
106 # existing outstanding debits, whether passed specific debits or otherwise.
107 if ( $payment->amountoutstanding ) {
110 { debits => [ $self->outstanding_debits->as_list ] } );
113 my $patron = Koha::Patrons->find( $self->{patron_id} );
114 my @account_offsets = $payment->debit_offsets;
115 if ( C4::Context->preference('UseEmailReceipts') ) {
117 my $letter = C4::Letters::GetPreparedLetter(
118 module => 'circulation',
119 letter_code => uc("ACCOUNT_$type"),
120 message_transport_type => 'email',
121 lang => $patron->lang,
123 borrowers => $self->{patron_id},
124 branches => $library_id,
128 offsets => \@account_offsets,
133 C4::Letters::EnqueueLetter(
136 borrowernumber => $self->{patron_id},
137 message_transport_type => 'email',
139 ) or warn "can't enqueue letter $letter";
143 my $renew_outcomes = [];
144 for my $message ( @{$payment->messages} ) {
145 push @{$renew_outcomes}, $message->payload;
148 return { payment_id => $payment->id, renew_result => $renew_outcomes };
153 This method allows adding credits to a patron's account
155 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
158 description => $description,
161 interface => $interface,
162 library_id => $library_id,
163 payment_type => $payment_type,
164 type => $credit_type,
169 $credit_type can be any of:
182 my ( $self, $params ) = @_;
184 # check for mandatory params
185 my @mandatory = ( 'interface', 'amount' );
186 for my $param (@mandatory) {
187 unless ( defined( $params->{$param} ) ) {
188 Koha::Exceptions::MissingParameter->throw(
189 error => "The $param parameter is mandatory" );
193 # amount should always be passed as a positive value
194 my $amount = $params->{amount} * -1;
195 unless ( $amount < 0 ) {
196 Koha::Exceptions::Account::AmountNotPositive->throw(
197 error => 'Debit amount passed is not positive' );
200 my $description = $params->{description} // q{};
201 my $note = $params->{note} // q{};
202 my $user_id = $params->{user_id};
203 my $interface = $params->{interface};
204 my $library_id = $params->{library_id};
205 my $cash_register = $params->{cash_register};
206 my $payment_type = $params->{payment_type};
207 my $credit_type = $params->{type} || 'PAYMENT';
208 my $item_id = $params->{item_id};
210 Koha::Exceptions::Account::RegisterRequired->throw()
211 if ( C4::Context->preference("UseCashRegisters")
212 && defined($payment_type)
213 && ( $payment_type eq 'CASH' )
214 && !defined($cash_register) );
217 my $schema = Koha::Database->new->schema;
222 # Insert the account line
223 $line = Koha::Account::Line->new(
225 borrowernumber => $self->{patron_id},
228 description => $description,
229 credit_type_code => $credit_type,
230 amountoutstanding => $amount,
231 payment_type => $payment_type,
233 manager_id => $user_id,
234 interface => $interface,
235 branchcode => $library_id,
236 register_id => $cash_register,
237 itemnumber => $item_id,
241 # Record the account offset
242 my $account_offset = Koha::Account::Offset->new(
244 credit_id => $line->id,
245 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
250 C4::Stats::UpdateStats(
252 branch => $library_id,
253 type => lc($credit_type),
255 borrowernumber => $self->{patron_id},
257 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
259 if ( C4::Context->preference("FinesLog") ) {
265 action => "create_$credit_type",
266 borrowernumber => $self->{patron_id},
268 description => $description,
269 amountoutstanding => $amount,
270 credit_type_code => $credit_type,
272 itemnumber => $item_id,
273 manager_id => $user_id,
274 branchcode => $library_id,
284 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
285 if ( $_->broken_fk eq 'credit_type_code' ) {
286 Koha::Exceptions::Account::UnrecognisedType->throw(
287 error => 'Type of credit not recognised' );
300 my $credit = $account->payin_amount(
303 type => $credit_type,
304 payment_type => $payment_type,
305 cash_register => $register_id,
306 interface => $interface,
307 library_id => $branchcode,
308 user_id => $staff_id,
309 debits => $debit_lines,
310 description => $description,
315 This method allows an amount to be paid into a patrons account and immediately applied against debts.
317 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
319 $credit_type can be any of:
327 my ( $self, $params ) = @_;
329 # check for mandatory params
330 my @mandatory = ( 'interface', 'amount', 'type' );
331 for my $param (@mandatory) {
332 unless ( defined( $params->{$param} ) ) {
333 Koha::Exceptions::MissingParameter->throw(
334 error => "The $param parameter is mandatory" );
338 # Check for mandatory register
339 Koha::Exceptions::Account::RegisterRequired->throw()
340 if ( C4::Context->preference("UseCashRegisters")
341 && defined( $params->{payment_type} )
342 && ( $params->{payment_type} eq 'CASH' )
343 && !defined($params->{cash_register}) );
345 # amount should always be passed as a positive value
346 my $amount = $params->{amount};
347 unless ( $amount > 0 ) {
348 Koha::Exceptions::Account::AmountNotPositive->throw(
349 error => 'Payin amount passed is not positive' );
353 my $schema = Koha::Database->new->schema;
358 $credit = $self->add_credit($params);
360 # Offset debts passed first
361 if ( exists( $params->{debits} ) ) {
362 $credit = $credit->apply(
364 debits => $params->{debits},
365 offset_type => $Koha::Account::offset_type->{$params->{type}}
370 # Offset against remaining balance if AutoReconcile
371 if ( C4::Context->preference("AccountAutoReconcile")
372 && $credit->amountoutstanding != 0 )
374 $credit = $credit->apply(
376 debits => [ $self->outstanding_debits->as_list ],
377 offset_type => $Koha::Account::offset_type->{$params->{type}}
389 This method allows adding debits to a patron's account
391 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
394 description => $description,
397 interface => $interface,
398 library_id => $library_id,
400 transaction_type => $transaction_type,
401 cash_register => $register_id,
403 issue_id => $issue_id
407 $debit_type can be any of:
427 my ( $self, $params ) = @_;
429 # check for mandatory params
430 my @mandatory = ( 'interface', 'type', 'amount' );
431 for my $param (@mandatory) {
432 unless ( defined( $params->{$param} ) ) {
433 Koha::Exceptions::MissingParameter->throw(
434 error => "The $param parameter is mandatory" );
438 # check for cash register if using cash
439 Koha::Exceptions::Account::RegisterRequired->throw()
440 if ( C4::Context->preference("UseCashRegisters")
441 && defined( $params->{transaction_type} )
442 && ( $params->{transaction_type} eq 'CASH' )
443 && !defined( $params->{cash_register} ) );
445 # amount should always be a positive value
446 my $amount = $params->{amount};
447 unless ( $amount > 0 ) {
448 Koha::Exceptions::Account::AmountNotPositive->throw(
449 error => 'Debit amount passed is not positive' );
452 my $description = $params->{description} // q{};
453 my $note = $params->{note} // q{};
454 my $user_id = $params->{user_id};
455 my $interface = $params->{interface};
456 my $library_id = $params->{library_id};
457 my $cash_register = $params->{cash_register};
458 my $debit_type = $params->{type};
459 my $transaction_type = $params->{transaction_type};
460 my $item_id = $params->{item_id};
461 my $issue_id = $params->{issue_id};
462 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
465 my $schema = Koha::Database->new->schema;
470 # Insert the account line
471 $line = Koha::Account::Line->new(
473 borrowernumber => $self->{patron_id},
476 description => $description,
477 debit_type_code => $debit_type,
478 amountoutstanding => $amount,
479 payment_type => $transaction_type,
481 manager_id => $user_id,
482 interface => $interface,
483 itemnumber => $item_id,
484 issue_id => $issue_id,
485 branchcode => $library_id,
486 register_id => $cash_register,
488 $debit_type eq 'OVERDUE'
489 ? ( status => 'UNRETURNED' )
495 # Record the account offset
496 my $account_offset = Koha::Account::Offset->new(
498 debit_id => $line->id,
499 type => $offset_type,
504 if ( C4::Context->preference("FinesLog") ) {
510 action => "create_$debit_type",
511 borrowernumber => $self->{patron_id},
513 description => $description,
514 amountoutstanding => $amount,
515 debit_type_code => $debit_type,
517 itemnumber => $item_id,
518 manager_id => $user_id,
528 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
529 if ( $_->broken_fk eq 'debit_type_code' ) {
530 Koha::Exceptions::Account::UnrecognisedType->throw(
531 error => 'Type of debit not recognised' );
544 my $debit = $account->payout_amount(
546 payout_type => $payout_type,
547 register_id => $register_id,
548 staff_id => $staff_id,
549 interface => 'intranet',
551 credits => $credit_lines
555 This method allows an amount to be paid out from a patrons account against outstanding credits.
557 $payout_type can be any of the defined payment_types:
562 my ( $self, $params ) = @_;
564 # Check for mandatory parameters
566 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
567 for my $param (@mandatory) {
568 unless ( defined( $params->{$param} ) ) {
569 Koha::Exceptions::MissingParameter->throw(
570 error => "The $param parameter is mandatory" );
574 # Check for mandatory register
575 Koha::Exceptions::Account::RegisterRequired->throw()
576 if ( C4::Context->preference("UseCashRegisters")
577 && ( $params->{payout_type} eq 'CASH' )
578 && !defined($params->{cash_register}) );
580 # Amount should always be passed as a positive value
581 my $amount = $params->{amount};
582 unless ( $amount > 0 ) {
583 Koha::Exceptions::Account::AmountNotPositive->throw(
584 error => 'Payout amount passed is not positive' );
587 # Amount should always be less than or equal to outstanding credit
589 my $outstanding_credits =
590 exists( $params->{credits} )
592 : $self->outstanding_credits->as_list;
593 for my $credit ( @{$outstanding_credits} ) {
594 $outstanding += $credit->amountoutstanding;
596 $outstanding = $outstanding * -1;
597 Koha::Exceptions::ParameterTooHigh->throw( error =>
598 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
599 ) unless ( $outstanding >= $amount );
602 my $schema = Koha::Database->new->schema;
606 # A 'payout' is a 'debit'
607 $payout = $self->add_debit(
609 amount => $params->{amount},
611 transaction_type => $params->{payout_type},
612 amountoutstanding => $params->{amount},
613 manager_id => $params->{staff_id},
614 interface => $params->{interface},
615 branchcode => $params->{branch},
616 cash_register => $params->{cash_register}
620 # Offset against credits
621 for my $credit ( @{$outstanding_credits} ) {
623 { debits => [$payout], offset_type => 'PAYOUT' } );
624 $payout->discard_changes;
625 last if $payout->amountoutstanding == 0;
629 $payout->status('PAID')->store;
638 my $balance = $self->balance
640 Return the balance (sum of amountoutstanding columns)
646 return $self->lines->total_outstanding;
649 =head3 outstanding_debits
651 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
653 It returns the debit lines with outstanding amounts for the patron.
655 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
656 return a list of Koha::Account::Line objects.
660 sub outstanding_debits {
663 return $self->lines->search(
665 amount => { '>' => 0 },
666 amountoutstanding => { '>' => 0 }
671 =head3 outstanding_credits
673 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
675 It returns the credit lines with outstanding amounts for the patron.
677 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
678 return a list of Koha::Account::Line objects.
682 sub outstanding_credits {
685 return $self->lines->search(
687 amount => { '<' => 0 },
688 amountoutstanding => { '<' => 0 }
693 =head3 non_issues_charges
695 my $non_issues_charges = $self->non_issues_charges
697 Calculates amount immediately owing by the patron - non-issue charges.
699 Charges exempt from non-issue are:
700 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
701 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
702 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
706 sub non_issues_charges {
709 #NOTE: With bug 23049 these preferences could be moved to being attached
710 #to individual debit types to give more flexability and specificity.
712 push @not_fines, 'RESERVE'
713 unless C4::Context->preference('HoldsInNoissuesCharge');
714 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
715 unless C4::Context->preference('RentalsInNoissuesCharge');
716 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
717 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
718 push @not_fines, @man_inv;
721 return $self->lines->search(
723 debit_type_code => { -not_in => \@not_fines }
725 )->total_outstanding;
730 my $lines = $self->lines;
732 Return all credits and debits for the user, outstanding or otherwise
739 return Koha::Account::Lines->search(
741 borrowernumber => $self->{patron_id},
746 =head3 reconcile_balance
748 $account->reconcile_balance();
750 Find outstanding credits and use them to pay outstanding debits.
751 Currently, this implicitly uses the 'First In First Out' rule for
752 applying credits against debits.
756 sub reconcile_balance {
759 my $outstanding_debits = $self->outstanding_debits;
760 my $outstanding_credits = $self->outstanding_credits;
762 while ( $outstanding_debits->total_outstanding > 0
763 and my $credit = $outstanding_credits->next )
765 # there's both outstanding debits and credits
766 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
768 $outstanding_debits = $self->outstanding_debits;
784 'CREDIT' => 'Manual Credit',
785 'FORGIVEN' => 'Writeoff',
786 'LOST_FOUND' => 'Lost Item Found',
787 'OVERPAYMENT' => 'Overpayment',
788 'PAYMENT' => 'Payment',
789 'WRITEOFF' => 'Writeoff',
790 'ACCOUNT' => 'Account Fee',
791 'ACCOUNT_RENEW' => 'Account Fee',
792 'RESERVE' => 'Reserve Fee',
793 'PROCESSING' => 'Processing Fee',
794 'LOST' => 'Lost Item',
795 'RENT' => 'Rental Fee',
796 'RENT_DAILY' => 'Rental Fee',
797 'RENT_RENEW' => 'Rental Fee',
798 'RENT_DAILY_RENEW' => 'Rental Fee',
799 'OVERDUE' => 'OVERDUE',
800 'RESERVE_EXPIRED' => 'Hold Expired',
801 'PAYOUT' => 'PAYOUT',
808 Kyle M Hall <kyle.m.hall@gmail.com>
809 Tomás Cohen Arazi <tomascohen@gmail.com>
810 Martin Renvoize <martin.renvoize@ptfs-europe.com>