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';
83 my $userenv = C4::Context->userenv;
85 my $patron = Koha::Patrons->find( $self->{patron_id} );
87 my $manager_id = $userenv ? $userenv->{number} : 0;
88 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
90 my @fines_paid; # List of account lines paid on with this payment
92 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
93 $balance_remaining ||= 0;
97 # We were passed a specific line to pay
98 foreach my $fine ( @$lines ) {
100 $fine->amountoutstanding > $balance_remaining
102 : $fine->amountoutstanding;
104 my $old_amountoutstanding = $fine->amountoutstanding;
105 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
106 $fine->amountoutstanding($new_amountoutstanding)->store();
107 $balance_remaining = $balance_remaining - $amount_to_pay;
109 # Same logic exists in Koha::Account::Line::apply
110 if ( $new_amountoutstanding == 0
112 && $fine->accounttype
113 && ( $fine->accounttype eq 'LOST' ) )
115 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
118 my $account_offset = Koha::Account::Offset->new(
120 debit_id => $fine->id,
121 type => $offset_type,
122 amount => $amount_to_pay * -1,
125 push( @account_offsets, $account_offset );
127 if ( C4::Context->preference("FinesLog") ) {
133 action => 'fee_payment',
134 borrowernumber => $fine->borrowernumber,
135 old_amountoutstanding => $old_amountoutstanding,
136 new_amountoutstanding => 0,
137 amount_paid => $old_amountoutstanding,
138 accountlines_id => $fine->id,
139 manager_id => $manager_id,
145 push( @fines_paid, $fine->id );
149 # Were not passed a specific line to pay, or the payment was for more
150 # than the what was owed on the given line. In that case pay down other
151 # lines with remaining balance.
152 my @outstanding_fines;
153 @outstanding_fines = $self->lines->search(
155 amountoutstanding => { '>' => 0 },
157 ) if $balance_remaining > 0;
159 foreach my $fine (@outstanding_fines) {
161 $fine->amountoutstanding > $balance_remaining
163 : $fine->amountoutstanding;
165 my $old_amountoutstanding = $fine->amountoutstanding;
166 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
169 if ( $fine->amountoutstanding == 0
171 && $fine->accounttype
172 && ( $fine->accounttype eq 'LOST' ) )
174 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
177 my $account_offset = Koha::Account::Offset->new(
179 debit_id => $fine->id,
180 type => $offset_type,
181 amount => $amount_to_pay * -1,
184 push( @account_offsets, $account_offset );
186 if ( C4::Context->preference("FinesLog") ) {
192 action => "fee_$type",
193 borrowernumber => $fine->borrowernumber,
194 old_amountoutstanding => $old_amountoutstanding,
195 new_amountoutstanding => $fine->amountoutstanding,
196 amount_paid => $amount_to_pay,
197 accountlines_id => $fine->id,
198 manager_id => $manager_id,
204 push( @fines_paid, $fine->id );
207 $balance_remaining = $balance_remaining - $amount_to_pay;
208 last unless $balance_remaining > 0;
216 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
218 my $payment = Koha::Account::Line->new(
220 borrowernumber => $self->{patron_id},
221 date => dt_from_string(),
222 amount => 0 - $amount,
223 description => $description,
224 accounttype => $account_type,
225 payment_type => $payment_type,
226 amountoutstanding => 0 - $balance_remaining,
227 manager_id => $manager_id,
228 interface => $interface,
229 branchcode => $library_id,
234 foreach my $o ( @account_offsets ) {
235 $o->credit_id( $payment->id() );
241 branch => $library_id,
244 borrowernumber => $self->{patron_id},
248 if ( C4::Context->preference("FinesLog") ) {
254 action => "create_$type",
255 borrowernumber => $self->{patron_id},
256 amount => 0 - $amount,
257 amountoutstanding => 0 - $balance_remaining,
258 accounttype => $account_type,
259 accountlines_paid => \@fines_paid,
260 manager_id => $manager_id,
267 if ( C4::Context->preference('UseEmailReceipts') ) {
269 my $letter = C4::Letters::GetPreparedLetter(
270 module => 'circulation',
271 letter_code => uc("ACCOUNT_$type"),
272 message_transport_type => 'email',
273 lang => $patron->lang,
275 borrowers => $self->{patron_id},
276 branches => $self->{library_id},
280 offsets => \@account_offsets,
285 C4::Letters::EnqueueLetter(
288 borrowernumber => $self->{patron_id},
289 message_transport_type => 'email',
291 ) or warn "can't enqueue letter $letter";
300 This method allows adding credits to a patron's account
302 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
305 description => $description,
308 interface => $interface,
309 library_id => $library_id,
310 payment_type => $payment_type,
311 type => $credit_type,
316 $credit_type can be any of:
327 my ( $self, $params ) = @_;
329 # amount is passed as a positive value, but we store credit as negative values
330 my $amount = $params->{amount} * -1;
331 my $description = $params->{description} // q{};
332 my $note = $params->{note} // q{};
333 my $user_id = $params->{user_id};
334 my $interface = $params->{interface};
335 my $library_id = $params->{library_id};
336 my $payment_type = $params->{payment_type};
337 my $type = $params->{type} || 'payment';
338 my $item_id = $params->{item_id};
340 unless ( $interface ) {
341 Koha::Exceptions::MissingParameter->throw(
342 error => 'The interface parameter is mandatory'
346 my $schema = Koha::Database->new->schema;
348 my $account_type = $Koha::Account::account_type_credit->{$type};
354 # Insert the account line
355 $line = Koha::Account::Line->new(
356 { borrowernumber => $self->{patron_id},
359 description => $description,
360 accounttype => $account_type,
361 amountoutstanding => $amount,
362 payment_type => $payment_type,
364 manager_id => $user_id,
365 interface => $interface,
366 branchcode => $library_id,
367 itemnumber => $item_id,
371 # Record the account offset
372 my $account_offset = Koha::Account::Offset->new(
373 { credit_id => $line->id,
374 type => $Koha::Account::offset_type->{$type},
380 { branch => $library_id,
383 borrowernumber => $self->{patron_id},
385 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
387 if ( C4::Context->preference("FinesLog") ) {
392 { action => "create_$type",
393 borrowernumber => $self->{patron_id},
395 description => $description,
396 amountoutstanding => $amount,
397 accounttype => $account_type,
399 itemnumber => $item_id,
400 manager_id => $user_id,
401 branchcode => $library_id,
415 This method allows adding debits to a patron's account
417 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
420 description => $description,
423 interface => $interface,
424 library_id => $library_id,
427 issue_id => $issue_id
431 $debit_type can be any of:
449 my ( $self, $params ) = @_;
451 # amount should always be a positive value
452 my $amount = $params->{amount};
454 unless ( $amount > 0 ) {
455 Koha::Exceptions::Account::AmountNotPositive->throw(
456 error => 'Debit amount passed is not positive'
460 my $description = $params->{description} // q{};
461 my $note = $params->{note} // q{};
462 my $user_id = $params->{user_id};
463 my $interface = $params->{interface};
464 my $library_id = $params->{library_id};
465 my $type = $params->{type};
466 my $item_id = $params->{item_id};
467 my $issue_id = $params->{issue_id};
469 unless ( $interface ) {
470 Koha::Exceptions::MissingParameter->throw(
471 error => 'The interface parameter is mandatory'
475 my $schema = Koha::Database->new->schema;
477 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
478 Koha::Exceptions::Account::UnrecognisedType->throw(
479 error => 'Type of debit not recognised'
483 my $account_type = $Koha::Account::account_type_debit->{$type};
490 # Insert the account line
491 $line = Koha::Account::Line->new(
492 { borrowernumber => $self->{patron_id},
495 description => $description,
496 accounttype => $account_type,
497 amountoutstanding => $amount,
498 payment_type => undef,
500 manager_id => $user_id,
501 interface => $interface,
502 itemnumber => $item_id,
503 issue_id => $issue_id,
504 branchcode => $library_id,
505 ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : ()),
509 # Record the account offset
510 my $account_offset = Koha::Account::Offset->new(
511 { debit_id => $line->id,
512 type => $Koha::Account::offset_type->{$type},
517 if ( C4::Context->preference("FinesLog") ) {
522 { action => "create_$type",
523 borrowernumber => $self->{patron_id},
525 description => $description,
526 amountoutstanding => $amount,
527 accounttype => $account_type,
529 itemnumber => $item_id,
530 manager_id => $user_id,
544 my $balance = $self->balance
546 Return the balance (sum of amountoutstanding columns)
552 return $self->lines->total_outstanding;
555 =head3 outstanding_debits
557 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
559 It returns the debit lines with outstanding amounts for the patron.
561 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
562 return a list of Koha::Account::Line objects.
566 sub outstanding_debits {
569 return $self->lines->search(
571 amount => { '>' => 0 },
572 amountoutstanding => { '>' => 0 }
577 =head3 outstanding_credits
579 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
581 It returns the credit lines with outstanding amounts for the patron.
583 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
584 return a list of Koha::Account::Line objects.
588 sub outstanding_credits {
591 return $self->lines->search(
593 amount => { '<' => 0 },
594 amountoutstanding => { '<' => 0 }
599 =head3 non_issues_charges
601 my $non_issues_charges = $self->non_issues_charges
603 Calculates amount immediately owing by the patron - non-issue charges.
605 Charges exempt from non-issue are:
606 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
607 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
608 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
612 sub non_issues_charges {
615 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
616 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
619 push @not_fines, 'Res'
620 unless C4::Context->preference('HoldsInNoissuesCharge');
621 push @not_fines, 'Rent'
622 unless C4::Context->preference('RentalsInNoissuesCharge');
623 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
624 my $dbh = C4::Context->dbh;
627 $dbh->selectcol_arrayref(q|
628 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
632 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
634 return $self->lines->search(
636 accounttype => { -not_in => \@not_fines }
638 )->total_outstanding;
643 my $lines = $self->lines;
645 Return all credits and debits for the user, outstanding or otherwise
652 return Koha::Account::Lines->search(
654 borrowernumber => $self->{patron_id},
659 =head3 reconcile_balance
661 $account->reconcile_balance();
663 Find outstanding credits and use them to pay outstanding debits.
664 Currently, this implicitly uses the 'First In First Out' rule for
665 applying credits against debits.
669 sub reconcile_balance {
672 my $outstanding_debits = $self->outstanding_debits;
673 my $outstanding_credits = $self->outstanding_credits;
675 while ( $outstanding_debits->total_outstanding > 0
676 and my $credit = $outstanding_credits->next )
678 # there's both outstanding debits and credits
679 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
681 $outstanding_debits = $self->outstanding_debits;
697 'credit' => 'Manual Credit',
698 'forgiven' => 'Writeoff',
699 'lost_item_return' => 'Lost Item',
700 'payment' => 'Payment',
701 'writeoff' => 'Writeoff',
702 'account' => 'Account Fee',
703 'reserve' => 'Reserve Fee',
704 'processing' => 'Processing Fee',
705 'lost_item' => 'Lost Item',
706 'rent' => 'Rental Fee',
707 'rent_daily' => 'Rental Fee',
708 'rent_renew' => 'Rental Fee',
709 'rent_daily_renew' => 'Rental Fee',
710 'overdue' => 'OVERDUE',
711 'manual_debit' => 'Manual Debit',
712 'hold_expired' => 'Hold Expired'
715 =head3 $account_type_credit
719 our $account_type_credit = {
722 'lost_item_return' => 'LOST_RETURN',
727 =head3 $account_type_debit
731 our $account_type_debit = {
733 'overdue' => 'OVERDUE',
734 'lost_item' => 'LOST',
737 'processing' => 'PF',
739 'rent_daily' => 'RENT_DAILY',
740 'rent_renew' => 'RENT_RENEW',
741 'rent_daily_renew' => 'RENT_DAILY_RENEW',
743 'manual_debit' => 'M',
744 'hold_expired' => 'HE'
751 Kyle M Hall <kyle.m.hall@gmail.com>
752 Tomás Cohen Arazi <tomascohen@gmail.com>
753 Martin Renvoize <martin.renvoize@ptfs-europe.com>