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::Credits;
34 use Koha::Account::Debits;
35 use Koha::Account::Lines;
36 use Koha::Account::Offsets;
37 use Koha::Account::DebitTypes;
39 use Koha::Exceptions::Account;
44 Koha::Accounts - Module for managing payments and fees for patrons
49 my ( $class, $params ) = @_;
51 Carp::croak("No patron id passed in!") unless $params->{patron_id};
53 return bless( $params, $class );
58 This method allows payments to be made against fees/fines
60 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
64 description => $description,
65 library_id => $branchcode,
66 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
67 credit_type => $type, # credit_type_code 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({ type => 'APPLY' })->as_list;
117 if ( C4::Context->preference('UseEmailReceipts') ) {
119 my $letter = C4::Letters::GetPreparedLetter(
120 module => 'circulation',
121 letter_code => uc("ACCOUNT_$type"),
122 message_transport_type => 'email',
123 lang => $patron->lang,
125 borrowers => $self->{patron_id},
126 branches => $library_id,
130 offsets => \@account_offsets,
135 C4::Letters::EnqueueLetter(
138 borrowernumber => $self->{patron_id},
139 message_transport_type => 'email',
141 ) or warn "can't enqueue letter $letter";
145 my $renew_outcomes = [];
146 for my $message ( @{$payment->object_messages} ) {
147 push @{$renew_outcomes}, $message->payload;
150 return { payment_id => $payment->id, renew_result => $renew_outcomes };
155 This method allows adding credits to a patron's account
157 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
160 description => $description,
163 interface => $interface,
164 library_id => $library_id,
165 payment_type => $payment_type,
166 type => $credit_type,
171 $credit_type can be any of:
185 my ( $self, $params ) = @_;
187 # check for mandatory params
188 my @mandatory = ( 'interface', 'amount' );
189 for my $param (@mandatory) {
190 unless ( defined( $params->{$param} ) ) {
191 Koha::Exceptions::MissingParameter->throw(
192 error => "The $param parameter is mandatory" );
196 # amount should always be passed as a positive value
197 my $amount = $params->{amount} * -1;
198 unless ( $amount < 0 ) {
199 Koha::Exceptions::Account::AmountNotPositive->throw(
200 error => 'Debit amount passed is not positive' );
203 my $description = $params->{description} // q{};
204 my $note = $params->{note} // q{};
205 my $user_id = $params->{user_id};
206 my $interface = $params->{interface};
207 my $library_id = $params->{library_id};
208 my $cash_register = $params->{cash_register};
209 my $payment_type = $params->{payment_type};
210 my $credit_type = $params->{type} || 'PAYMENT';
211 my $item_id = $params->{item_id};
213 Koha::Exceptions::Account::RegisterRequired->throw()
214 if ( C4::Context->preference("UseCashRegisters")
215 && defined($payment_type)
216 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
217 && !defined($cash_register) );
220 my $schema = Koha::Database->new->schema;
225 # Insert the account line
226 $line = Koha::Account::Line->new(
228 borrowernumber => $self->{patron_id},
231 description => $description,
232 credit_type_code => $credit_type,
233 amountoutstanding => $amount,
234 payment_type => $payment_type,
236 manager_id => $user_id,
237 interface => $interface,
238 branchcode => $library_id,
239 register_id => $cash_register,
240 itemnumber => $item_id,
244 # Record the account offset
245 my $account_offset = Koha::Account::Offset->new(
247 credit_id => $line->id,
249 amount => $amount * -1
253 C4::Stats::UpdateStats(
255 branch => $library_id,
256 type => lc($credit_type),
258 borrowernumber => $self->{patron_id},
260 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
263 'after_account_action',
265 action => "add_credit",
267 type => lc($credit_type),
268 line => $line->get_from_storage, #TODO Seems unneeded
273 if ( C4::Context->preference("FinesLog") ) {
279 action => "create_$credit_type",
280 borrowernumber => $self->{patron_id},
282 description => $description,
283 amountoutstanding => $amount,
284 credit_type_code => $credit_type,
286 itemnumber => $item_id,
287 manager_id => $user_id,
288 branchcode => $library_id,
298 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
299 if ( $_->broken_fk eq 'credit_type_code' ) {
300 Koha::Exceptions::Account::UnrecognisedType->throw(
301 error => 'Type of credit not recognised' );
314 my $credit = $account->payin_amount(
317 type => $credit_type,
318 payment_type => $payment_type,
319 cash_register => $register_id,
320 interface => $interface,
321 library_id => $branchcode,
322 user_id => $staff_id,
323 debits => $debit_lines,
324 description => $description,
329 This method allows an amount to be paid into a patrons account and immediately applied against debts.
331 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
333 $credit_type can be any of:
341 my ( $self, $params ) = @_;
343 # check for mandatory params
344 my @mandatory = ( 'interface', 'amount', 'type' );
345 for my $param (@mandatory) {
346 unless ( defined( $params->{$param} ) ) {
347 Koha::Exceptions::MissingParameter->throw(
348 error => "The $param parameter is mandatory" );
352 # Check for mandatory register
353 Koha::Exceptions::Account::RegisterRequired->throw()
354 if ( C4::Context->preference("UseCashRegisters")
355 && defined( $params->{payment_type} )
356 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
357 && !defined($params->{cash_register}) );
359 # amount should always be passed as a positive value
360 my $amount = $params->{amount};
361 unless ( $amount > 0 ) {
362 Koha::Exceptions::Account::AmountNotPositive->throw(
363 error => 'Payin amount passed is not positive' );
367 my $schema = Koha::Database->new->schema;
372 $credit = $self->add_credit($params);
374 # Offset debts passed first
375 if ( exists( $params->{debits} ) ) {
376 $credit = $credit->apply(
378 debits => $params->{debits}
383 # Offset against remaining balance if AutoReconcile
384 if ( C4::Context->preference("AccountAutoReconcile")
385 && $credit->amountoutstanding != 0 )
387 $credit = $credit->apply(
389 debits => [ $self->outstanding_debits->as_list ]
401 This method allows adding debits to a patron's account
403 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
406 description => $description,
409 interface => $interface,
410 library_id => $library_id,
412 transaction_type => $transaction_type,
413 cash_register => $register_id,
415 issue_id => $issue_id
419 $debit_type can be any of:
439 my ( $self, $params ) = @_;
441 # check for mandatory params
442 my @mandatory = ( 'interface', 'type', 'amount' );
443 for my $param (@mandatory) {
444 unless ( defined( $params->{$param} ) ) {
445 Koha::Exceptions::MissingParameter->throw(
446 error => "The $param parameter is mandatory" );
450 # check for cash register if using cash
451 Koha::Exceptions::Account::RegisterRequired->throw()
452 if ( C4::Context->preference("UseCashRegisters")
453 && defined( $params->{transaction_type} )
454 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
455 && !defined( $params->{cash_register} ) );
457 # amount should always be a positive value
458 my $amount = $params->{amount};
459 unless ( $amount > 0 ) {
460 Koha::Exceptions::Account::AmountNotPositive->throw(
461 error => 'Debit amount passed is not positive' );
464 my $description = $params->{description} // q{};
465 my $note = $params->{note} // q{};
466 my $user_id = $params->{user_id};
467 my $interface = $params->{interface};
468 my $library_id = $params->{library_id};
469 my $cash_register = $params->{cash_register};
470 my $debit_type = $params->{type};
471 my $transaction_type = $params->{transaction_type};
472 my $item_id = $params->{item_id};
473 my $issue_id = $params->{issue_id};
476 my $schema = Koha::Database->new->schema;
481 # Insert the account line
482 $line = Koha::Account::Line->new(
484 borrowernumber => $self->{patron_id},
487 description => $description,
488 debit_type_code => $debit_type,
489 amountoutstanding => $amount,
490 payment_type => $transaction_type,
492 manager_id => $user_id,
493 interface => $interface,
494 itemnumber => $item_id,
495 issue_id => $issue_id,
496 branchcode => $library_id,
497 register_id => $cash_register,
499 $debit_type eq 'OVERDUE'
500 ? ( status => 'UNRETURNED' )
506 # Record the account offset
507 my $account_offset = Koha::Account::Offset->new(
509 debit_id => $line->id,
515 if ( C4::Context->preference("FinesLog") ) {
521 action => "create_$debit_type",
522 borrowernumber => $self->{patron_id},
524 description => $description,
525 amountoutstanding => $amount,
526 debit_type_code => $debit_type,
528 itemnumber => $item_id,
529 manager_id => $user_id,
539 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
540 if ( $_->broken_fk eq 'debit_type_code' ) {
541 Koha::Exceptions::Account::UnrecognisedType->throw(
542 error => 'Type of debit not recognised' );
555 my $debit = $account->payout_amount(
557 payout_type => $payout_type,
558 register_id => $register_id,
559 staff_id => $staff_id,
560 interface => 'intranet',
562 credits => $credit_lines
566 This method allows an amount to be paid out from a patrons account against outstanding credits.
568 $payout_type can be any of the defined payment_types:
573 my ( $self, $params ) = @_;
575 # Check for mandatory parameters
577 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
578 for my $param (@mandatory) {
579 unless ( defined( $params->{$param} ) ) {
580 Koha::Exceptions::MissingParameter->throw(
581 error => "The $param parameter is mandatory" );
585 # Check for mandatory register
586 Koha::Exceptions::Account::RegisterRequired->throw()
587 if ( C4::Context->preference("UseCashRegisters")
588 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
589 && !defined($params->{cash_register}) );
591 # Amount should always be passed as a positive value
592 my $amount = $params->{amount};
593 unless ( $amount > 0 ) {
594 Koha::Exceptions::Account::AmountNotPositive->throw(
595 error => 'Payout amount passed is not positive' );
598 # Amount should always be less than or equal to outstanding credit
600 my $outstanding_credits =
601 exists( $params->{credits} )
603 : $self->outstanding_credits->as_list;
604 for my $credit ( @{$outstanding_credits} ) {
605 $outstanding += $credit->amountoutstanding;
607 $outstanding = $outstanding * -1;
608 Koha::Exceptions::ParameterTooHigh->throw( error =>
609 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
610 ) unless ( $outstanding >= $amount );
613 my $schema = Koha::Database->new->schema;
617 # A 'payout' is a 'debit'
618 $payout = $self->add_debit(
620 amount => $params->{amount},
622 transaction_type => $params->{payout_type},
623 amountoutstanding => $params->{amount},
624 user_id => $params->{staff_id},
625 interface => $params->{interface},
626 branchcode => $params->{branch},
627 cash_register => $params->{cash_register}
631 # Offset against credits
632 for my $credit ( @{$outstanding_credits} ) {
633 $credit->apply( { debits => [$payout] } );
634 $payout->discard_changes;
635 last if $payout->amountoutstanding == 0;
639 $payout->status('PAID')->store;
648 my $balance = $self->balance
650 Return the balance (sum of amountoutstanding columns)
656 return $self->lines->total_outstanding;
659 =head3 outstanding_debits
661 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
663 It returns the debit lines with outstanding amounts for the patron.
665 It returns a Koha::Account::Lines iterator.
669 sub outstanding_debits {
672 return $self->lines->search(
674 amount => { '>' => 0 },
675 amountoutstanding => { '>' => 0 }
680 =head3 outstanding_credits
682 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
684 It returns the credit lines with outstanding amounts for the patron.
686 It returns a Koha::Account::Lines iterator.
690 sub outstanding_credits {
693 return $self->lines->search(
695 amount => { '<' => 0 },
696 amountoutstanding => { '<' => 0 }
701 =head3 non_issues_charges
703 my $non_issues_charges = $self->non_issues_charges
705 Calculates amount immediately owing by the patron - non-issue charges.
707 Charges exempt from non-issue are:
708 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
709 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
710 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
714 sub non_issues_charges {
717 #NOTE: With bug 23049 these preferences could be moved to being attached
718 #to individual debit types to give more flexability and specificity.
720 push @not_fines, 'RESERVE'
721 unless C4::Context->preference('HoldsInNoissuesCharge');
722 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
723 unless C4::Context->preference('RentalsInNoissuesCharge');
724 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
725 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
726 push @not_fines, @man_inv;
729 return $self->lines->search(
731 debit_type_code => { -not_in => \@not_fines }
733 )->total_outstanding;
738 my $lines = $self->lines;
740 Return all credits and debits for the user, outstanding or otherwise
747 return Koha::Account::Lines->search(
749 borrowernumber => $self->{patron_id},
757 my $credits = $self->credits;
759 Return all credits for the user
766 return Koha::Account::Credits->search(
768 borrowernumber => $self->{patron_id}
775 my $debits = $self->debits;
777 Return all debits for the user
784 return Koha::Account::Debits->search(
786 borrowernumber => $self->{patron_id},
791 =head3 reconcile_balance
793 $account->reconcile_balance();
795 Find outstanding credits and use them to pay outstanding debits.
796 Currently, this implicitly uses the 'First In First Out' rule for
797 applying credits against debits.
801 sub reconcile_balance {
804 my $outstanding_debits = $self->outstanding_debits;
805 my $outstanding_credits = $self->outstanding_credits;
807 while ( $outstanding_debits->total_outstanding > 0
808 and my $credit = $outstanding_credits->next )
810 # there's both outstanding debits and credits
811 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
813 $outstanding_debits = $self->outstanding_debits;
826 Kyle M Hall <kyle.m.hall@gmail.com>
827 Tomás Cohen Arazi <tomascohen@gmail.com>
828 Martin Renvoize <martin.renvoize@ptfs-europe.com>