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->credit_offsets(
118 type => $Koha::Account::offset_type->{$type},
119 -not => { debit_id => undef }
122 if ( C4::Context->preference('UseEmailReceipts') ) {
124 my $letter = C4::Letters::GetPreparedLetter(
125 module => 'circulation',
126 letter_code => uc("ACCOUNT_$type"),
127 message_transport_type => 'email',
128 lang => $patron->lang,
130 borrowers => $self->{patron_id},
131 branches => $library_id,
135 offsets => \@account_offsets,
140 C4::Letters::EnqueueLetter(
143 borrowernumber => $self->{patron_id},
144 message_transport_type => 'email',
146 ) or warn "can't enqueue letter $letter";
150 my $renew_outcomes = [];
151 for my $message ( @{$payment->messages} ) {
152 push @{$renew_outcomes}, $message->payload;
155 return { payment_id => $payment->id, renew_result => $renew_outcomes };
160 This method allows adding credits to a patron's account
162 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
165 description => $description,
168 interface => $interface,
169 library_id => $library_id,
170 payment_type => $payment_type,
171 type => $credit_type,
176 $credit_type can be any of:
189 my ( $self, $params ) = @_;
191 # check for mandatory params
192 my @mandatory = ( 'interface', 'amount' );
193 for my $param (@mandatory) {
194 unless ( defined( $params->{$param} ) ) {
195 Koha::Exceptions::MissingParameter->throw(
196 error => "The $param parameter is mandatory" );
200 # amount should always be passed as a positive value
201 my $amount = $params->{amount} * -1;
202 unless ( $amount < 0 ) {
203 Koha::Exceptions::Account::AmountNotPositive->throw(
204 error => 'Debit amount passed is not positive' );
207 my $description = $params->{description} // q{};
208 my $note = $params->{note} // q{};
209 my $user_id = $params->{user_id};
210 my $interface = $params->{interface};
211 my $library_id = $params->{library_id};
212 my $cash_register = $params->{cash_register};
213 my $payment_type = $params->{payment_type};
214 my $credit_type = $params->{type} || 'PAYMENT';
215 my $item_id = $params->{item_id};
217 Koha::Exceptions::Account::RegisterRequired->throw()
218 if ( C4::Context->preference("UseCashRegisters")
219 && defined($payment_type)
220 && ( $payment_type eq 'CASH' )
221 && !defined($cash_register) );
224 my $schema = Koha::Database->new->schema;
229 # Insert the account line
230 $line = Koha::Account::Line->new(
232 borrowernumber => $self->{patron_id},
235 description => $description,
236 credit_type_code => $credit_type,
237 amountoutstanding => $amount,
238 payment_type => $payment_type,
240 manager_id => $user_id,
241 interface => $interface,
242 branchcode => $library_id,
243 register_id => $cash_register,
244 itemnumber => $item_id,
248 # Record the account offset
249 my $account_offset = Koha::Account::Offset->new(
251 credit_id => $line->id,
252 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
257 C4::Stats::UpdateStats(
259 branch => $library_id,
260 type => lc($credit_type),
262 borrowernumber => $self->{patron_id},
264 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
266 if ( C4::Context->preference("FinesLog") ) {
272 action => "create_$credit_type",
273 borrowernumber => $self->{patron_id},
275 description => $description,
276 amountoutstanding => $amount,
277 credit_type_code => $credit_type,
279 itemnumber => $item_id,
280 manager_id => $user_id,
281 branchcode => $library_id,
291 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
292 if ( $_->broken_fk eq 'credit_type_code' ) {
293 Koha::Exceptions::Account::UnrecognisedType->throw(
294 error => 'Type of credit not recognised' );
307 my $credit = $account->payin_amount(
310 type => $credit_type,
311 payment_type => $payment_type,
312 cash_register => $register_id,
313 interface => $interface,
314 library_id => $branchcode,
315 user_id => $staff_id,
316 debits => $debit_lines,
317 description => $description,
322 This method allows an amount to be paid into a patrons account and immediately applied against debts.
324 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
326 $credit_type can be any of:
334 my ( $self, $params ) = @_;
336 # check for mandatory params
337 my @mandatory = ( 'interface', 'amount', 'type' );
338 for my $param (@mandatory) {
339 unless ( defined( $params->{$param} ) ) {
340 Koha::Exceptions::MissingParameter->throw(
341 error => "The $param parameter is mandatory" );
345 # Check for mandatory register
346 Koha::Exceptions::Account::RegisterRequired->throw()
347 if ( C4::Context->preference("UseCashRegisters")
348 && defined( $params->{payment_type} )
349 && ( $params->{payment_type} eq 'CASH' )
350 && !defined($params->{cash_register}) );
352 # amount should always be passed as a positive value
353 my $amount = $params->{amount};
354 unless ( $amount > 0 ) {
355 Koha::Exceptions::Account::AmountNotPositive->throw(
356 error => 'Payin amount passed is not positive' );
360 my $schema = Koha::Database->new->schema;
365 $credit = $self->add_credit($params);
367 # Offset debts passed first
368 if ( exists( $params->{debits} ) ) {
369 $credit = $credit->apply(
371 debits => $params->{debits},
372 offset_type => $Koha::Account::offset_type->{$params->{type}}
377 # Offset against remaining balance if AutoReconcile
378 if ( C4::Context->preference("AccountAutoReconcile")
379 && $credit->amountoutstanding != 0 )
381 $credit = $credit->apply(
383 debits => [ $self->outstanding_debits->as_list ],
384 offset_type => $Koha::Account::offset_type->{$params->{type}}
396 This method allows adding debits to a patron's account
398 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
401 description => $description,
404 interface => $interface,
405 library_id => $library_id,
407 transaction_type => $transaction_type,
408 cash_register => $register_id,
410 issue_id => $issue_id
414 $debit_type can be any of:
434 my ( $self, $params ) = @_;
436 # check for mandatory params
437 my @mandatory = ( 'interface', 'type', 'amount' );
438 for my $param (@mandatory) {
439 unless ( defined( $params->{$param} ) ) {
440 Koha::Exceptions::MissingParameter->throw(
441 error => "The $param parameter is mandatory" );
445 # check for cash register if using cash
446 Koha::Exceptions::Account::RegisterRequired->throw()
447 if ( C4::Context->preference("UseCashRegisters")
448 && defined( $params->{transaction_type} )
449 && ( $params->{transaction_type} eq 'CASH' )
450 && !defined( $params->{cash_register} ) );
452 # amount should always be a positive value
453 my $amount = $params->{amount};
454 unless ( $amount > 0 ) {
455 Koha::Exceptions::Account::AmountNotPositive->throw(
456 error => 'Debit amount passed is not positive' );
459 my $description = $params->{description} // q{};
460 my $note = $params->{note} // q{};
461 my $user_id = $params->{user_id};
462 my $interface = $params->{interface};
463 my $library_id = $params->{library_id};
464 my $cash_register = $params->{cash_register};
465 my $debit_type = $params->{type};
466 my $transaction_type = $params->{transaction_type};
467 my $item_id = $params->{item_id};
468 my $issue_id = $params->{issue_id};
469 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
472 my $schema = Koha::Database->new->schema;
477 # Insert the account line
478 $line = Koha::Account::Line->new(
480 borrowernumber => $self->{patron_id},
483 description => $description,
484 debit_type_code => $debit_type,
485 amountoutstanding => $amount,
486 payment_type => $transaction_type,
488 manager_id => $user_id,
489 interface => $interface,
490 itemnumber => $item_id,
491 issue_id => $issue_id,
492 branchcode => $library_id,
493 register_id => $cash_register,
495 $debit_type eq 'OVERDUE'
496 ? ( status => 'UNRETURNED' )
502 # Record the account offset
503 my $account_offset = Koha::Account::Offset->new(
505 debit_id => $line->id,
506 type => $offset_type,
511 if ( C4::Context->preference("FinesLog") ) {
517 action => "create_$debit_type",
518 borrowernumber => $self->{patron_id},
520 description => $description,
521 amountoutstanding => $amount,
522 debit_type_code => $debit_type,
524 itemnumber => $item_id,
525 manager_id => $user_id,
535 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
536 if ( $_->broken_fk eq 'debit_type_code' ) {
537 Koha::Exceptions::Account::UnrecognisedType->throw(
538 error => 'Type of debit not recognised' );
551 my $debit = $account->payout_amount(
553 payout_type => $payout_type,
554 register_id => $register_id,
555 staff_id => $staff_id,
556 interface => 'intranet',
558 credits => $credit_lines
562 This method allows an amount to be paid out from a patrons account against outstanding credits.
564 $payout_type can be any of the defined payment_types:
569 my ( $self, $params ) = @_;
571 # Check for mandatory parameters
573 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
574 for my $param (@mandatory) {
575 unless ( defined( $params->{$param} ) ) {
576 Koha::Exceptions::MissingParameter->throw(
577 error => "The $param parameter is mandatory" );
581 # Check for mandatory register
582 Koha::Exceptions::Account::RegisterRequired->throw()
583 if ( C4::Context->preference("UseCashRegisters")
584 && ( $params->{payout_type} eq 'CASH' )
585 && !defined($params->{cash_register}) );
587 # Amount should always be passed as a positive value
588 my $amount = $params->{amount};
589 unless ( $amount > 0 ) {
590 Koha::Exceptions::Account::AmountNotPositive->throw(
591 error => 'Payout amount passed is not positive' );
594 # Amount should always be less than or equal to outstanding credit
596 my $outstanding_credits =
597 exists( $params->{credits} )
599 : $self->outstanding_credits->as_list;
600 for my $credit ( @{$outstanding_credits} ) {
601 $outstanding += $credit->amountoutstanding;
603 $outstanding = $outstanding * -1;
604 Koha::Exceptions::ParameterTooHigh->throw( error =>
605 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
606 ) unless ( $outstanding >= $amount );
609 my $schema = Koha::Database->new->schema;
613 # A 'payout' is a 'debit'
614 $payout = $self->add_debit(
616 amount => $params->{amount},
618 transaction_type => $params->{payout_type},
619 amountoutstanding => $params->{amount},
620 manager_id => $params->{staff_id},
621 interface => $params->{interface},
622 branchcode => $params->{branch},
623 cash_register => $params->{cash_register}
627 # Offset against credits
628 for my $credit ( @{$outstanding_credits} ) {
630 { debits => [$payout], offset_type => 'PAYOUT' } );
631 $payout->discard_changes;
632 last if $payout->amountoutstanding == 0;
636 $payout->status('PAID')->store;
645 my $balance = $self->balance
647 Return the balance (sum of amountoutstanding columns)
653 return $self->lines->total_outstanding;
656 =head3 outstanding_debits
658 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
660 It returns the debit lines with outstanding amounts for the patron.
662 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
663 return a list of Koha::Account::Line objects.
667 sub outstanding_debits {
670 return $self->lines->search(
672 amount => { '>' => 0 },
673 amountoutstanding => { '>' => 0 }
678 =head3 outstanding_credits
680 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
682 It returns the credit lines with outstanding amounts for the patron.
684 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
685 return a list of Koha::Account::Line objects.
689 sub outstanding_credits {
692 return $self->lines->search(
694 amount => { '<' => 0 },
695 amountoutstanding => { '<' => 0 }
700 =head3 non_issues_charges
702 my $non_issues_charges = $self->non_issues_charges
704 Calculates amount immediately owing by the patron - non-issue charges.
706 Charges exempt from non-issue are:
707 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
708 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
709 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
713 sub non_issues_charges {
716 #NOTE: With bug 23049 these preferences could be moved to being attached
717 #to individual debit types to give more flexability and specificity.
719 push @not_fines, 'RESERVE'
720 unless C4::Context->preference('HoldsInNoissuesCharge');
721 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
722 unless C4::Context->preference('RentalsInNoissuesCharge');
723 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
724 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
725 push @not_fines, @man_inv;
728 return $self->lines->search(
730 debit_type_code => { -not_in => \@not_fines }
732 )->total_outstanding;
737 my $lines = $self->lines;
739 Return all credits and debits for the user, outstanding or otherwise
746 return Koha::Account::Lines->search(
748 borrowernumber => $self->{patron_id},
753 =head3 reconcile_balance
755 $account->reconcile_balance();
757 Find outstanding credits and use them to pay outstanding debits.
758 Currently, this implicitly uses the 'First In First Out' rule for
759 applying credits against debits.
763 sub reconcile_balance {
766 my $outstanding_debits = $self->outstanding_debits;
767 my $outstanding_credits = $self->outstanding_credits;
769 while ( $outstanding_debits->total_outstanding > 0
770 and my $credit = $outstanding_credits->next )
772 # there's both outstanding debits and credits
773 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
775 $outstanding_debits = $self->outstanding_debits;
791 'CREDIT' => 'Manual Credit',
792 'FORGIVEN' => 'Writeoff',
793 'LOST_FOUND' => 'Lost Item Found',
794 'OVERPAYMENT' => 'Overpayment',
795 'PAYMENT' => 'Payment',
796 'WRITEOFF' => 'Writeoff',
797 'ACCOUNT' => 'Account Fee',
798 'ACCOUNT_RENEW' => 'Account Fee',
799 'RESERVE' => 'Reserve Fee',
800 'PROCESSING' => 'Processing Fee',
801 'LOST' => 'Lost Item',
802 'RENT' => 'Rental Fee',
803 'RENT_DAILY' => 'Rental Fee',
804 'RENT_RENEW' => 'Rental Fee',
805 'RENT_DAILY_RENEW' => 'Rental Fee',
806 'OVERDUE' => 'OVERDUE',
807 'RESERVE_EXPIRED' => 'Hold Expired',
808 'PAYOUT' => 'PAYOUT',
815 Kyle M Hall <kyle.m.hall@gmail.com>
816 Tomás Cohen Arazi <tomascohen@gmail.com>
817 Martin Renvoize <martin.renvoize@ptfs-europe.com>