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},
259 interface => $interface,
261 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
264 'after_account_action',
266 action => "add_credit",
268 type => lc($credit_type),
269 line => $line->get_from_storage, #TODO Seems unneeded
274 if ( C4::Context->preference("FinesLog") ) {
280 action => "create_$credit_type",
281 borrowernumber => $self->{patron_id},
283 description => $description,
284 amountoutstanding => $amount,
285 credit_type_code => $credit_type,
287 itemnumber => $item_id,
288 manager_id => $user_id,
289 branchcode => $library_id,
299 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
300 if ( $_->broken_fk eq 'credit_type_code' ) {
301 Koha::Exceptions::Account::UnrecognisedType->throw(
302 error => 'Type of credit not recognised' );
315 my $credit = $account->payin_amount(
318 type => $credit_type,
319 payment_type => $payment_type,
320 cash_register => $register_id,
321 interface => $interface,
322 library_id => $branchcode,
323 user_id => $staff_id,
324 debits => $debit_lines,
325 description => $description,
330 This method allows an amount to be paid into a patrons account and immediately applied against debts.
332 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
334 $credit_type can be any of:
342 my ( $self, $params ) = @_;
344 # check for mandatory params
345 my @mandatory = ( 'interface', 'amount', 'type' );
346 for my $param (@mandatory) {
347 unless ( defined( $params->{$param} ) ) {
348 Koha::Exceptions::MissingParameter->throw(
349 error => "The $param parameter is mandatory" );
353 # Check for mandatory register
354 Koha::Exceptions::Account::RegisterRequired->throw()
355 if ( C4::Context->preference("UseCashRegisters")
356 && defined( $params->{payment_type} )
357 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
358 && !defined($params->{cash_register}) );
360 # amount should always be passed as a positive value
361 my $amount = $params->{amount};
362 unless ( $amount > 0 ) {
363 Koha::Exceptions::Account::AmountNotPositive->throw(
364 error => 'Payin amount passed is not positive' );
368 my $schema = Koha::Database->new->schema;
373 $credit = $self->add_credit($params);
375 # Offset debts passed first
376 if ( exists( $params->{debits} ) ) {
377 $credit = $credit->apply(
379 debits => $params->{debits}
384 # Offset against remaining balance if AutoReconcile
385 if ( C4::Context->preference("AccountAutoReconcile")
386 && $credit->amountoutstanding != 0 )
388 $credit = $credit->apply(
390 debits => [ $self->outstanding_debits->as_list ]
402 This method allows adding debits to a patron's account
404 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
407 description => $description,
410 interface => $interface,
411 library_id => $library_id,
413 transaction_type => $transaction_type,
414 cash_register => $register_id,
416 issue_id => $issue_id
420 $debit_type can be any of:
440 my ( $self, $params ) = @_;
442 # check for mandatory params
443 my @mandatory = ( 'interface', 'type', 'amount' );
444 for my $param (@mandatory) {
445 unless ( defined( $params->{$param} ) ) {
446 Koha::Exceptions::MissingParameter->throw(
447 error => "The $param parameter is mandatory" );
451 # check for cash register if using cash
452 Koha::Exceptions::Account::RegisterRequired->throw()
453 if ( C4::Context->preference("UseCashRegisters")
454 && defined( $params->{transaction_type} )
455 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
456 && !defined( $params->{cash_register} ) );
458 # amount should always be a positive value
459 my $amount = $params->{amount};
460 unless ( $amount > 0 ) {
461 Koha::Exceptions::Account::AmountNotPositive->throw(
462 error => 'Debit amount passed is not positive' );
465 my $description = $params->{description} // q{};
466 my $note = $params->{note} // q{};
467 my $user_id = $params->{user_id};
468 my $interface = $params->{interface};
469 my $library_id = $params->{library_id};
470 my $cash_register = $params->{cash_register};
471 my $debit_type = $params->{type};
472 my $transaction_type = $params->{transaction_type};
473 my $item_id = $params->{item_id};
474 my $issue_id = $params->{issue_id};
477 my $schema = Koha::Database->new->schema;
482 # Insert the account line
483 $line = Koha::Account::Line->new(
485 borrowernumber => $self->{patron_id},
488 description => $description,
489 debit_type_code => $debit_type,
490 amountoutstanding => $amount,
491 payment_type => $transaction_type,
493 manager_id => $user_id,
494 interface => $interface,
495 itemnumber => $item_id,
496 issue_id => $issue_id,
497 branchcode => $library_id,
498 register_id => $cash_register,
500 $debit_type eq 'OVERDUE'
501 ? ( status => 'UNRETURNED' )
507 # Record the account offset
508 my $account_offset = Koha::Account::Offset->new(
510 debit_id => $line->id,
516 if ( C4::Context->preference("FinesLog") ) {
522 action => "create_$debit_type",
523 borrowernumber => $self->{patron_id},
525 description => $description,
526 amountoutstanding => $amount,
527 debit_type_code => $debit_type,
529 itemnumber => $item_id,
530 manager_id => $user_id,
540 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
541 if ( $_->broken_fk eq 'debit_type_code' ) {
542 Koha::Exceptions::Account::UnrecognisedType->throw(
543 error => 'Type of debit not recognised' );
556 my $debit = $account->payout_amount(
558 payout_type => $payout_type,
559 register_id => $register_id,
560 staff_id => $staff_id,
561 interface => 'intranet',
563 credits => $credit_lines
567 This method allows an amount to be paid out from a patrons account against outstanding credits.
569 $payout_type can be any of the defined payment_types:
574 my ( $self, $params ) = @_;
576 # Check for mandatory parameters
578 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
579 for my $param (@mandatory) {
580 unless ( defined( $params->{$param} ) ) {
581 Koha::Exceptions::MissingParameter->throw(
582 error => "The $param parameter is mandatory" );
586 # Check for mandatory register
587 Koha::Exceptions::Account::RegisterRequired->throw()
588 if ( C4::Context->preference("UseCashRegisters")
589 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
590 && !defined($params->{cash_register}) );
592 # Amount should always be passed as a positive value
593 my $amount = $params->{amount};
594 unless ( $amount > 0 ) {
595 Koha::Exceptions::Account::AmountNotPositive->throw(
596 error => 'Payout amount passed is not positive' );
599 # Amount should always be less than or equal to outstanding credit
601 my $outstanding_credits =
602 exists( $params->{credits} )
604 : $self->outstanding_credits->as_list;
605 for my $credit ( @{$outstanding_credits} ) {
606 $outstanding += $credit->amountoutstanding;
608 $outstanding = $outstanding * -1;
609 Koha::Exceptions::ParameterTooHigh->throw( error =>
610 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
611 ) unless ( $outstanding >= $amount );
614 my $schema = Koha::Database->new->schema;
618 # A 'payout' is a 'debit'
619 $payout = $self->add_debit(
621 amount => $params->{amount},
623 transaction_type => $params->{payout_type},
624 amountoutstanding => $params->{amount},
625 user_id => $params->{staff_id},
626 interface => $params->{interface},
627 branchcode => $params->{branch},
628 cash_register => $params->{cash_register}
632 # Offset against credits
633 for my $credit ( @{$outstanding_credits} ) {
634 $credit->apply( { debits => [$payout] } );
635 $payout->discard_changes;
636 last if $payout->amountoutstanding == 0;
640 $payout->status('PAID')->store;
649 my $balance = $self->balance
651 Return the balance (sum of amountoutstanding columns)
657 return $self->lines->total_outstanding;
660 =head3 outstanding_debits
662 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
664 It returns the debit lines with outstanding amounts for the patron.
666 It returns a Koha::Account::Lines iterator.
670 sub outstanding_debits {
673 return $self->lines->search(
675 amount => { '>' => 0 },
676 amountoutstanding => { '>' => 0 }
681 =head3 outstanding_credits
683 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
685 It returns the credit lines with outstanding amounts for the patron.
687 It returns a Koha::Account::Lines iterator.
691 sub outstanding_credits {
694 return $self->lines->search(
696 amount => { '<' => 0 },
697 amountoutstanding => { '<' => 0 }
702 =head3 non_issues_charges
704 my $non_issues_charges = $self->non_issues_charges
706 Calculates amount immediately owing by the patron - non-issue charges.
708 Charges exempt from non-issue are:
709 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
710 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
711 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
715 sub non_issues_charges {
718 #NOTE: With bug 23049 these preferences could be moved to being attached
719 #to individual debit types to give more flexability and specificity.
721 push @not_fines, 'RESERVE'
722 unless C4::Context->preference('HoldsInNoissuesCharge');
723 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
724 unless C4::Context->preference('RentalsInNoissuesCharge');
725 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
726 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
727 push @not_fines, @man_inv;
730 return $self->lines->search(
732 debit_type_code => { -not_in => \@not_fines }
734 )->total_outstanding;
739 my $lines = $self->lines;
741 Return all credits and debits for the user, outstanding or otherwise
748 return Koha::Account::Lines->search(
750 borrowernumber => $self->{patron_id},
758 my $credits = $self->credits;
760 Return all credits for the user
767 return Koha::Account::Credits->search(
769 borrowernumber => $self->{patron_id}
776 my $debits = $self->debits;
778 Return all debits for the user
785 return Koha::Account::Debits->search(
787 borrowernumber => $self->{patron_id},
792 =head3 reconcile_balance
794 $account->reconcile_balance();
796 Find outstanding credits and use them to pay outstanding debits.
797 Currently, this implicitly uses the 'First In First Out' rule for
798 applying credits against debits.
802 sub reconcile_balance {
805 my $outstanding_debits = $self->outstanding_debits;
806 my $outstanding_credits = $self->outstanding_credits;
808 while ( $outstanding_debits->total_outstanding > 0
809 and my $credit = $outstanding_credits->next )
811 # there's both outstanding debits and credits
812 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
814 $outstanding_debits = $self->outstanding_debits;
827 Kyle M Hall <kyle.m.hall@gmail.com>
828 Tomás Cohen Arazi <tomascohen@gmail.com>
829 Martin Renvoize <martin.renvoize@ptfs-europe.com>