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 );
35 use Koha::Exceptions::Account;
39 Koha::Accounts - Module for managing payments and fees for patrons
44 my ( $class, $params ) = @_;
46 Carp::croak("No patron id passed in!") unless $params->{patron_id};
48 return bless( $params, $class );
53 This method allows payments to be made against fees/fines
55 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 $sip = $params->{sip};
75 my $description = $params->{description};
76 my $note = $params->{note} || q{};
77 my $library_id = $params->{library_id};
78 my $lines = $params->{lines};
79 my $type = $params->{type} || 'payment';
80 my $payment_type = $params->{payment_type} || undef;
81 my $account_type = $params->{account_type};
82 my $offset_type = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
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;
91 my @fines_paid; # List of account lines paid on with this payment
93 my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
94 $balance_remaining ||= 0;
98 # We were passed a specific line to pay
99 foreach my $fine ( @$lines ) {
101 $fine->amountoutstanding > $balance_remaining
103 : $fine->amountoutstanding;
105 my $old_amountoutstanding = $fine->amountoutstanding;
106 my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
107 $fine->amountoutstanding($new_amountoutstanding)->store();
108 $balance_remaining = $balance_remaining - $amount_to_pay;
110 if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
112 C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
115 my $account_offset = Koha::Account::Offset->new(
117 debit_id => $fine->id,
118 type => $offset_type,
119 amount => $amount_to_pay * -1,
122 push( @account_offsets, $account_offset );
124 if ( C4::Context->preference("FinesLog") ) {
130 action => 'fee_payment',
131 borrowernumber => $fine->borrowernumber,
132 old_amountoutstanding => $old_amountoutstanding,
133 new_amountoutstanding => 0,
134 amount_paid => $old_amountoutstanding,
135 accountlines_id => $fine->id,
136 manager_id => $manager_id,
142 push( @fines_paid, $fine->id );
146 # Were not passed a specific line to pay, or the payment was for more
147 # than the what was owed on the given line. In that case pay down other
148 # lines with remaining balance.
149 my @outstanding_fines;
150 @outstanding_fines = $self->lines->search(
152 amountoutstanding => { '>' => 0 },
154 ) if $balance_remaining > 0;
156 foreach my $fine (@outstanding_fines) {
158 $fine->amountoutstanding > $balance_remaining
160 : $fine->amountoutstanding;
162 my $old_amountoutstanding = $fine->amountoutstanding;
163 $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
166 my $account_offset = Koha::Account::Offset->new(
168 debit_id => $fine->id,
169 type => $offset_type,
170 amount => $amount_to_pay * -1,
173 push( @account_offsets, $account_offset );
175 if ( C4::Context->preference("FinesLog") ) {
181 action => "fee_$type",
182 borrowernumber => $fine->borrowernumber,
183 old_amountoutstanding => $old_amountoutstanding,
184 new_amountoutstanding => $fine->amountoutstanding,
185 amount_paid => $amount_to_pay,
186 accountlines_id => $fine->id,
187 manager_id => $manager_id,
193 push( @fines_paid, $fine->id );
196 $balance_remaining = $balance_remaining - $amount_to_pay;
197 last unless $balance_remaining > 0;
201 $type eq 'writeoff' ? 'W'
202 : defined($sip) ? "Pay$sip"
205 $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
207 my $payment = Koha::Account::Line->new(
209 borrowernumber => $self->{patron_id},
210 date => dt_from_string(),
211 amount => 0 - $amount,
212 description => $description,
213 accounttype => $account_type,
214 payment_type => $payment_type,
215 amountoutstanding => 0 - $balance_remaining,
216 manager_id => $manager_id,
217 interface => $interface,
218 branchcode => $library_id,
223 foreach my $o ( @account_offsets ) {
224 $o->credit_id( $payment->id() );
230 branch => $library_id,
233 borrowernumber => $self->{patron_id},
237 if ( C4::Context->preference("FinesLog") ) {
243 action => "create_$type",
244 borrowernumber => $self->{patron_id},
245 amount => 0 - $amount,
246 amountoutstanding => 0 - $balance_remaining,
247 accounttype => $account_type,
248 accountlines_paid => \@fines_paid,
249 manager_id => $manager_id,
256 if ( C4::Context->preference('UseEmailReceipts') ) {
258 my $letter = C4::Letters::GetPreparedLetter(
259 module => 'circulation',
260 letter_code => uc("ACCOUNT_$type"),
261 message_transport_type => 'email',
262 lang => $patron->lang,
264 borrowers => $self->{patron_id},
265 branches => $self->{library_id},
269 offsets => \@account_offsets,
274 C4::Letters::EnqueueLetter(
277 borrowernumber => $self->{patron_id},
278 message_transport_type => 'email',
280 ) or warn "can't enqueue letter $letter";
289 This method allows adding credits to a patron's account
291 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
294 description => $description,
297 interface => $interface,
298 library_id => $library_id,
300 payment_type => $payment_type,
301 type => $credit_type,
306 $credit_type can be any of:
317 my ( $self, $params ) = @_;
319 # amount is passed as a positive value, but we store credit as negative values
320 my $amount = $params->{amount} * -1;
321 my $description = $params->{description} // q{};
322 my $note = $params->{note} // q{};
323 my $user_id = $params->{user_id};
324 my $interface = $params->{interface};
325 my $library_id = $params->{library_id};
326 my $sip = $params->{sip};
327 my $payment_type = $params->{payment_type};
328 my $type = $params->{type} || 'payment';
329 my $item_id = $params->{item_id};
331 my $schema = Koha::Database->new->schema;
333 my $account_type = $Koha::Account::account_type_credit->{$type};
334 $account_type .= $sip
343 # Insert the account line
344 $line = Koha::Account::Line->new(
345 { borrowernumber => $self->{patron_id},
348 description => $description,
349 accounttype => $account_type,
350 amountoutstanding => $amount,
351 payment_type => $payment_type,
353 manager_id => $user_id,
354 interface => $interface,
355 branchcode => $library_id,
356 itemnumber => $item_id,
360 # Record the account offset
361 my $account_offset = Koha::Account::Offset->new(
362 { credit_id => $line->id,
363 type => $Koha::Account::offset_type->{$type},
369 { branch => $library_id,
372 borrowernumber => $self->{patron_id},
374 ) if grep { $type eq $_ } ('payment', 'writeoff') ;
376 if ( C4::Context->preference("FinesLog") ) {
381 { action => "create_$type",
382 borrowernumber => $self->{patron_id},
384 description => $description,
385 amountoutstanding => $amount,
386 accounttype => $account_type,
388 itemnumber => $item_id,
389 manager_id => $user_id,
390 branchcode => $library_id,
404 This method allows adding debits to a patron's account
406 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
409 description => $description,
412 interface => $interface,
413 library_id => $library_id,
416 issue_id => $issue_id
420 $debit_type can be any of:
435 my ( $self, $params ) = @_;
437 # amount should always be a positive value
438 my $amount = $params->{amount};
440 unless ( $amount > 0 ) {
441 Koha::Exceptions::Account::AmountNotPositive->throw(
442 error => 'Debit amount passed is not positive'
446 my $description = $params->{description} // q{};
447 my $note = $params->{note} // q{};
448 my $user_id = $params->{user_id};
449 my $interface = $params->{interface};
450 my $library_id = $params->{library_id};
451 my $type = $params->{type};
452 my $item_id = $params->{item_id};
453 my $issue_id = $params->{issue_id};
455 my $schema = Koha::Database->new->schema;
457 unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
458 Koha::Exceptions::Account::UnrecognisedType->throw(
459 error => 'Type of debit not recognised'
463 my $account_type = $Koha::Account::account_type_debit->{$type};
470 # Insert the account line
471 $line = Koha::Account::Line->new(
472 { borrowernumber => $self->{patron_id},
475 description => $description,
476 accounttype => $account_type,
477 amountoutstanding => $amount,
478 payment_type => undef,
480 manager_id => $user_id,
481 interface => $interface,
482 itemnumber => $item_id,
483 issue_id => $issue_id,
484 branchcode => $library_id,
488 # Record the account offset
489 my $account_offset = Koha::Account::Offset->new(
490 { debit_id => $line->id,
491 type => $Koha::Account::offset_type->{$type},
496 if ( C4::Context->preference("FinesLog") ) {
501 { action => "create_$type",
502 borrowernumber => $self->{patron_id},
504 description => $description,
505 amountoutstanding => $amount,
506 accounttype => $account_type,
508 itemnumber => $item_id,
509 manager_id => $user_id,
523 my $balance = $self->balance
525 Return the balance (sum of amountoutstanding columns)
531 return $self->lines->total_outstanding;
534 =head3 outstanding_debits
536 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
538 It returns the debit lines with outstanding amounts for the patron.
540 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
541 return a list of Koha::Account::Line objects.
545 sub outstanding_debits {
548 return $self->lines->search(
550 amount => { '>' => 0 },
551 amountoutstanding => { '>' => 0 }
556 =head3 outstanding_credits
558 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
560 It returns the credit lines with outstanding amounts for the patron.
562 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
563 return a list of Koha::Account::Line objects.
567 sub outstanding_credits {
570 return $self->lines->search(
572 amount => { '<' => 0 },
573 amountoutstanding => { '<' => 0 }
578 =head3 non_issues_charges
580 my $non_issues_charges = $self->non_issues_charges
582 Calculates amount immediately owing by the patron - non-issue charges.
584 Charges exempt from non-issue are:
585 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
586 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
587 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
591 sub non_issues_charges {
594 # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
595 my $ACCOUNT_TYPE_LENGTH = 5; # this is plain ridiculous...
598 push @not_fines, 'Res'
599 unless C4::Context->preference('HoldsInNoissuesCharge');
600 push @not_fines, 'Rent'
601 unless C4::Context->preference('RentalsInNoissuesCharge');
602 unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
603 my $dbh = C4::Context->dbh;
606 $dbh->selectcol_arrayref(q|
607 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
611 @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
613 return $self->lines->search(
615 accounttype => { -not_in => \@not_fines }
617 )->total_outstanding;
622 my $lines = $self->lines;
624 Return all credits and debits for the user, outstanding or otherwise
631 return Koha::Account::Lines->search(
633 borrowernumber => $self->{patron_id},
638 =head3 reconcile_balance
640 $account->reconcile_balance();
642 Find outstanding credits and use them to pay outstanding debits.
643 Currently, this implicitly uses the 'First In First Out' rule for
644 applying credits against debits.
648 sub reconcile_balance {
651 my $outstanding_debits = $self->outstanding_debits;
652 my $outstanding_credits = $self->outstanding_credits;
654 while ( $outstanding_debits->total_outstanding > 0
655 and my $credit = $outstanding_credits->next )
657 # there's both outstanding debits and credits
658 $credit->apply( { debits => $outstanding_debits } ); # applying credit, no special offset
660 $outstanding_debits = $self->outstanding_debits;
676 'credit' => 'Manual Credit',
677 'forgiven' => 'Writeoff',
678 'lost_item_return' => 'Lost Item',
679 'payment' => 'Payment',
680 'writeoff' => 'Writeoff',
681 'account' => 'Account Fee',
682 'reserve' => 'Reserve Fee',
683 'processing' => 'Processing Fee',
684 'lost_item' => 'Lost Item',
685 'rent' => 'Rental Fee',
687 'manual_debit' => 'Manual Debit',
688 'hold_expired' => 'Hold Expired'
691 =head3 $account_type_credit
695 our $account_type_credit = {
698 'lost_item_return' => 'CR',
703 =head3 $account_type_debit
707 our $account_type_debit = {
713 'processing' => 'PF',
716 'manual_debit' => 'M',
717 'hold_expired' => 'HE'
724 Kyle M Hall <kyle.m.hall@gmail.com>
725 Tomás Cohen Arazi <tomascohen@gmail.com>
726 Martin Renvoize <martin.renvoize@ptfs-europe.com>