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 $credit_type = $params->{credit_type};
85 my $offset_type = $params->{offset_type} || $type eq 'WRITEOFF' ? 'Writeoff' : 'Payment';
86 my $cash_register = $params->{cash_register};
87 my $item_id = $params->{item_id};
89 my $userenv = C4::Context->userenv;
96 my $patron = Koha::Patrons->find( $self->{patron_id} );
98 my $manager_id = $userenv ? $userenv->{number} : undef;
99 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
100 Koha::Exceptions::Account::RegisterRequired->throw()
101 if ( C4::Context->preference("UseCashRegisters")
102 && defined($payment_type)
103 && ( $payment_type eq 'CASH' )
104 && !defined($cash_register) );
106 my @fines_paid; # List of account lines paid on with this payment
108 # The outcome of any attempted item renewals as a result of fines being
110 my $renew_outcomes = [];
112 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
113 $balance_remaining ||= 0;
117 # We were passed a specific line to pay
118 foreach my $fine ( @$lines ) {
120 $fine->amountoutstanding > $balance_remaining
122 : $fine->amountoutstanding;
124 my $old_amountoutstanding = $fine->amountoutstanding;
125 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
126 $fine->amountoutstanding($new_amountoutstanding)->store();
127 $balance_remaining = $balance_remaining - $amount_to_pay;
129 # Attempt to renew the item associated with this debit if
131 if ($fine->renewable) {
132 # We're ignoring the definition of $interface above, by all
133 # accounts we can't rely on C4::Context::interface, so here
134 # we're only using what we've been explicitly passed
135 my $outcome = $fine->renew_item({ interface => $interface });
136 push @{$renew_outcomes}, $outcome if $outcome;
139 # Same logic exists in Koha::Account::Line::apply
140 if ( C4::Context->preference('MarkLostItemsAsReturned') =~ m|onpayment|
141 && $fine->debit_type_code
142 && $fine->debit_type_code eq 'LOST'
143 && $new_amountoutstanding == 0
145 && !( $credit_type eq 'LOST_FOUND'
146 && $item_id == $fine->itemnumber ) )
148 C4::Circulation::ReturnLostItem( $self->{patron_id},
152 my $account_offset = Koha::Account::Offset->new(
154 debit_id => $fine->id,
155 type => $offset_type,
156 amount => $amount_to_pay * -1,
159 push( @account_offsets, $account_offset );
161 if ( C4::Context->preference("FinesLog") ) {
167 action => 'fee_payment',
168 borrowernumber => $fine->borrowernumber,
169 old_amountoutstanding => $old_amountoutstanding,
170 new_amountoutstanding => 0,
171 amount_paid => $old_amountoutstanding,
172 accountlines_id => $fine->id,
173 manager_id => $manager_id,
179 push( @fines_paid, $fine->id );
183 # Were not passed a specific line to pay, or the payment was for more
184 # than the what was owed on the given line. In that case pay down other
185 # lines with remaining balance.
186 my @outstanding_fines;
187 @outstanding_fines = $self->lines->search(
189 amountoutstanding => { '>' => 0 },
191 ) if $balance_remaining > 0;
193 foreach my $fine (@outstanding_fines) {
195 $fine->amountoutstanding > $balance_remaining
197 : $fine->amountoutstanding;
199 my $old_amountoutstanding = $fine->amountoutstanding;
200 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
203 # If we need to make a note of the item associated with this line,
204 # in order that we can potentially renew it, do so.
205 my $amt = $old_amountoutstanding - $amount_to_pay;
206 if ($fine->renewable) {
207 my $outcome = $fine->renew_item;
208 push @{$renew_outcomes}, $outcome;
211 if ( C4::Context->preference('MarkLostItemsAsReturned') =~ m|onpayment|
212 && $fine->debit_type_code
213 && $fine->debit_type_code eq 'LOST'
214 && $fine->amountoutstanding == 0
216 && !( $credit_type eq 'LOST_FOUND'
217 && $item_id == $fine->itemnumber ) )
219 C4::Circulation::ReturnLostItem( $self->{patron_id},
223 my $account_offset = Koha::Account::Offset->new(
225 debit_id => $fine->id,
226 type => $offset_type,
227 amount => $amount_to_pay * -1,
230 push( @account_offsets, $account_offset );
232 if ( C4::Context->preference("FinesLog") ) {
238 action => "fee_$type",
239 borrowernumber => $fine->borrowernumber,
240 old_amountoutstanding => $old_amountoutstanding,
241 new_amountoutstanding => $fine->amountoutstanding,
242 amount_paid => $amount_to_pay,
243 accountlines_id => $fine->id,
244 manager_id => $manager_id,
250 push( @fines_paid, $fine->id );
253 $balance_remaining = $balance_remaining - $amount_to_pay;
254 last unless $balance_remaining > 0;
257 $description ||= $type eq 'WRITEOFF' ? 'Writeoff' : q{};
259 my $payment = Koha::Account::Line->new(
261 borrowernumber => $self->{patron_id},
262 date => dt_from_string(),
263 amount => 0 - $amount,
264 description => $description,
265 credit_type_code => $credit_type,
266 payment_type => $payment_type,
267 amountoutstanding => 0 - $balance_remaining,
268 manager_id => $manager_id,
269 interface => $interface,
270 branchcode => $library_id,
271 register_id => $cash_register,
273 itemnumber => $item_id,
277 foreach my $o ( @account_offsets ) {
278 $o->credit_id( $payment->id() );
282 C4::Stats::UpdateStats(
284 branch => $library_id,
287 borrowernumber => $self->{patron_id},
291 if ( C4::Context->preference("FinesLog") ) {
297 action => "create_$type",
298 borrowernumber => $self->{patron_id},
299 amount => 0 - $amount,
300 amountoutstanding => 0 - $balance_remaining,
301 credit_type_code => $credit_type,
302 accountlines_paid => \@fines_paid,
303 manager_id => $manager_id,
310 if ( C4::Context->preference('UseEmailReceipts') ) {
312 my $letter = C4::Letters::GetPreparedLetter(
313 module => 'circulation',
314 letter_code => uc("ACCOUNT_$type"),
315 message_transport_type => 'email',
316 lang => $patron->lang,
318 borrowers => $self->{patron_id},
319 branches => $library_id,
323 offsets => \@account_offsets,
328 C4::Letters::EnqueueLetter(
331 borrowernumber => $self->{patron_id},
332 message_transport_type => 'email',
334 ) or warn "can't enqueue letter $letter";
338 return { payment_id => $payment->id, renew_result => $renew_outcomes };
343 This method allows adding credits to a patron's account
345 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
348 description => $description,
351 interface => $interface,
352 library_id => $library_id,
353 payment_type => $payment_type,
354 type => $credit_type,
359 $credit_type can be any of:
372 my ( $self, $params ) = @_;
374 # check for mandatory params
375 my @mandatory = ( 'interface', 'amount' );
376 for my $param (@mandatory) {
377 unless ( defined( $params->{$param} ) ) {
378 Koha::Exceptions::MissingParameter->throw(
379 error => "The $param parameter is mandatory" );
383 # amount should always be passed as a positive value
384 my $amount = $params->{amount} * -1;
385 unless ( $amount < 0 ) {
386 Koha::Exceptions::Account::AmountNotPositive->throw(
387 error => 'Debit amount passed is not positive' );
390 my $description = $params->{description} // q{};
391 my $note = $params->{note} // q{};
392 my $user_id = $params->{user_id};
393 my $interface = $params->{interface};
394 my $library_id = $params->{library_id};
395 my $cash_register = $params->{cash_register};
396 my $payment_type = $params->{payment_type};
397 my $credit_type = $params->{type} || 'PAYMENT';
398 my $item_id = $params->{item_id};
400 Koha::Exceptions::Account::RegisterRequired->throw()
401 if ( C4::Context->preference("UseCashRegisters")
402 && defined($payment_type)
403 && ( $payment_type eq 'CASH' )
404 && !defined($cash_register) );
407 my $schema = Koha::Database->new->schema;
412 # Insert the account line
413 $line = Koha::Account::Line->new(
415 borrowernumber => $self->{patron_id},
418 description => $description,
419 credit_type_code => $credit_type,
420 amountoutstanding => $amount,
421 payment_type => $payment_type,
423 manager_id => $user_id,
424 interface => $interface,
425 branchcode => $library_id,
426 register_id => $cash_register,
427 itemnumber => $item_id,
431 # Record the account offset
432 my $account_offset = Koha::Account::Offset->new(
434 credit_id => $line->id,
435 type => $Koha::Account::offset_type->{$credit_type} // $Koha::Account::offset_type->{CREDIT},
440 C4::Stats::UpdateStats(
442 branch => $library_id,
443 type => lc($credit_type),
445 borrowernumber => $self->{patron_id},
447 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
449 if ( C4::Context->preference("FinesLog") ) {
455 action => "create_$credit_type",
456 borrowernumber => $self->{patron_id},
458 description => $description,
459 amountoutstanding => $amount,
460 credit_type_code => $credit_type,
462 itemnumber => $item_id,
463 manager_id => $user_id,
464 branchcode => $library_id,
474 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
475 if ( $_->broken_fk eq 'credit_type_code' ) {
476 Koha::Exceptions::Account::UnrecognisedType->throw(
477 error => 'Type of credit not recognised' );
490 This method allows adding debits to a patron's account
492 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
495 description => $description,
498 interface => $interface,
499 library_id => $library_id,
502 issue_id => $issue_id
506 $debit_type can be any of:
525 my ( $self, $params ) = @_;
527 # check for mandatory params
528 my @mandatory = ( 'interface', 'type', 'amount' );
529 for my $param (@mandatory) {
530 unless ( defined( $params->{$param} ) ) {
531 Koha::Exceptions::MissingParameter->throw(
532 error => "The $param parameter is mandatory" );
536 # amount should always be a positive value
537 my $amount = $params->{amount};
538 unless ( $amount > 0 ) {
539 Koha::Exceptions::Account::AmountNotPositive->throw(
540 error => 'Debit amount passed is not positive' );
543 my $description = $params->{description} // q{};
544 my $note = $params->{note} // q{};
545 my $user_id = $params->{user_id};
546 my $interface = $params->{interface};
547 my $library_id = $params->{library_id};
548 my $debit_type = $params->{type};
549 my $item_id = $params->{item_id};
550 my $issue_id = $params->{issue_id};
551 my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
554 my $schema = Koha::Database->new->schema;
559 # Insert the account line
560 $line = Koha::Account::Line->new(
562 borrowernumber => $self->{patron_id},
565 description => $description,
566 debit_type_code => $debit_type,
567 amountoutstanding => $amount,
568 payment_type => undef,
570 manager_id => $user_id,
571 interface => $interface,
572 itemnumber => $item_id,
573 issue_id => $issue_id,
574 branchcode => $library_id,
576 $debit_type eq 'OVERDUE'
577 ? ( status => 'UNRETURNED' )
583 # Record the account offset
584 my $account_offset = Koha::Account::Offset->new(
586 debit_id => $line->id,
587 type => $offset_type,
592 if ( C4::Context->preference("FinesLog") ) {
598 action => "create_$debit_type",
599 borrowernumber => $self->{patron_id},
601 description => $description,
602 amountoutstanding => $amount,
603 debit_type_code => $debit_type,
605 itemnumber => $item_id,
606 manager_id => $user_id,
616 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
617 if ( $_->broken_fk eq 'debit_type_code' ) {
618 Koha::Exceptions::Account::UnrecognisedType->throw(
619 error => 'Type of debit not recognised' );
632 my $balance = $self->balance
634 Return the balance (sum of amountoutstanding columns)
640 return $self->lines->total_outstanding;
643 =head3 outstanding_debits
645 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
647 It returns the debit lines with outstanding amounts for the patron.
649 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
650 return a list of Koha::Account::Line objects.
654 sub outstanding_debits {
657 return $self->lines->search(
659 amount => { '>' => 0 },
660 amountoutstanding => { '>' => 0 }
665 =head3 outstanding_credits
667 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
669 It returns the credit lines with outstanding amounts for the patron.
671 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
672 return a list of Koha::Account::Line objects.
676 sub outstanding_credits {
679 return $self->lines->search(
681 amount => { '<' => 0 },
682 amountoutstanding => { '<' => 0 }
687 =head3 non_issues_charges
689 my $non_issues_charges = $self->non_issues_charges
691 Calculates amount immediately owing by the patron - non-issue charges.
693 Charges exempt from non-issue are:
694 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
695 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
696 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
700 sub non_issues_charges {
703 #NOTE: With bug 23049 these preferences could be moved to being attached
704 #to individual debit types to give more flexability and specificity.
706 push @not_fines, 'RESERVE'
707 unless C4::Context->preference('HoldsInNoissuesCharge');
708 push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
709 unless C4::Context->preference('RentalsInNoissuesCharge');
710 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
711 my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
712 push @not_fines, @man_inv;
715 return $self->lines->search(
717 debit_type_code => { -not_in => \@not_fines }
719 )->total_outstanding;
724 my $lines = $self->lines;
726 Return all credits and debits for the user, outstanding or otherwise
733 return Koha::Account::Lines->search(
735 borrowernumber => $self->{patron_id},
740 =head3 reconcile_balance
742 $account->reconcile_balance();
744 Find outstanding credits and use them to pay outstanding debits.
745 Currently, this implicitly uses the 'First In First Out' rule for
746 applying credits against debits.
750 sub reconcile_balance {
753 my $outstanding_debits = $self->outstanding_debits;
754 my $outstanding_credits = $self->outstanding_credits;
756 while ( $outstanding_debits->total_outstanding > 0
757 and my $credit = $outstanding_credits->next )
759 # there's both outstanding debits and credits
760 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
762 $outstanding_debits = $self->outstanding_debits;
778 'CREDIT' => 'Manual Credit',
779 'FORGIVEN' => 'Writeoff',
780 'LOST_FOUND' => 'Lost Item Found',
781 'OVERPAYMENT' => 'Overpayment',
782 'PAYMENT' => 'Payment',
783 'WRITEOFF' => 'Writeoff',
784 'ACCOUNT' => 'Account Fee',
785 'ACCOUNT_RENEW' => 'Account Fee',
786 'RESERVE' => 'Reserve Fee',
787 'PROCESSING' => 'Processing Fee',
788 'LOST' => 'Lost Item',
789 'RENT' => 'Rental Fee',
790 'RENT_DAILY' => 'Rental Fee',
791 'RENT_RENEW' => 'Rental Fee',
792 'RENT_DAILY_RENEW' => 'Rental Fee',
793 'OVERDUE' => 'OVERDUE',
794 'RESERVE_EXPIRED' => 'Hold Expired'
801 Kyle M Hall <kyle.m.hall@gmail.com>
802 Tomás Cohen Arazi <tomascohen@gmail.com>
803 Martin Renvoize <martin.renvoize@ptfs-europe.com>