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::Lines;
34 use Koha::Account::Offsets;
35 use Koha::Account::DebitTypes;
37 use Koha::Exceptions::Account;
41 Koha::Accounts - Module for managing payments and fees for patrons
46 my ( $class, $params ) = @_;
48 Carp::croak("No patron id passed in!") unless $params->{patron_id};
50 return bless( $params, $class );
55 This method allows payments to be made against fees/fines
57 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
61 description => $description,
62 library_id => $branchcode,
63 lines => $lines, # Arrayref of Koha::Account::Line objects to pay
64 credit_type => $type, # credit_type_code code
65 item_id => $itemnumber, # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
72 my ( $self, $params ) = @_;
74 my $amount = $params->{amount};
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 $cash_register = $params->{cash_register};
82 my $item_id = $params->{item_id};
84 my $userenv = C4::Context->userenv;
86 my $manager_id = $userenv ? $userenv->{number} : undef;
87 my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
88 my $payment = $self->payin_amount(
90 interface => $interface,
93 payment_type => $payment_type,
94 cash_register => $cash_register,
95 user_id => $manager_id,
96 library_id => $library_id,
98 description => $description,
104 # NOTE: Pay historically always applied as much credit as it could to all
105 # existing outstanding debits, whether passed specific debits or otherwise.
106 if ( $payment->amountoutstanding ) {
109 { debits => [ $self->outstanding_debits->as_list ] } );
112 my $patron = Koha::Patrons->find( $self->{patron_id} );
113 my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
114 if ( C4::Context->preference('UseEmailReceipts') ) {
116 my $letter = C4::Letters::GetPreparedLetter(
117 module => 'circulation',
118 letter_code => uc("ACCOUNT_$type"),
119 message_transport_type => 'email',
120 lang => $patron->lang,
122 borrowers => $self->{patron_id},
123 branches => $library_id,
127 offsets => \@account_offsets,
132 C4::Letters::EnqueueLetter(
135 borrowernumber => $self->{patron_id},
136 message_transport_type => 'email',
138 ) or warn "can't enqueue letter $letter";
142 my $renew_outcomes = [];
143 for my $message ( @{$payment->object_messages} ) {
144 push @{$renew_outcomes}, $message->payload;
147 return { payment_id => $payment->id, renew_result => $renew_outcomes };
152 This method allows adding credits to a patron's account
154 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
157 description => $description,
160 interface => $interface,
161 library_id => $library_id,
162 payment_type => $payment_type,
163 type => $credit_type,
168 $credit_type can be any of:
182 my ( $self, $params ) = @_;
184 # check for mandatory params
185 my @mandatory = ( 'interface', 'amount' );
186 for my $param (@mandatory) {
187 unless ( defined( $params->{$param} ) ) {
188 Koha::Exceptions::MissingParameter->throw(
189 error => "The $param parameter is mandatory" );
193 # amount should always be passed as a positive value
194 my $amount = $params->{amount} * -1;
195 unless ( $amount < 0 ) {
196 Koha::Exceptions::Account::AmountNotPositive->throw(
197 error => 'Debit amount passed is not positive' );
200 my $description = $params->{description} // q{};
201 my $note = $params->{note} // q{};
202 my $user_id = $params->{user_id};
203 my $interface = $params->{interface};
204 my $library_id = $params->{library_id};
205 my $cash_register = $params->{cash_register};
206 my $payment_type = $params->{payment_type};
207 my $credit_type = $params->{type} || 'PAYMENT';
208 my $item_id = $params->{item_id};
210 Koha::Exceptions::Account::RegisterRequired->throw()
211 if ( C4::Context->preference("UseCashRegisters")
212 && defined($payment_type)
213 && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
214 && !defined($cash_register) );
217 my $schema = Koha::Database->new->schema;
222 # Insert the account line
223 $line = Koha::Account::Line->new(
225 borrowernumber => $self->{patron_id},
228 description => $description,
229 credit_type_code => $credit_type,
230 amountoutstanding => $amount,
231 payment_type => $payment_type,
233 manager_id => $user_id,
234 interface => $interface,
235 branchcode => $library_id,
236 register_id => $cash_register,
237 itemnumber => $item_id,
241 # Record the account offset
242 my $account_offset = Koha::Account::Offset->new(
244 credit_id => $line->id,
246 amount => $amount * -1
250 C4::Stats::UpdateStats(
252 branch => $library_id,
253 type => lc($credit_type),
255 borrowernumber => $self->{patron_id},
257 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
259 if ( C4::Context->preference("FinesLog") ) {
265 action => "create_$credit_type",
266 borrowernumber => $self->{patron_id},
268 description => $description,
269 amountoutstanding => $amount,
270 credit_type_code => $credit_type,
272 itemnumber => $item_id,
273 manager_id => $user_id,
274 branchcode => $library_id,
284 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
285 if ( $_->broken_fk eq 'credit_type_code' ) {
286 Koha::Exceptions::Account::UnrecognisedType->throw(
287 error => 'Type of credit not recognised' );
300 my $credit = $account->payin_amount(
303 type => $credit_type,
304 payment_type => $payment_type,
305 cash_register => $register_id,
306 interface => $interface,
307 library_id => $branchcode,
308 user_id => $staff_id,
309 debits => $debit_lines,
310 description => $description,
315 This method allows an amount to be paid into a patrons account and immediately applied against debts.
317 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
319 $credit_type can be any of:
327 my ( $self, $params ) = @_;
329 # check for mandatory params
330 my @mandatory = ( 'interface', 'amount', 'type' );
331 for my $param (@mandatory) {
332 unless ( defined( $params->{$param} ) ) {
333 Koha::Exceptions::MissingParameter->throw(
334 error => "The $param parameter is mandatory" );
338 # Check for mandatory register
339 Koha::Exceptions::Account::RegisterRequired->throw()
340 if ( C4::Context->preference("UseCashRegisters")
341 && defined( $params->{payment_type} )
342 && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
343 && !defined($params->{cash_register}) );
345 # amount should always be passed as a positive value
346 my $amount = $params->{amount};
347 unless ( $amount > 0 ) {
348 Koha::Exceptions::Account::AmountNotPositive->throw(
349 error => 'Payin amount passed is not positive' );
353 my $schema = Koha::Database->new->schema;
358 $credit = $self->add_credit($params);
360 # Offset debts passed first
361 if ( exists( $params->{debits} ) ) {
362 $credit = $credit->apply(
364 debits => $params->{debits}
369 # Offset against remaining balance if AutoReconcile
370 if ( C4::Context->preference("AccountAutoReconcile")
371 && $credit->amountoutstanding != 0 )
373 $credit = $credit->apply(
375 debits => [ $self->outstanding_debits->as_list ]
387 This method allows adding debits to a patron's account
389 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
392 description => $description,
395 interface => $interface,
396 library_id => $library_id,
398 transaction_type => $transaction_type,
399 cash_register => $register_id,
401 issue_id => $issue_id
405 $debit_type can be any of:
425 my ( $self, $params ) = @_;
427 # check for mandatory params
428 my @mandatory = ( 'interface', 'type', 'amount' );
429 for my $param (@mandatory) {
430 unless ( defined( $params->{$param} ) ) {
431 Koha::Exceptions::MissingParameter->throw(
432 error => "The $param parameter is mandatory" );
436 # check for cash register if using cash
437 Koha::Exceptions::Account::RegisterRequired->throw()
438 if ( C4::Context->preference("UseCashRegisters")
439 && defined( $params->{transaction_type} )
440 && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
441 && !defined( $params->{cash_register} ) );
443 # amount should always be a positive value
444 my $amount = $params->{amount};
445 unless ( $amount > 0 ) {
446 Koha::Exceptions::Account::AmountNotPositive->throw(
447 error => 'Debit amount passed is not positive' );
450 my $description = $params->{description} // q{};
451 my $note = $params->{note} // q{};
452 my $user_id = $params->{user_id};
453 my $interface = $params->{interface};
454 my $library_id = $params->{library_id};
455 my $cash_register = $params->{cash_register};
456 my $debit_type = $params->{type};
457 my $transaction_type = $params->{transaction_type};
458 my $item_id = $params->{item_id};
459 my $issue_id = $params->{issue_id};
462 my $schema = Koha::Database->new->schema;
467 # Insert the account line
468 $line = Koha::Account::Line->new(
470 borrowernumber => $self->{patron_id},
473 description => $description,
474 debit_type_code => $debit_type,
475 amountoutstanding => $amount,
476 payment_type => $transaction_type,
478 manager_id => $user_id,
479 interface => $interface,
480 itemnumber => $item_id,
481 issue_id => $issue_id,
482 branchcode => $library_id,
483 register_id => $cash_register,
485 $debit_type eq 'OVERDUE'
486 ? ( status => 'UNRETURNED' )
492 # Record the account offset
493 my $account_offset = Koha::Account::Offset->new(
495 debit_id => $line->id,
501 if ( C4::Context->preference("FinesLog") ) {
507 action => "create_$debit_type",
508 borrowernumber => $self->{patron_id},
510 description => $description,
511 amountoutstanding => $amount,
512 debit_type_code => $debit_type,
514 itemnumber => $item_id,
515 manager_id => $user_id,
525 if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
526 if ( $_->broken_fk eq 'debit_type_code' ) {
527 Koha::Exceptions::Account::UnrecognisedType->throw(
528 error => 'Type of debit not recognised' );
541 my $debit = $account->payout_amount(
543 payout_type => $payout_type,
544 register_id => $register_id,
545 staff_id => $staff_id,
546 interface => 'intranet',
548 credits => $credit_lines
552 This method allows an amount to be paid out from a patrons account against outstanding credits.
554 $payout_type can be any of the defined payment_types:
559 my ( $self, $params ) = @_;
561 # Check for mandatory parameters
563 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
564 for my $param (@mandatory) {
565 unless ( defined( $params->{$param} ) ) {
566 Koha::Exceptions::MissingParameter->throw(
567 error => "The $param parameter is mandatory" );
571 # Check for mandatory register
572 Koha::Exceptions::Account::RegisterRequired->throw()
573 if ( C4::Context->preference("UseCashRegisters")
574 && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
575 && !defined($params->{cash_register}) );
577 # Amount should always be passed as a positive value
578 my $amount = $params->{amount};
579 unless ( $amount > 0 ) {
580 Koha::Exceptions::Account::AmountNotPositive->throw(
581 error => 'Payout amount passed is not positive' );
584 # Amount should always be less than or equal to outstanding credit
586 my $outstanding_credits =
587 exists( $params->{credits} )
589 : $self->outstanding_credits->as_list;
590 for my $credit ( @{$outstanding_credits} ) {
591 $outstanding += $credit->amountoutstanding;
593 $outstanding = $outstanding * -1;
594 Koha::Exceptions::ParameterTooHigh->throw( error =>
595 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
596 ) unless ( $outstanding >= $amount );
599 my $schema = Koha::Database->new->schema;
603 # A 'payout' is a 'debit'
604 $payout = $self->add_debit(
606 amount => $params->{amount},
608 transaction_type => $params->{payout_type},
609 amountoutstanding => $params->{amount},
610 user_id => $params->{staff_id},
611 interface => $params->{interface},
612 branchcode => $params->{branch},
613 cash_register => $params->{cash_register}
617 # Offset against credits
618 for my $credit ( @{$outstanding_credits} ) {
619 $credit->apply( { debits => [$payout] } );
620 $payout->discard_changes;
621 last if $payout->amountoutstanding == 0;
625 $payout->status('PAID')->store;
634 my $balance = $self->balance
636 Return the balance (sum of amountoutstanding columns)
642 return $self->lines->total_outstanding;
645 =head3 outstanding_debits
647 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
649 It returns the debit lines with outstanding amounts for the patron.
651 It returns a Koha::Account::Lines iterator.
655 sub outstanding_debits {
658 return $self->lines->search(
660 amount => { '>' => 0 },
661 amountoutstanding => { '>' => 0 }
666 =head3 outstanding_credits
668 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
670 It returns the credit lines with outstanding amounts for the patron.
672 It returns a Koha::Account::Lines iterator.
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;
775 Kyle M Hall <kyle.m.hall@gmail.com>
776 Tomás Cohen Arazi <tomascohen@gmail.com>
777 Martin Renvoize <martin.renvoize@ptfs-europe.com>