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;
90 my $manager_id = $userenv ? $userenv->{number} : undef;
91 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
92 my $payment = $self->payin_amount(
94 interface => $interface,
97 payment_type => $payment_type,
98 cash_register => $cash_register,
99 user_id => $manager_id,
100 library_id => $library_id,
102 description => $description,
108 my $patron = Koha::Patrons->find( $self->{patron_id} );
109 my @account_offsets = $payment->debit_offsets;
110 if ( C4::Context->preference('UseEmailReceipts') ) {
112 my $letter = C4::Letters::GetPreparedLetter(
113 module => 'circulation',
114 letter_code => uc("ACCOUNT_$type"),
115 message_transport_type => 'email',
116 lang => $patron->lang,
118 borrowers => $self->{patron_id},
119 branches => $library_id,
123 offsets => \@account_offsets,
128 C4::Letters::EnqueueLetter(
131 borrowernumber => $self->{patron_id},
132 message_transport_type => 'email',
134 ) or warn "can't enqueue letter $letter";
138 my $renew_outcomes = [];
139 for my $message ( @{$payment->messages} ) {
140 push @{$renew_outcomes}, $message->payload;
143 return { payment_id => $payment->id, renew_result => $renew_outcomes };
148 This method allows adding credits to a patron's account
150 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
153 description => $description,
156 interface => $interface,
157 library_id => $library_id,
158 payment_type => $payment_type,
159 type => $credit_type,
164 $credit_type can be any of:
177 my ( $self, $params ) = @_;
179 # check for mandatory params
180 my @mandatory = ( 'interface', 'amount' );
181 for my $param (@mandatory) {
182 unless ( defined( $params->{$param} ) ) {
183 Koha::Exceptions::MissingParameter->throw(
184 error => "The $param parameter is mandatory" );
188 # amount should always be passed as a positive value
189 my $amount = $params->{amount} * -1;
190 unless ( $amount < 0 ) {
191 Koha::Exceptions::Account::AmountNotPositive->throw(
192 error => 'Debit amount passed is not positive' );
195 my $description = $params->{description} // q{};
196 my $note = $params->{note} // q{};
197 my $user_id = $params->{user_id};
198 my $interface = $params->{interface};
199 my $library_id = $params->{library_id};
200 my $cash_register = $params->{cash_register};
201 my $payment_type = $params->{payment_type};
202 my $credit_type = $params->{type} || 'PAYMENT';
203 my $item_id = $params->{item_id};
205 Koha::Exceptions::Account::RegisterRequired->throw()
206 if ( C4::Context->preference("UseCashRegisters")
207 && defined($payment_type)
208 && ( $payment_type eq 'CASH' )
209 && !defined($cash_register) );
212 my $schema = Koha::Database->new->schema;
217 # Insert the account line
218 $line = Koha::Account::Line->new(
220 borrowernumber => $self->{patron_id},
223 description => $description,
224 credit_type_code => $credit_type,
225 amountoutstanding => $amount,
226 payment_type => $payment_type,
228 manager_id => $user_id,
229 interface => $interface,
230 branchcode => $library_id,
231 register_id => $cash_register,
232 itemnumber => $item_id,
236 # Record the account offset
237 my $account_offset = Koha::Account::Offset->new(
239 credit_id => $line->id,
240 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
245 C4::Stats::UpdateStats(
247 branch => $library_id,
248 type => lc($credit_type),
250 borrowernumber => $self->{patron_id},
252 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
254 if ( C4::Context->preference("FinesLog") ) {
260 action => "create_$credit_type",
261 borrowernumber => $self->{patron_id},
263 description => $description,
264 amountoutstanding => $amount,
265 credit_type_code => $credit_type,
267 itemnumber => $item_id,
268 manager_id => $user_id,
269 branchcode => $library_id,
279 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
280 if ( $_->broken_fk eq 'credit_type_code' ) {
281 Koha::Exceptions::Account::UnrecognisedType->throw(
282 error => 'Type of credit not recognised' );
295 my $credit = $account->payin_amount(
298 type => $credit_type,
299 payment_type => $payment_type,
300 cash_register => $register_id,
301 interface => $interface,
302 library_id => $branchcode,
303 user_id => $staff_id,
304 debits => $debit_lines,
305 description => $description,
310 This method allows an amount to be paid into a patrons account and immediately applied against debts.
312 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
314 $credit_type can be any of:
322 my ( $self, $params ) = @_;
324 # check for mandatory params
325 my @mandatory = ( 'interface', 'amount', 'type' );
326 for my $param (@mandatory) {
327 unless ( defined( $params->{$param} ) ) {
328 Koha::Exceptions::MissingParameter->throw(
329 error => "The $param parameter is mandatory" );
333 # Check for mandatory register
334 Koha::Exceptions::Account::RegisterRequired->throw()
335 if ( C4::Context->preference("UseCashRegisters")
336 && defined( $params->{payment_type} )
337 && ( $params->{payment_type} eq 'CASH' )
338 && !defined($params->{cash_register}) );
340 # amount should always be passed as a positive value
341 my $amount = $params->{amount};
342 unless ( $amount > 0 ) {
343 Koha::Exceptions::Account::AmountNotPositive->throw(
344 error => 'Payin amount passed is not positive' );
348 my $schema = Koha::Database->new->schema;
353 $credit = $self->add_credit($params);
355 # Offset debts passed first
356 if ( exists( $params->{debits} ) ) {
357 $credit = $credit->apply(
359 debits => $params->{debits},
360 offset_type => $Koha::Account::offset_type->{$params->{type}}
365 # Offset against remaining balance if AutoReconcile
366 if ( C4::Context->preference("AccountAutoReconcile")
367 && $credit->amountoutstanding != 0 )
369 $credit = $credit->apply(
371 debits => [ $self->outstanding_debits->as_list ],
372 offset_type => $Koha::Account::offset_type->{$params->{type}}
384 This method allows adding debits to a patron's account
386 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
389 description => $description,
392 interface => $interface,
393 library_id => $library_id,
395 transaction_type => $transaction_type,
396 cash_register => $register_id,
398 issue_id => $issue_id
402 $debit_type can be any of:
422 my ( $self, $params ) = @_;
424 # check for mandatory params
425 my @mandatory = ( 'interface', 'type', 'amount' );
426 for my $param (@mandatory) {
427 unless ( defined( $params->{$param} ) ) {
428 Koha::Exceptions::MissingParameter->throw(
429 error => "The $param parameter is mandatory" );
433 # check for cash register if using cash
434 Koha::Exceptions::Account::RegisterRequired->throw()
435 if ( C4::Context->preference("UseCashRegisters")
436 && defined( $params->{transaction_type} )
437 && ( $params->{transaction_type} eq 'CASH' )
438 && !defined( $params->{cash_register} ) );
440 # amount should always be a positive value
441 my $amount = $params->{amount};
442 unless ( $amount > 0 ) {
443 Koha::Exceptions::Account::AmountNotPositive->throw(
444 error => 'Debit amount passed is not positive' );
447 my $description = $params->{description} // q{};
448 my $note = $params->{note} // q{};
449 my $user_id = $params->{user_id};
450 my $interface = $params->{interface};
451 my $library_id = $params->{library_id};
452 my $cash_register = $params->{cash_register};
453 my $debit_type = $params->{type};
454 my $transaction_type = $params->{transaction_type};
455 my $item_id = $params->{item_id};
456 my $issue_id = $params->{issue_id};
457 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
460 my $schema = Koha::Database->new->schema;
465 # Insert the account line
466 $line = Koha::Account::Line->new(
468 borrowernumber => $self->{patron_id},
471 description => $description,
472 debit_type_code => $debit_type,
473 amountoutstanding => $amount,
474 payment_type => $transaction_type,
476 manager_id => $user_id,
477 interface => $interface,
478 itemnumber => $item_id,
479 issue_id => $issue_id,
480 branchcode => $library_id,
481 register_id => $cash_register,
483 $debit_type eq 'OVERDUE'
484 ? ( status => 'UNRETURNED' )
490 # Record the account offset
491 my $account_offset = Koha::Account::Offset->new(
493 debit_id => $line->id,
494 type => $offset_type,
499 if ( C4::Context->preference("FinesLog") ) {
505 action => "create_$debit_type",
506 borrowernumber => $self->{patron_id},
508 description => $description,
509 amountoutstanding => $amount,
510 debit_type_code => $debit_type,
512 itemnumber => $item_id,
513 manager_id => $user_id,
523 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
524 if ( $_->broken_fk eq 'debit_type_code' ) {
525 Koha::Exceptions::Account::UnrecognisedType->throw(
526 error => 'Type of debit not recognised' );
539 my $debit = $account->payout_amount(
541 payout_type => $payout_type,
542 register_id => $register_id,
543 staff_id => $staff_id,
544 interface => 'intranet',
546 credits => $credit_lines
550 This method allows an amount to be paid out from a patrons account against outstanding credits.
552 $payout_type can be any of the defined payment_types:
557 my ( $self, $params ) = @_;
559 # Check for mandatory parameters
561 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
562 for my $param (@mandatory) {
563 unless ( defined( $params->{$param} ) ) {
564 Koha::Exceptions::MissingParameter->throw(
565 error => "The $param parameter is mandatory" );
569 # Check for mandatory register
570 Koha::Exceptions::Account::RegisterRequired->throw()
571 if ( C4::Context->preference("UseCashRegisters")
572 && ( $params->{payout_type} eq 'CASH' )
573 && !defined($params->{cash_register}) );
575 # Amount should always be passed as a positive value
576 my $amount = $params->{amount};
577 unless ( $amount > 0 ) {
578 Koha::Exceptions::Account::AmountNotPositive->throw(
579 error => 'Payout amount passed is not positive' );
582 # Amount should always be less than or equal to outstanding credit
584 my $outstanding_credits =
585 exists( $params->{credits} )
587 : $self->outstanding_credits->as_list;
588 for my $credit ( @{$outstanding_credits} ) {
589 $outstanding += $credit->amountoutstanding;
591 $outstanding = $outstanding * -1;
592 Koha::Exceptions::ParameterTooHigh->throw( error =>
593 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
594 ) unless ( $outstanding >= $amount );
597 my $schema = Koha::Database->new->schema;
601 # A 'payout' is a 'debit'
602 $payout = $self->add_debit(
604 amount => $params->{amount},
606 transaction_type => $params->{payout_type},
607 amountoutstanding => $params->{amount},
608 manager_id => $params->{staff_id},
609 interface => $params->{interface},
610 branchcode => $params->{branch},
611 cash_register => $params->{cash_register}
615 # Offset against credits
616 for my $credit ( @{$outstanding_credits} ) {
618 { debits => [$payout], offset_type => 'PAYOUT' } );
619 $payout->discard_changes;
620 last if $payout->amountoutstanding == 0;
624 $payout->status('PAID')->store;
633 my $balance = $self->balance
635 Return the balance (sum of amountoutstanding columns)
641 return $self->lines->total_outstanding;
644 =head3 outstanding_debits
646 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
648 It returns the debit lines with outstanding amounts for the patron.
650 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
651 return a list of Koha::Account::Line objects.
655 sub outstanding_debits {
658 return $self->lines->search(
660 amount => { '>' => 0 },
661 amountoutstanding => { '>' => 0 }
666 =head3 outstanding_credits
668 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
670 It returns the credit lines with outstanding amounts for the patron.
672 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
673 return a list of Koha::Account::Line objects.
677 sub outstanding_credits {
680 return $self->lines->search(
682 amount => { '<' => 0 },
683 amountoutstanding => { '<' => 0 }
688 =head3 non_issues_charges
690 my $non_issues_charges = $self->non_issues_charges
692 Calculates amount immediately owing by the patron - non-issue charges.
694 Charges exempt from non-issue are:
695 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
696 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
697 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
701 sub non_issues_charges {
704 #NOTE: With bug 23049 these preferences could be moved to being attached
705 #to individual debit types to give more flexability and specificity.
707 push @not_fines, 'RESERVE'
708 unless C4::Context->preference('HoldsInNoissuesCharge');
709 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
710 unless C4::Context->preference('RentalsInNoissuesCharge');
711 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
712 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
713 push @not_fines, @man_inv;
716 return $self->lines->search(
718 debit_type_code => { -not_in => \@not_fines }
720 )->total_outstanding;
725 my $lines = $self->lines;
727 Return all credits and debits for the user, outstanding or otherwise
734 return Koha::Account::Lines->search(
736 borrowernumber => $self->{patron_id},
741 =head3 reconcile_balance
743 $account->reconcile_balance();
745 Find outstanding credits and use them to pay outstanding debits.
746 Currently, this implicitly uses the 'First In First Out' rule for
747 applying credits against debits.
751 sub reconcile_balance {
754 my $outstanding_debits = $self->outstanding_debits;
755 my $outstanding_credits = $self->outstanding_credits;
757 while ( $outstanding_debits->total_outstanding > 0
758 and my $credit = $outstanding_credits->next )
760 # there's both outstanding debits and credits
761 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
763 $outstanding_debits = $self->outstanding_debits;
779 'CREDIT' => 'Manual Credit',
780 'FORGIVEN' => 'Writeoff',
781 'LOST_FOUND' => 'Lost Item Found',
782 'OVERPAYMENT' => 'Overpayment',
783 'PAYMENT' => 'Payment',
784 'WRITEOFF' => 'Writeoff',
785 'ACCOUNT' => 'Account Fee',
786 'ACCOUNT_RENEW' => 'Account Fee',
787 'RESERVE' => 'Reserve Fee',
788 'PROCESSING' => 'Processing Fee',
789 'LOST' => 'Lost Item',
790 'RENT' => 'Rental Fee',
791 'RENT_DAILY' => 'Rental Fee',
792 'RENT_RENEW' => 'Rental Fee',
793 'RENT_DAILY_RENEW' => 'Rental Fee',
794 'OVERDUE' => 'OVERDUE',
795 'RESERVE_EXPIRED' => 'Hold Expired',
796 'PAYOUT' => 'PAYOUT',
803 Kyle M Hall <kyle.m.hall@gmail.com>
804 Tomás Cohen Arazi <tomascohen@gmail.com>
805 Martin Renvoize <martin.renvoize@ptfs-europe.com>