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 $offset_type = $params->{offset_type} || $type eq 'WRITEOFF' ? 'Writeoff' : 'Payment';
85 my $cash_register = $params->{cash_register};
86 my $item_id = $params->{item_id};
88 my $userenv = C4::Context->userenv;
91 my $manager_id = $userenv ? $userenv->{number} : undef;
92 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
93 my $payment = $self->payin_amount(
95 interface => $interface,
98 payment_type => $payment_type,
99 cash_register => $cash_register,
100 user_id => $manager_id,
101 library_id => $library_id,
103 description => $description,
109 my $patron = Koha::Patrons->find( $self->{patron_id} );
110 my @account_offsets = $payment->debit_offsets;
111 if ( C4::Context->preference('UseEmailReceipts') ) {
113 my $letter = C4::Letters::GetPreparedLetter(
114 module => 'circulation',
115 letter_code => uc("ACCOUNT_$type"),
116 message_transport_type => 'email',
117 lang => $patron->lang,
119 borrowers => $self->{patron_id},
120 branches => $library_id,
124 offsets => \@account_offsets,
129 C4::Letters::EnqueueLetter(
132 borrowernumber => $self->{patron_id},
133 message_transport_type => 'email',
135 ) or warn "can't enqueue letter $letter";
139 my $renew_outcomes = [];
140 for my $message ( @{$payment->messages} ) {
141 push @{$renew_outcomes}, $message->payload;
144 return { payment_id => $payment->id, renew_result => $renew_outcomes };
149 This method allows adding credits to a patron's account
151 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
154 description => $description,
157 interface => $interface,
158 library_id => $library_id,
159 payment_type => $payment_type,
160 type => $credit_type,
165 $credit_type can be any of:
178 my ( $self, $params ) = @_;
180 # check for mandatory params
181 my @mandatory = ( 'interface', 'amount' );
182 for my $param (@mandatory) {
183 unless ( defined( $params->{$param} ) ) {
184 Koha::Exceptions::MissingParameter->throw(
185 error => "The $param parameter is mandatory" );
189 # amount should always be passed as a positive value
190 my $amount = $params->{amount} * -1;
191 unless ( $amount < 0 ) {
192 Koha::Exceptions::Account::AmountNotPositive->throw(
193 error => 'Debit amount passed is not positive' );
196 my $description = $params->{description} // q{};
197 my $note = $params->{note} // q{};
198 my $user_id = $params->{user_id};
199 my $interface = $params->{interface};
200 my $library_id = $params->{library_id};
201 my $cash_register = $params->{cash_register};
202 my $payment_type = $params->{payment_type};
203 my $credit_type = $params->{type} || 'PAYMENT';
204 my $item_id = $params->{item_id};
206 Koha::Exceptions::Account::RegisterRequired->throw()
207 if ( C4::Context->preference("UseCashRegisters")
208 && defined($payment_type)
209 && ( $payment_type eq 'CASH' )
210 && !defined($cash_register) );
213 my $schema = Koha::Database->new->schema;
218 # Insert the account line
219 $line = Koha::Account::Line->new(
221 borrowernumber => $self->{patron_id},
224 description => $description,
225 credit_type_code => $credit_type,
226 amountoutstanding => $amount,
227 payment_type => $payment_type,
229 manager_id => $user_id,
230 interface => $interface,
231 branchcode => $library_id,
232 register_id => $cash_register,
233 itemnumber => $item_id,
237 # Record the account offset
238 my $account_offset = Koha::Account::Offset->new(
240 credit_id => $line->id,
241 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
246 C4::Stats::UpdateStats(
248 branch => $library_id,
249 type => lc($credit_type),
251 borrowernumber => $self->{patron_id},
253 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
255 if ( C4::Context->preference("FinesLog") ) {
261 action => "create_$credit_type",
262 borrowernumber => $self->{patron_id},
264 description => $description,
265 amountoutstanding => $amount,
266 credit_type_code => $credit_type,
268 itemnumber => $item_id,
269 manager_id => $user_id,
270 branchcode => $library_id,
280 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
281 if ( $_->broken_fk eq 'credit_type_code' ) {
282 Koha::Exceptions::Account::UnrecognisedType->throw(
283 error => 'Type of credit not recognised' );
296 my $credit = $account->payin_amount(
299 type => $credit_type,
300 payment_type => $payment_type,
301 cash_register => $register_id,
302 interface => $interface,
303 library_id => $branchcode,
304 user_id => $staff_id,
305 debits => $debit_lines,
306 description => $description,
311 This method allows an amount to be paid into a patrons account and immediately applied against debts.
313 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
315 $credit_type can be any of:
323 my ( $self, $params ) = @_;
325 # check for mandatory params
326 my @mandatory = ( 'interface', 'amount', 'type' );
327 for my $param (@mandatory) {
328 unless ( defined( $params->{$param} ) ) {
329 Koha::Exceptions::MissingParameter->throw(
330 error => "The $param parameter is mandatory" );
334 # Check for mandatory register
335 Koha::Exceptions::Account::RegisterRequired->throw()
336 if ( C4::Context->preference("UseCashRegisters")
337 && defined( $params->{payment_type} )
338 && ( $params->{payment_type} eq 'CASH' )
339 && !defined($params->{cash_register}) );
341 # amount should always be passed as a positive value
342 my $amount = $params->{amount};
343 unless ( $amount > 0 ) {
344 Koha::Exceptions::Account::AmountNotPositive->throw(
345 error => 'Payin amount passed is not positive' );
349 my $schema = Koha::Database->new->schema;
354 $credit = $self->add_credit($params);
356 # Offset debts passed first
357 if ( exists( $params->{debits} ) ) {
358 $credit = $credit->apply(
360 debits => $params->{debits},
361 offset_type => $params->{type}
366 # Offset against remaining balance if AutoReconcile
367 if ( C4::Context->preference("AccountAutoReconcile")
368 && $credit->amountoutstanding != 0 )
370 $credit = $credit->apply(
372 debits => [ $self->outstanding_debits->as_list ],
373 offset_type => $params->{type}
385 This method allows adding debits to a patron's account
387 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
390 description => $description,
393 interface => $interface,
394 library_id => $library_id,
396 transaction_type => $transaction_type,
397 cash_register => $register_id,
399 issue_id => $issue_id
403 $debit_type can be any of:
423 my ( $self, $params ) = @_;
425 # check for mandatory params
426 my @mandatory = ( 'interface', 'type', 'amount' );
427 for my $param (@mandatory) {
428 unless ( defined( $params->{$param} ) ) {
429 Koha::Exceptions::MissingParameter->throw(
430 error => "The $param parameter is mandatory" );
434 # check for cash register if using cash
435 Koha::Exceptions::Account::RegisterRequired->throw()
436 if ( C4::Context->preference("UseCashRegisters")
437 && defined( $params->{transaction_type} )
438 && ( $params->{transaction_type} eq 'CASH' )
439 && !defined( $params->{cash_register} ) );
441 # amount should always be a positive value
442 my $amount = $params->{amount};
443 unless ( $amount > 0 ) {
444 Koha::Exceptions::Account::AmountNotPositive->throw(
445 error => 'Debit amount passed is not positive' );
448 my $description = $params->{description} // q{};
449 my $note = $params->{note} // q{};
450 my $user_id = $params->{user_id};
451 my $interface = $params->{interface};
452 my $library_id = $params->{library_id};
453 my $cash_register = $params->{cash_register};
454 my $debit_type = $params->{type};
455 my $transaction_type = $params->{transaction_type};
456 my $item_id = $params->{item_id};
457 my $issue_id = $params->{issue_id};
458 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
461 my $schema = Koha::Database->new->schema;
466 # Insert the account line
467 $line = Koha::Account::Line->new(
469 borrowernumber => $self->{patron_id},
472 description => $description,
473 debit_type_code => $debit_type,
474 amountoutstanding => $amount,
475 payment_type => $transaction_type,
477 manager_id => $user_id,
478 interface => $interface,
479 itemnumber => $item_id,
480 issue_id => $issue_id,
481 branchcode => $library_id,
482 register_id => $cash_register,
484 $debit_type eq 'OVERDUE'
485 ? ( status => 'UNRETURNED' )
491 # Record the account offset
492 my $account_offset = Koha::Account::Offset->new(
494 debit_id => $line->id,
495 type => $offset_type,
500 if ( C4::Context->preference("FinesLog") ) {
506 action => "create_$debit_type",
507 borrowernumber => $self->{patron_id},
509 description => $description,
510 amountoutstanding => $amount,
511 debit_type_code => $debit_type,
513 itemnumber => $item_id,
514 manager_id => $user_id,
524 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
525 if ( $_->broken_fk eq 'debit_type_code' ) {
526 Koha::Exceptions::Account::UnrecognisedType->throw(
527 error => 'Type of debit not recognised' );
540 my $debit = $account->payout_amount(
542 payout_type => $payout_type,
543 register_id => $register_id,
544 staff_id => $staff_id,
545 interface => 'intranet',
547 credits => $credit_lines
551 This method allows an amount to be paid out from a patrons account against outstanding credits.
553 $payout_type can be any of the defined payment_types:
558 my ( $self, $params ) = @_;
560 # Check for mandatory parameters
562 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
563 for my $param (@mandatory) {
564 unless ( defined( $params->{$param} ) ) {
565 Koha::Exceptions::MissingParameter->throw(
566 error => "The $param parameter is mandatory" );
570 # Check for mandatory register
571 Koha::Exceptions::Account::RegisterRequired->throw()
572 if ( C4::Context->preference("UseCashRegisters")
573 && ( $params->{payout_type} eq 'CASH' )
574 && !defined($params->{cash_register}) );
576 # Amount should always be passed as a positive value
577 my $amount = $params->{amount};
578 unless ( $amount > 0 ) {
579 Koha::Exceptions::Account::AmountNotPositive->throw(
580 error => 'Payout amount passed is not positive' );
583 # Amount should always be less than or equal to outstanding credit
585 my $outstanding_credits =
586 exists( $params->{credits} )
588 : $self->outstanding_credits->as_list;
589 for my $credit ( @{$outstanding_credits} ) {
590 $outstanding += $credit->amountoutstanding;
592 $outstanding = $outstanding * -1;
593 Koha::Exceptions::ParameterTooHigh->throw( error =>
594 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
595 ) unless ( $outstanding >= $amount );
598 my $schema = Koha::Database->new->schema;
602 # A 'payout' is a 'debit'
603 $payout = $self->add_debit(
605 amount => $params->{amount},
607 transaction_type => $params->{payout_type},
608 amountoutstanding => $params->{amount},
609 manager_id => $params->{staff_id},
610 interface => $params->{interface},
611 branchcode => $params->{branch},
612 cash_register => $params->{cash_register}
616 # Offset against credits
617 for my $credit ( @{$outstanding_credits} ) {
619 { debits => [$payout], offset_type => 'PAYOUT' } );
620 $payout->discard_changes;
621 last if $payout->amountoutstanding == 0;
625 $payout->status('PAID')->store;
634 my $balance = $self->balance
636 Return the balance (sum of amountoutstanding columns)
642 return $self->lines->total_outstanding;
645 =head3 outstanding_debits
647 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
649 It returns the debit lines with outstanding amounts for the patron.
651 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
652 return a list of Koha::Account::Line objects.
656 sub outstanding_debits {
659 return $self->lines->search(
661 amount => { '>' => 0 },
662 amountoutstanding => { '>' => 0 }
667 =head3 outstanding_credits
669 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
671 It returns the credit lines with outstanding amounts for the patron.
673 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
674 return a list of Koha::Account::Line objects.
678 sub outstanding_credits {
681 return $self->lines->search(
683 amount => { '<' => 0 },
684 amountoutstanding => { '<' => 0 }
689 =head3 non_issues_charges
691 my $non_issues_charges = $self->non_issues_charges
693 Calculates amount immediately owing by the patron - non-issue charges.
695 Charges exempt from non-issue are:
696 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
697 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
698 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
702 sub non_issues_charges {
705 #NOTE: With bug 23049 these preferences could be moved to being attached
706 #to individual debit types to give more flexability and specificity.
708 push @not_fines, 'RESERVE'
709 unless C4::Context->preference('HoldsInNoissuesCharge');
710 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
711 unless C4::Context->preference('RentalsInNoissuesCharge');
712 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
713 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
714 push @not_fines, @man_inv;
717 return $self->lines->search(
719 debit_type_code => { -not_in => \@not_fines }
721 )->total_outstanding;
726 my $lines = $self->lines;
728 Return all credits and debits for the user, outstanding or otherwise
735 return Koha::Account::Lines->search(
737 borrowernumber => $self->{patron_id},
742 =head3 reconcile_balance
744 $account->reconcile_balance();
746 Find outstanding credits and use them to pay outstanding debits.
747 Currently, this implicitly uses the 'First In First Out' rule for
748 applying credits against debits.
752 sub reconcile_balance {
755 my $outstanding_debits = $self->outstanding_debits;
756 my $outstanding_credits = $self->outstanding_credits;
758 while ( $outstanding_debits->total_outstanding > 0
759 and my $credit = $outstanding_credits->next )
761 # there's both outstanding debits and credits
762 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
764 $outstanding_debits = $self->outstanding_debits;
780 'CREDIT' => 'Manual Credit',
781 'FORGIVEN' => 'Writeoff',
782 'LOST_FOUND' => 'Lost Item Found',
783 'OVERPAYMENT' => 'Overpayment',
784 'PAYMENT' => 'Payment',
785 'WRITEOFF' => 'Writeoff',
786 'ACCOUNT' => 'Account Fee',
787 'ACCOUNT_RENEW' => 'Account Fee',
788 'RESERVE' => 'Reserve Fee',
789 'PROCESSING' => 'Processing Fee',
790 'LOST' => 'Lost Item',
791 'RENT' => 'Rental Fee',
792 'RENT_DAILY' => 'Rental Fee',
793 'RENT_RENEW' => 'Rental Fee',
794 'RENT_DAILY_RENEW' => 'Rental Fee',
795 'OVERDUE' => 'OVERDUE',
796 'RESERVE_EXPIRED' => 'Hold Expired',
797 'PAYOUT' => 'PAYOUT',
804 Kyle M Hall <kyle.m.hall@gmail.com>
805 Tomás Cohen Arazi <tomascohen@gmail.com>
806 Martin Renvoize <martin.renvoize@ptfs-europe.com>