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;
42 Koha::Accounts - Module for managing payments and fees for patrons
47 my ( $class, $params ) = @_;
49 Carp::croak("No patron id passed in!") unless $params->{patron_id};
51 return bless( $params, $class );
56 This method allows payments to be made against fees/fines
58 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
62 description => $description,
63 library_id => $branchcode,
64 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
65 credit_type => $type, # credit_type_code 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 Koha::Exceptions::Account::PaymentTypeRequired->throw()
88 if ( C4::Context->preference("RequirePaymentType")
89 && !defined($payment_type) );
91 my $av = Koha::AuthorisedValues->search_with_library_limits({ category => 'PAYMENT_TYPE', authorised_value => $payment_type });
93 if ( !$av->count && C4::Context->preference("RequirePaymentType")) {
94 Koha::Exceptions::Account::InvalidPaymentType->throw(
95 error => 'Invalid payment type'
99 my $manager_id = $userenv ? $userenv->{number} : undef;
100 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
101 my $payment = $self->payin_amount(
103 interface => $interface,
106 payment_type => $payment_type,
107 cash_register => $cash_register,
108 user_id => $manager_id,
109 library_id => $library_id,
111 description => $description,
117 # NOTE: Pay historically always applied as much credit as it could to all
118 # existing outstanding debits, whether passed specific debits or otherwise.
119 if ( $payment->amountoutstanding ) {
122 { debits => [ $self->outstanding_debits->as_list ] } );
125 my $patron = Koha::Patrons->find( $self->{patron_id} );
126 my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
127 if ( C4::Context->preference('UseEmailReceipts') ) {
129 my $letter = C4::Letters::GetPreparedLetter(
130 module => 'circulation',
131 letter_code => uc("ACCOUNT_$type"),
132 message_transport_type => 'email',
133 lang => $patron->lang,
135 borrowers => $self->{patron_id},
136 branches => $library_id,
140 offsets => \@account_offsets,
145 C4::Letters::EnqueueLetter(
148 borrowernumber => $self->{patron_id},
149 message_transport_type => 'email',
151 ) or warn "can't enqueue letter $letter";
155 my $renew_outcomes = [];
156 for my $message ( @{$payment->object_messages} ) {
157 push @{$renew_outcomes}, $message->payload;
160 return { payment_id => $payment->id, renew_result => $renew_outcomes };
165 This method allows adding credits to a patron's account
167 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
170 description => $description,
173 interface => $interface,
174 library_id => $library_id,
175 payment_type => $payment_type,
176 type => $credit_type,
181 $credit_type can be any of:
195 my ( $self, $params ) = @_;
197 # check for mandatory params
198 my @mandatory = ( 'interface', 'amount' );
199 for my $param (@mandatory) {
200 unless ( defined( $params->{$param} ) ) {
201 Koha::Exceptions::MissingParameter->throw(
202 error => "The $param parameter is mandatory" );
206 # amount should always be passed as a positive value
207 my $amount = $params->{amount} * -1;
208 unless ( $amount < 0 ) {
209 Koha::Exceptions::Account::AmountNotPositive->throw(
210 error => 'Debit amount passed is not positive' );
213 my $description = $params->{description} // q{};
214 my $note = $params->{note} // q{};
215 my $user_id = $params->{user_id};
216 my $interface = $params->{interface};
217 my $library_id = $params->{library_id};
218 my $cash_register = $params->{cash_register};
219 my $payment_type = $params->{payment_type};
220 my $credit_type = $params->{type} || 'PAYMENT';
221 my $item_id = $params->{item_id};
223 Koha::Exceptions::Account::RegisterRequired->throw()
224 if ( C4::Context->preference("UseCashRegisters")
225 && defined($payment_type)
226 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
227 && !defined($cash_register) );
230 my $schema = Koha::Database->new->schema;
235 # Insert the account line
236 $line = Koha::Account::Line->new(
238 borrowernumber => $self->{patron_id},
241 description => $description,
242 credit_type_code => $credit_type,
243 amountoutstanding => $amount,
244 payment_type => $payment_type,
246 manager_id => $user_id,
247 interface => $interface,
248 branchcode => $library_id,
249 register_id => $cash_register,
250 itemnumber => $item_id,
254 # Record the account offset
255 my $account_offset = Koha::Account::Offset->new(
257 credit_id => $line->id,
259 amount => $amount * -1
263 C4::Stats::UpdateStats(
265 branch => $library_id,
266 type => lc($credit_type),
268 borrowernumber => $self->{patron_id},
270 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
273 'after_account_action',
275 action => "add_credit",
277 type => lc($credit_type),
278 line => $line->get_from_storage, #TODO Seems unneeded
283 if ( C4::Context->preference("FinesLog") ) {
289 action => "create_$credit_type",
290 borrowernumber => $self->{patron_id},
292 description => $description,
293 amountoutstanding => $amount,
294 credit_type_code => $credit_type,
296 itemnumber => $item_id,
297 manager_id => $user_id,
298 branchcode => $library_id,
308 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
309 if ( $_->broken_fk eq 'credit_type_code' ) {
310 Koha::Exceptions::Account::UnrecognisedType->throw(
311 error => 'Type of credit not recognised' );
324 my $credit = $account->payin_amount(
327 type => $credit_type,
328 payment_type => $payment_type,
329 cash_register => $register_id,
330 interface => $interface,
331 library_id => $branchcode,
332 user_id => $staff_id,
333 debits => $debit_lines,
334 description => $description,
339 This method allows an amount to be paid into a patrons account and immediately applied against debts.
341 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
343 $credit_type can be any of:
351 my ( $self, $params ) = @_;
353 # check for mandatory params
354 my @mandatory = ( 'interface', 'amount', 'type' );
355 for my $param (@mandatory) {
356 unless ( defined( $params->{$param} ) ) {
357 Koha::Exceptions::MissingParameter->throw(
358 error => "The $param parameter is mandatory" );
362 # Check for mandatory register
363 Koha::Exceptions::Account::RegisterRequired->throw()
364 if ( C4::Context->preference("UseCashRegisters")
365 && defined( $params->{payment_type} )
366 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
367 && !defined($params->{cash_register}) );
369 # amount should always be passed as a positive value
370 my $amount = $params->{amount};
371 unless ( $amount > 0 ) {
372 Koha::Exceptions::Account::AmountNotPositive->throw(
373 error => 'Payin amount passed is not positive' );
377 my $schema = Koha::Database->new->schema;
382 $credit = $self->add_credit($params);
384 # Offset debts passed first
385 if ( exists( $params->{debits} ) ) {
386 $credit = $credit->apply(
388 debits => $params->{debits}
393 # Offset against remaining balance if AutoReconcile
394 if ( C4::Context->preference("AccountAutoReconcile")
395 && $credit->amountoutstanding != 0 )
397 $credit = $credit->apply(
399 debits => [ $self->outstanding_debits->as_list ]
411 This method allows adding debits to a patron's account
413 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
416 description => $description,
419 interface => $interface,
420 library_id => $library_id,
422 transaction_type => $transaction_type,
423 cash_register => $register_id,
425 issue_id => $issue_id
429 $debit_type can be any of:
449 my ( $self, $params ) = @_;
451 # check for mandatory params
452 my @mandatory = ( 'interface', 'type', 'amount' );
453 for my $param (@mandatory) {
454 unless ( defined( $params->{$param} ) ) {
455 Koha::Exceptions::MissingParameter->throw(
456 error => "The $param parameter is mandatory" );
460 # check for cash register if using cash
461 Koha::Exceptions::Account::RegisterRequired->throw()
462 if ( C4::Context->preference("UseCashRegisters")
463 && defined( $params->{transaction_type} )
464 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
465 && !defined( $params->{cash_register} ) );
467 # amount should always be a positive value
468 my $amount = $params->{amount};
469 unless ( $amount > 0 ) {
470 Koha::Exceptions::Account::AmountNotPositive->throw(
471 error => 'Debit amount passed is not positive' );
474 my $description = $params->{description} // q{};
475 my $note = $params->{note} // q{};
476 my $user_id = $params->{user_id};
477 my $interface = $params->{interface};
478 my $library_id = $params->{library_id};
479 my $cash_register = $params->{cash_register};
480 my $debit_type = $params->{type};
481 my $transaction_type = $params->{transaction_type};
482 my $item_id = $params->{item_id};
483 my $issue_id = $params->{issue_id};
486 my $schema = Koha::Database->new->schema;
491 # Insert the account line
492 $line = Koha::Account::Line->new(
494 borrowernumber => $self->{patron_id},
497 description => $description,
498 debit_type_code => $debit_type,
499 amountoutstanding => $amount,
500 payment_type => $transaction_type,
502 manager_id => $user_id,
503 interface => $interface,
504 itemnumber => $item_id,
505 issue_id => $issue_id,
506 branchcode => $library_id,
507 register_id => $cash_register,
509 $debit_type eq 'OVERDUE'
510 ? ( status => 'UNRETURNED' )
516 # Record the account offset
517 my $account_offset = Koha::Account::Offset->new(
519 debit_id => $line->id,
525 if ( C4::Context->preference("FinesLog") ) {
531 action => "create_$debit_type",
532 borrowernumber => $self->{patron_id},
534 description => $description,
535 amountoutstanding => $amount,
536 debit_type_code => $debit_type,
538 itemnumber => $item_id,
539 manager_id => $user_id,
549 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
550 if ( $_->broken_fk eq 'debit_type_code' ) {
551 Koha::Exceptions::Account::UnrecognisedType->throw(
552 error => 'Type of debit not recognised' );
565 my $debit = $account->payout_amount(
567 payout_type => $payout_type,
568 register_id => $register_id,
569 staff_id => $staff_id,
570 interface => 'intranet',
572 credits => $credit_lines
576 This method allows an amount to be paid out from a patrons account against outstanding credits.
578 $payout_type can be any of the defined payment_types:
583 my ( $self, $params ) = @_;
585 # Check for mandatory parameters
587 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
588 for my $param (@mandatory) {
589 unless ( defined( $params->{$param} ) ) {
590 Koha::Exceptions::MissingParameter->throw(
591 error => "The $param parameter is mandatory" );
595 # Check for mandatory register
596 Koha::Exceptions::Account::RegisterRequired->throw()
597 if ( C4::Context->preference("UseCashRegisters")
598 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
599 && !defined($params->{cash_register}) );
601 # Amount should always be passed as a positive value
602 my $amount = $params->{amount};
603 unless ( $amount > 0 ) {
604 Koha::Exceptions::Account::AmountNotPositive->throw(
605 error => 'Payout amount passed is not positive' );
608 # Amount should always be less than or equal to outstanding credit
610 my $outstanding_credits =
611 exists( $params->{credits} )
613 : $self->outstanding_credits->as_list;
614 for my $credit ( @{$outstanding_credits} ) {
615 $outstanding += $credit->amountoutstanding;
617 $outstanding = $outstanding * -1;
618 Koha::Exceptions::ParameterTooHigh->throw( error =>
619 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
620 ) unless ( $outstanding >= $amount );
623 my $schema = Koha::Database->new->schema;
627 # A 'payout' is a 'debit'
628 $payout = $self->add_debit(
630 amount => $params->{amount},
632 transaction_type => $params->{payout_type},
633 amountoutstanding => $params->{amount},
634 user_id => $params->{staff_id},
635 interface => $params->{interface},
636 branchcode => $params->{branch},
637 cash_register => $params->{cash_register}
641 # Offset against credits
642 for my $credit ( @{$outstanding_credits} ) {
643 $credit->apply( { debits => [$payout] } );
644 $payout->discard_changes;
645 last if $payout->amountoutstanding == 0;
649 $payout->status('PAID')->store;
658 my $balance = $self->balance
660 Return the balance (sum of amountoutstanding columns)
666 return $self->lines->total_outstanding;
669 =head3 outstanding_debits
671 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
673 It returns the debit lines with outstanding amounts for the patron.
675 It returns a Koha::Account::Lines iterator.
679 sub outstanding_debits {
682 return $self->lines->search(
684 amount => { '>' => 0 },
685 amountoutstanding => { '>' => 0 }
690 =head3 outstanding_credits
692 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
694 It returns the credit lines with outstanding amounts for the patron.
696 It returns a Koha::Account::Lines iterator.
700 sub outstanding_credits {
703 return $self->lines->search(
705 amount => { '<' => 0 },
706 amountoutstanding => { '<' => 0 }
711 =head3 non_issues_charges
713 my $non_issues_charges = $self->non_issues_charges
715 Calculates amount immediately owing by the patron - non-issue charges.
717 Charges exempt from non-issue are:
718 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
719 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
720 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
724 sub non_issues_charges {
727 #NOTE: With bug 23049 these preferences could be moved to being attached
728 #to individual debit types to give more flexability and specificity.
730 push @not_fines, 'RESERVE'
731 unless C4::Context->preference('HoldsInNoissuesCharge');
732 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
733 unless C4::Context->preference('RentalsInNoissuesCharge');
734 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
735 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
736 push @not_fines, @man_inv;
739 return $self->lines->search(
741 debit_type_code => { -not_in => \@not_fines }
743 )->total_outstanding;
748 my $lines = $self->lines;
750 Return all credits and debits for the user, outstanding or otherwise
757 return Koha::Account::Lines->search(
759 borrowernumber => $self->{patron_id},
764 =head3 reconcile_balance
766 $account->reconcile_balance();
768 Find outstanding credits and use them to pay outstanding debits.
769 Currently, this implicitly uses the 'First In First Out' rule for
770 applying credits against debits.
774 sub reconcile_balance {
777 my $outstanding_debits = $self->outstanding_debits;
778 my $outstanding_credits = $self->outstanding_credits;
780 while ( $outstanding_debits->total_outstanding > 0
781 and my $credit = $outstanding_credits->next )
783 # there's both outstanding debits and credits
784 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
786 $outstanding_debits = $self->outstanding_debits;
799 Kyle M Hall <kyle.m.hall@gmail.com>
800 Tomás Cohen Arazi <tomascohen@gmail.com>
801 Martin Renvoize <martin.renvoize@ptfs-europe.com>