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 );
26 use C4::Circulation qw( ReturnLostItem );
28 use C4::Log qw( logaction );
29 use C4::Stats qw( UpdateStats );
32 use Koha::Account::Lines;
33 use Koha::Account::Offsets;
34 use Koha::DateUtils qw( dt_from_string );
36 use Koha::Exceptions::Account;
40 Koha::Accounts - Module for managing payments and fees for patrons
45 my ( $class, $params ) = @_;
47 Carp::croak("No patron id passed in!") unless $params->{patron_id};
49 return bless( $params, $class );
54 This method allows payments to be made against fees/fines
56 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
60 description => $description,
61 library_id => $branchcode,
62 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
63 account_type => $type, # accounttype code
64 offset_type => $offset_type, # offset type code
71 my ( $self, $params ) = @_;
73 my $amount = $params->{amount};
74 my $description = $params->{description};
75 my $note = $params->{note} || q{};
76 my $library_id = $params->{library_id};
77 my $lines = $params->{lines};
78 my $type = $params->{type} || 'payment';
79 my $payment_type = $params->{payment_type} || undef;
80 my $account_type = $params->{account_type};
81 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
82 my $cash_register = $params->{cash_register};
84 my $userenv = C4::Context->userenv;
86 my $patron = Koha::Patrons->find( $self->{patron_id} );
88 my $manager_id = $userenv ? $userenv->{number} : 0;
89 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
90 Koha::Exceptions::Account::RegisterRequired->throw()
91 if ( C4::Context->preference("UseCashRegisters")
92 && !defined($cash_register)
93 && ( $interface ne 'opac' ) );
95 my @fines_paid; # List of account lines paid on with this payment
97 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
98 $balance_remaining ||= 0;
102 # We were passed a specific line to pay
103 foreach my $fine ( @$lines ) {
105 $fine->amountoutstanding > $balance_remaining
107 : $fine->amountoutstanding;
109 my $old_amountoutstanding = $fine->amountoutstanding;
110 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
111 $fine->amountoutstanding($new_amountoutstanding)->store();
112 $balance_remaining = $balance_remaining - $amount_to_pay;
114 # Same logic exists in Koha::Account::Line::apply
115 if ( $new_amountoutstanding == 0
117 && $fine->accounttype
118 && ( $fine->accounttype eq 'LOST' ) )
120 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
123 my $account_offset = Koha::Account::Offset->new(
125 debit_id => $fine->id,
126 type => $offset_type,
127 amount => $amount_to_pay * -1,
130 push( @account_offsets, $account_offset );
132 if ( C4::Context->preference("FinesLog") ) {
138 action => 'fee_payment',
139 borrowernumber => $fine->borrowernumber,
140 old_amountoutstanding => $old_amountoutstanding,
141 new_amountoutstanding => 0,
142 amount_paid => $old_amountoutstanding,
143 accountlines_id => $fine->id,
144 manager_id => $manager_id,
150 push( @fines_paid, $fine->id );
154 # Were not passed a specific line to pay, or the payment was for more
155 # than the what was owed on the given line. In that case pay down other
156 # lines with remaining balance.
157 my @outstanding_fines;
158 @outstanding_fines = $self->lines->search(
160 amountoutstanding => { '>' => 0 },
162 ) if $balance_remaining > 0;
164 foreach my $fine (@outstanding_fines) {
166 $fine->amountoutstanding > $balance_remaining
168 : $fine->amountoutstanding;
170 my $old_amountoutstanding = $fine->amountoutstanding;
171 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
174 if ( $fine->amountoutstanding == 0
176 && $fine->accounttype
177 && ( $fine->accounttype eq 'LOST' ) )
179 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
182 my $account_offset = Koha::Account::Offset->new(
184 debit_id => $fine->id,
185 type => $offset_type,
186 amount => $amount_to_pay * -1,
189 push( @account_offsets, $account_offset );
191 if ( C4::Context->preference("FinesLog") ) {
197 action => "fee_$type",
198 borrowernumber => $fine->borrowernumber,
199 old_amountoutstanding => $old_amountoutstanding,
200 new_amountoutstanding => $fine->amountoutstanding,
201 amount_paid => $amount_to_pay,
202 accountlines_id => $fine->id,
203 manager_id => $manager_id,
209 push( @fines_paid, $fine->id );
212 $balance_remaining = $balance_remaining - $amount_to_pay;
213 last unless $balance_remaining > 0;
221 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
223 my $payment = Koha::Account::Line->new(
225 borrowernumber => $self->{patron_id},
226 date => dt_from_string(),
227 amount => 0 - $amount,
228 description => $description,
229 accounttype => $account_type,
230 payment_type => $payment_type,
231 amountoutstanding => 0 - $balance_remaining,
232 manager_id => $manager_id,
233 interface => $interface,
234 branchcode => $library_id,
235 register_id => $cash_register,
240 foreach my $o ( @account_offsets ) {
241 $o->credit_id( $payment->id() );
247 branch => $library_id,
250 borrowernumber => $self->{patron_id},
254 if ( C4::Context->preference("FinesLog") ) {
260 action => "create_$type",
261 borrowernumber => $self->{patron_id},
262 amount => 0 - $amount,
263 amountoutstanding => 0 - $balance_remaining,
264 accounttype => $account_type,
265 accountlines_paid => \@fines_paid,
266 manager_id => $manager_id,
273 if ( C4::Context->preference('UseEmailReceipts') ) {
275 my $letter = C4::Letters::GetPreparedLetter(
276 module => 'circulation',
277 letter_code => uc("ACCOUNT_$type"),
278 message_transport_type => 'email',
279 lang => $patron->lang,
281 borrowers => $self->{patron_id},
282 branches => $self->{library_id},
286 offsets => \@account_offsets,
291 C4::Letters::EnqueueLetter(
294 borrowernumber => $self->{patron_id},
295 message_transport_type => 'email',
297 ) or warn "can't enqueue letter $letter";
306 This method allows adding credits to a patron's account
308 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
311 description => $description,
314 interface => $interface,
315 library_id => $library_id,
316 payment_type => $payment_type,
317 type => $credit_type,
322 $credit_type can be any of:
333 my ( $self, $params ) = @_;
335 # amount is passed as a positive value, but we store credit as negative values
336 my $amount = $params->{amount} * -1;
337 my $description = $params->{description} // q{};
338 my $note = $params->{note} // q{};
339 my $user_id = $params->{user_id};
340 my $interface = $params->{interface};
341 my $library_id = $params->{library_id};
342 my $cash_register = $params->{cash_register};
343 my $payment_type = $params->{payment_type};
344 my $type = $params->{type} || 'payment';
345 my $item_id = $params->{item_id};
347 unless ( $interface ) {
348 Koha::Exceptions::MissingParameter->throw(
349 error => 'The interface parameter is mandatory'
353 Koha::Exceptions::Account::RegisterRequired->throw()
354 if ( C4::Context->preference("UseCashRegisters")
355 && !defined($cash_register)
356 && ( $payment_type eq 'CASH' ) );
358 my $schema = Koha::Database->new->schema;
360 my $account_type = $Koha::Account::account_type_credit->{$type};
366 # Insert the account line
367 $line = Koha::Account::Line->new(
368 { borrowernumber => $self->{patron_id},
371 description => $description,
372 accounttype => $account_type,
373 amountoutstanding => $amount,
374 payment_type => $payment_type,
376 manager_id => $user_id,
377 interface => $interface,
378 branchcode => $library_id,
379 register_id => $cash_register,
380 itemnumber => $item_id,
384 # Record the account offset
385 my $account_offset = Koha::Account::Offset->new(
386 { credit_id => $line->id,
387 type => $Koha::Account::offset_type->{$type},
393 { branch => $library_id,
396 borrowernumber => $self->{patron_id},
398 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
400 if ( C4::Context->preference("FinesLog") ) {
405 { action => "create_$type",
406 borrowernumber => $self->{patron_id},
408 description => $description,
409 amountoutstanding => $amount,
410 accounttype => $account_type,
412 itemnumber => $item_id,
413 manager_id => $user_id,
414 branchcode => $library_id,
428 This method allows adding debits to a patron's account
430 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
433 description => $description,
436 interface => $interface,
437 library_id => $library_id,
440 issue_id => $issue_id
444 $debit_type can be any of:
463 my ( $self, $params ) = @_;
465 # amount should always be a positive value
466 my $amount = $params->{amount};
468 unless ( $amount > 0 ) {
469 Koha::Exceptions::Account::AmountNotPositive->throw(
470 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 $type = $params->{type};
480 my $item_id = $params->{item_id};
481 my $issue_id = $params->{issue_id};
483 unless ( $interface ) {
484 Koha::Exceptions::MissingParameter->throw(
485 error => 'The interface parameter is mandatory'
489 my $schema = Koha::Database->new->schema;
491 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
492 Koha::Exceptions::Account::UnrecognisedType->throw(
493 error => 'Type of debit not recognised'
497 my $account_type = $Koha::Account::account_type_debit->{$type};
504 # Insert the account line
505 $line = Koha::Account::Line->new(
506 { borrowernumber => $self->{patron_id},
509 description => $description,
510 accounttype => $account_type,
511 amountoutstanding => $amount,
512 payment_type => undef,
514 manager_id => $user_id,
515 interface => $interface,
516 itemnumber => $item_id,
517 issue_id => $issue_id,
518 branchcode => $library_id,
519 ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : ()),
523 # Record the account offset
524 my $account_offset = Koha::Account::Offset->new(
525 { debit_id => $line->id,
526 type => $Koha::Account::offset_type->{$type},
531 if ( C4::Context->preference("FinesLog") ) {
536 { action => "create_$type",
537 borrowernumber => $self->{patron_id},
539 description => $description,
540 amountoutstanding => $amount,
541 accounttype => $account_type,
543 itemnumber => $item_id,
544 manager_id => $user_id,
558 my $balance = $self->balance
560 Return the balance (sum of amountoutstanding columns)
566 return $self->lines->total_outstanding;
569 =head3 outstanding_debits
571 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
573 It returns the debit lines with outstanding amounts for the patron.
575 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
576 return a list of Koha::Account::Line objects.
580 sub outstanding_debits {
583 return $self->lines->search(
585 amount => { '>' => 0 },
586 amountoutstanding => { '>' => 0 }
591 =head3 outstanding_credits
593 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
595 It returns the credit lines with outstanding amounts for the patron.
597 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
598 return a list of Koha::Account::Line objects.
602 sub outstanding_credits {
605 return $self->lines->search(
607 amount => { '<' => 0 },
608 amountoutstanding => { '<' => 0 }
613 =head3 non_issues_charges
615 my $non_issues_charges = $self->non_issues_charges
617 Calculates amount immediately owing by the patron - non-issue charges.
619 Charges exempt from non-issue are:
620 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
621 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
622 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
626 sub non_issues_charges {
629 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
630 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
633 push @not_fines, 'Res'
634 unless C4::Context->preference('HoldsInNoissuesCharge');
635 push @not_fines, 'Rent'
636 unless C4::Context->preference('RentalsInNoissuesCharge');
637 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
638 my $dbh = C4::Context->dbh;
641 $dbh->selectcol_arrayref(q|
642 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
646 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
648 return $self->lines->search(
650 accounttype => { -not_in => \@not_fines }
652 )->total_outstanding;
657 my $lines = $self->lines;
659 Return all credits and debits for the user, outstanding or otherwise
666 return Koha::Account::Lines->search(
668 borrowernumber => $self->{patron_id},
673 =head3 reconcile_balance
675 $account->reconcile_balance();
677 Find outstanding credits and use them to pay outstanding debits.
678 Currently, this implicitly uses the 'First In First Out' rule for
679 applying credits against debits.
683 sub reconcile_balance {
686 my $outstanding_debits = $self->outstanding_debits;
687 my $outstanding_credits = $self->outstanding_credits;
689 while ( $outstanding_debits->total_outstanding > 0
690 and my $credit = $outstanding_credits->next )
692 # there's both outstanding debits and credits
693 $credit->apply( { debits => [ $outstanding_debits->as_list ] } ); # applying credit, no special offset
695 $outstanding_debits = $self->outstanding_debits;
711 'credit' => 'Manual Credit',
712 'forgiven' => 'Writeoff',
713 'lost_item_return' => 'Lost Item',
714 'payment' => 'Payment',
715 'writeoff' => 'Writeoff',
716 'account' => 'Account Fee',
717 'account_renew' => 'Account Fee',
718 'reserve' => 'Reserve Fee',
719 'processing' => 'Processing Fee',
720 'lost_item' => 'Lost Item',
721 'rent' => 'Rental Fee',
722 'rent_daily' => 'Rental Fee',
723 'rent_renew' => 'Rental Fee',
724 'rent_daily_renew' => 'Rental Fee',
725 'overdue' => 'OVERDUE',
726 'manual_debit' => 'Manual Debit',
727 'hold_expired' => 'Hold Expired'
730 =head3 $account_type_credit
734 our $account_type_credit = {
737 'lost_item_return' => 'LOST_RETURN',
742 =head3 $account_type_debit
746 our $account_type_debit = {
747 'account' => 'ACCOUNT',
748 'account_renew' => 'ACCOUNT_RENEW',
749 'overdue' => 'OVERDUE',
750 'lost_item' => 'LOST',
753 'processing' => 'PF',
755 'rent_daily' => 'RENT_DAILY',
756 'rent_renew' => 'RENT_RENEW',
757 'rent_daily_renew' => 'RENT_DAILY_RENEW',
759 'manual_debit' => 'M',
760 'hold_expired' => 'HE'
767 Kyle M Hall <kyle.m.hall@gmail.com>
768 Tomás Cohen Arazi <tomascohen@gmail.com>
769 Martin Renvoize <martin.renvoize@ptfs-europe.com>