1 package Koha::Account::Line;
3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
23 use C4::Log qw(logaction);
24 use C4::Overdues qw(GetFine);
26 use Koha::Account::CreditType;
27 use Koha::Account::DebitType;
28 use Koha::Account::Offsets;
30 use Koha::Exceptions::Account;
33 use base qw(Koha::Object);
39 Koha::Account::Line - Koha accountline Object class
49 Return the patron linked to this account line
55 my $rs = $self->_result->borrowernumber;
57 return Koha::Patron->_new_from_dbic( $rs );
62 Return the item linked to this account line if exists
68 my $rs = $self->_result->itemnumber;
70 return Koha::Item->_new_from_dbic( $rs );
75 Return the checkout linked to this account line if exists
81 return unless $self->issue_id ;
83 $self->{_checkout} ||= Koha::Checkouts->find( $self->issue_id );
84 $self->{_checkout} ||= Koha::Old::Checkouts->find( $self->issue_id );
85 return $self->{_checkout};
90 Return the credit_type linked to this account line
96 my $rs = $self->_result->credit_type_code;
98 return Koha::Account::CreditType->_new_from_dbic( $rs );
103 Return the debit_type linked to this account line
109 my $rs = $self->_result->debit_type_code;
111 return Koha::Account::DebitType->_new_from_dbic( $rs );
114 =head3 credit_offsets
116 Return the credit_offsets linked to this account line if some exist
122 my $rs = $self->_result->account_offsets_credits;
124 return Koha::Account::Offsets->_new_from_dbic($rs);
129 Return the debit_offsets linked to this account line if some exist
135 my $rs = $self->_result->account_offsets_debits;
137 return Koha::Account::Offsets->_new_from_dbic($rs);
143 my $credits = $accountline->credits;
144 my $credits = $accountline->credits( $cond, $attr );
146 Return the credits linked to this account line if some exist.
147 Search conditions and attributes may be passed if you wish to filter
148 the resultant resultant resultset.
153 my ( $self, $cond, $attr ) = @_;
155 unless ( $self->is_debit ) {
156 Koha::Exceptions::Account::IsNotCredit->throw(
157 error => 'Account line ' . $self->id . ' is not a debit'
162 $self->_result->search_related('account_offsets_debits')
163 ->search_related( 'credit', $cond, $attr );
165 return Koha::Account::Lines->_new_from_dbic($rs);
170 my $debits = $accountline->debits;
171 my $debits = $accountline->debits( $cond, $attr );
173 Return the debits linked to this account line if some exist.
174 Search conditions and attributes may be passed if you wish to filter
175 the resultant resultant resultset.
180 my ( $self, $cond, $attr ) = @_;
182 unless ( $self->is_credit ) {
183 Koha::Exceptions::Account::IsNotCredit->throw(
184 error => 'Account line ' . $self->id . ' is not a credit'
189 $self->_result->search_related('account_offsets_credits')
190 ->search_related( 'debit', $cond, $attr );
192 return Koha::Account::Lines->_new_from_dbic($rs);
197 $payment_accountline->void();
199 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
200 created by the application of this credit upon any debits and mark the credit
201 as 'void' by updating it's status to "VOID".
208 # Make sure it is a payment we are voiding
209 return unless $self->amount < 0;
211 my @account_offsets =
212 Koha::Account::Offsets->search(
213 { credit_id => $self->id, amount => { '<' => 0 } } );
215 $self->_result->result_source->schema->txn_do(
217 foreach my $account_offset (@account_offsets) {
219 Koha::Account::Lines->find( $account_offset->debit_id );
221 next unless $fee_paid;
223 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
224 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
225 $fee_paid->amountoutstanding($new_amount);
228 Koha::Account::Offset->new(
230 credit_id => $self->id,
231 debit_id => $fee_paid->id,
232 amount => $amount_paid,
233 type => 'Void Payment',
238 if ( C4::Context->preference("FinesLog") ) {
241 $self->borrowernumber,
244 action => 'void_payment',
245 borrowernumber => $self->borrowernumber,
246 amount => $self->amount,
247 amountoutstanding => $self->amountoutstanding,
248 description => $self->description,
249 credit_type_code => $self->credit_type_code,
250 payment_type => $self->payment_type,
252 itemnumber => $self->itemnumber,
253 manager_id => $self->manager_id,
255 [ map { $_->unblessed } @account_offsets ],
264 amountoutstanding => 0,
276 $charge_accountline->reduce({
277 reduction_type => $reduction_type
280 Used to 'reduce' a charge/debit by adding a credit to offset against the amount
283 May be used to apply a discount whilst retaining the original debit amounts or
284 to apply a full or partial refund for example when a lost item is found and
287 It will immediately be applied to the given debit unless the debit has already
288 been paid, in which case a 'zero' offset will be added to maintain a link to
289 the debit but the outstanding credit will be left so it may be applied to other
292 Reduction type may be one of:
297 Returns the reduction accountline (which will be a credit)
302 my ( $self, $params ) = @_;
304 # Make sure it is a charge we are reducing
305 unless ( $self->is_debit ) {
306 Koha::Exceptions::Account::IsNotDebit->throw(
307 error => 'Account line ' . $self->id . 'is not a debit' );
309 if ( $self->debit_type_code eq 'PAYOUT' ) {
310 Koha::Exceptions::Account::IsNotDebit->throw(
311 error => 'Account line ' . $self->id . 'is a payout' );
314 # Check for mandatory parameters
315 my @mandatory = ( 'interface', 'reduction_type', 'amount' );
316 for my $param (@mandatory) {
317 unless ( defined( $params->{$param} ) ) {
318 Koha::Exceptions::MissingParameter->throw(
319 error => "The $param parameter is mandatory" );
323 # More mandatory parameters
324 if ( $params->{interface} eq 'intranet' ) {
325 my @optional = ( 'staff_id', 'branch' );
326 for my $param (@optional) {
327 unless ( defined( $params->{$param} ) ) {
328 Koha::Exceptions::MissingParameter->throw( error =>
329 "The $param parameter is mandatory when interface is set to 'intranet'"
335 # Make sure the reduction isn't more than the original
336 my $original = $self->amount;
337 Koha::Exceptions::Account::AmountNotPositive->throw(
338 error => 'Reduce amount passed is not positive' )
339 unless ( $params->{amount} > 0 );
340 Koha::Exceptions::ParameterTooHigh->throw( error =>
341 "Amount to reduce ($params->{amount}) is higher than original amount ($original)"
342 ) unless ( $original >= $params->{amount} );
344 $self->credits( { credit_type_code => [ 'DISCOUNT', 'REFUND' ] } )->total;
345 Koha::Exceptions::ParameterTooHigh->throw( error =>
346 "Combined reduction ($params->{amount} + $reduced) is higher than original amount ("
349 unless ( $original >= ( $params->{amount} + abs($reduced) ) );
351 my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' };
354 $self->_result->result_source->schema->txn_do(
357 # A 'reduction' is a 'credit'
358 $reduction = Koha::Account::Line->new(
361 amount => 0 - $params->{amount},
362 credit_type_code => $params->{reduction_type},
364 amountoutstanding => 0 - $params->{amount},
365 manager_id => $params->{staff_id},
366 borrowernumber => $self->borrowernumber,
367 interface => $params->{interface},
368 branchcode => $params->{branch},
372 my $reduction_offset = Koha::Account::Offset->new(
374 credit_id => $reduction->accountlines_id,
375 type => uc( $params->{reduction_type} ),
376 amount => $params->{amount}
380 # Link reduction to charge (and apply as required)
381 my $debit_outstanding = $self->amountoutstanding;
382 if ( $debit_outstanding >= $params->{amount} ) {
387 offset_type => uc( $params->{reduction_type} )
390 $reduction->status('APPLIED')->store();
394 # Zero amount offset used to link original 'debit' to reduction 'credit'
395 my $link_reduction_offset = Koha::Account::Offset->new(
397 credit_id => $reduction->accountlines_id,
398 debit_id => $self->accountlines_id,
399 type => uc( $params->{reduction_type} ),
405 # Update status of original debit
406 $self->status( $status->{ $params->{reduction_type} } )->store;
410 $reduction->discard_changes;
416 my $debits = $account->outstanding_debits;
417 my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
419 Applies the credit to a given debits array reference.
421 =head4 arguments hashref
425 =item debits - Koha::Account::Lines object set of debits
427 =item offset_type (optional) - a string indicating the offset type (valid values are those from
428 the 'account_offset_types' table)
435 my ( $self, $params ) = @_;
437 my $debits = $params->{debits};
438 my $offset_type = $params->{offset_type} // 'Credit Applied';
440 unless ( $self->is_credit ) {
441 Koha::Exceptions::Account::IsNotCredit->throw(
442 error => 'Account line ' . $self->id . ' is not a credit'
446 my $available_credit = $self->amountoutstanding * -1;
448 unless ( $available_credit > 0 ) {
449 Koha::Exceptions::Account::NoAvailableCredit->throw(
450 error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
454 my $schema = Koha::Database->new->schema;
456 # Item numbers that have had a fine paid where the line has a accounttype
457 # of OVERDUE and a status of UNRETURNED. We might want to try and renew
459 my $overdue_unreturned = {};
461 $schema->txn_do( sub {
462 for my $debit ( @{$debits} ) {
464 unless ( $debit->is_debit ) {
465 Koha::Exceptions::Account::IsNotDebit->throw(
466 error => 'Account line ' . $debit->id . 'is not a debit'
469 my $amount_to_cancel;
470 my $owed = $debit->amountoutstanding;
472 if ( $available_credit >= $owed ) {
473 $amount_to_cancel = $owed;
475 else { # $available_credit < $debit->amountoutstanding
476 $amount_to_cancel = $available_credit;
479 # record the account offset
480 Koha::Account::Offset->new(
481 { credit_id => $self->id,
482 debit_id => $debit->id,
483 amount => $amount_to_cancel * -1,
484 type => $offset_type,
488 $available_credit -= $amount_to_cancel;
490 $self->amountoutstanding( $available_credit * -1 )->store;
491 $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
493 # If we need to make a note of the item associated with this line,
494 # in order that we can potentially renew it, do so.
495 # Same logic existing in Koha::Account::pay
497 $debit->amountoutstanding == 0 &&
498 $debit->accounttype &&
499 $debit->accounttype eq 'OVERDUE' &&
501 $debit->status eq 'UNRETURNED'
503 $overdue_unreturned->{$debit->itemnumber} = $debit;
506 # Same logic exists in Koha::Account::pay
507 if ( $debit->amountoutstanding == 0
508 && $debit->itemnumber
509 && $debit->debit_type_code
510 && $debit->debit_type_code eq 'LOST' )
512 C4::Circulation::ReturnLostItem( $self->borrowernumber, $debit->itemnumber );
518 # If we have overdue unreturned items that have had payments made
519 # against them, check whether the balance on those items is now zero
520 # and, if the syspref is set, renew them
521 # Same logic existing in Koha::Account::pay
523 C4::Context->preference('RenewAccruingItemWhenPaid') &&
524 keys %{$overdue_unreturned}
526 foreach my $itemnumber (keys %{$overdue_unreturned}) {
527 # Only do something if this item has no fines left on it
528 my $fine = C4::Overdues::GetFine( $itemnumber, $self->borrowernumber );
529 next if $fine && $fine > 0;
531 my ( $renew_ok, $error ) =
532 C4::Circulation::CanBookBeRenewed(
533 $self->borrowernumber, $itemnumber
536 C4::Circulation::AddRenewal(
537 $self->borrowernumber,
539 $overdue_unreturned->{$itemnumber}->{branchcode},
548 return $available_credit;
553 $credit_accountline->payout(
555 payout_type => $payout_type,
556 register_id => $register_id,
557 staff_id => $staff_id,
558 interface => 'intranet',
563 Used to 'pay out' a credit to a user.
565 Payout type may be one of any existing payment types
567 Returns the payout debit line that is created via this transaction.
572 my ( $self, $params ) = @_;
574 # Make sure it is a credit we are paying out
575 unless ( $self->is_credit ) {
576 Koha::Exceptions::Account::IsNotCredit->throw(
577 error => 'Account line ' . $self->id . ' is not a credit' );
580 # Check for mandatory parameters
582 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
583 for my $param (@mandatory) {
584 unless ( defined( $params->{$param} ) ) {
585 Koha::Exceptions::MissingParameter->throw(
586 error => "The $param parameter is mandatory" );
590 # Make sure there is outstanding credit to pay out
591 my $outstanding = -1 * $self->amountoutstanding;
593 $params->{amount} ? $params->{amount} : $outstanding;
594 Koha::Exceptions::Account::AmountNotPositive->throw(
595 error => 'Payout amount passed is not positive' )
596 unless ( $amount > 0 );
597 Koha::Exceptions::ParameterTooHigh->throw(
598 error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
599 unless ($outstanding >= $amount );
601 # Make sure we record the cash register for cash transactions
602 Koha::Exceptions::Account::RegisterRequired->throw()
603 if ( C4::Context->preference("UseCashRegisters")
604 && defined( $params->{payout_type} )
605 && ( $params->{payout_type} eq 'CASH' )
606 && !defined( $params->{cash_register} ) );
609 $self->_result->result_source->schema->txn_do(
612 # A 'payout' is a 'debit'
613 $payout = Koha::Account::Line->new(
617 debit_type_code => 'PAYOUT',
618 payment_type => $params->{payout_type},
619 amountoutstanding => $amount,
620 manager_id => $params->{staff_id},
621 borrowernumber => $self->borrowernumber,
622 interface => $params->{interface},
623 branchcode => $params->{branch},
624 register_id => $params->{cash_register}
628 my $payout_offset = Koha::Account::Offset->new(
630 debit_id => $payout->accountlines_id,
636 $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
637 $self->status('PAID')->store;
641 $payout->discard_changes;
647 This method allows updating a debit or credit on a patron's account
649 $account_line->adjust(
652 type => $update_type,
653 interface => $interface
657 $update_type can be any of:
660 Authors Note: The intention here is that this method is only used
661 to adjust accountlines where the final amount is not yet known/fixed.
662 Incrementing fines are the only existing case at the time of writing,
663 all other forms of 'adjustment' should be recorded as distinct credits
664 or debits and applied, via an offset, to the corresponding debit or credit.
669 my ( $self, $params ) = @_;
671 my $amount = $params->{amount};
672 my $update_type = $params->{type};
673 my $interface = $params->{interface};
675 unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
676 Koha::Exceptions::Account::UnrecognisedType->throw(
677 error => 'Update type not recognised'
681 my $debit_type_code = $self->debit_type_code;
682 my $account_status = $self->status;
686 $Koha::Account::Line::allowed_update->{$update_type}
689 && ( $Koha::Account::Line::allowed_update->{$update_type}
690 ->{$debit_type_code} eq $account_status )
694 Koha::Exceptions::Account::UnrecognisedType->throw(
695 error => 'Update type not allowed on this debit_type' );
698 my $schema = Koha::Database->new->schema;
703 my $amount_before = $self->amount;
704 my $amount_outstanding_before = $self->amountoutstanding;
705 my $difference = $amount - $amount_before;
706 my $new_outstanding = $amount_outstanding_before + $difference;
708 my $offset_type = $debit_type_code;
709 $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
711 # Catch cases that require patron refunds
712 if ( $new_outstanding < 0 ) {
714 Koha::Patrons->find( $self->borrowernumber )->account;
715 my $credit = $account->add_credit(
717 amount => $new_outstanding * -1,
718 description => 'Overpayment refund',
720 interface => $interface,
721 ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
724 $new_outstanding = 0;
727 # Update the account line
732 amountoutstanding => $new_outstanding,
736 # Record the account offset
737 my $account_offset = Koha::Account::Offset->new(
739 debit_id => $self->id,
740 type => $offset_type,
741 amount => $difference
745 if ( C4::Context->preference("FinesLog") ) {
747 "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
748 $self->borrowernumber,
750 { action => $update_type,
751 borrowernumber => $self->borrowernumber,
753 description => undef,
754 amountoutstanding => $new_outstanding,
755 debit_type_code => $self->debit_type_code,
757 itemnumber => $self->itemnumber,
761 ) if ( $update_type eq 'overdue_update' );
771 my $bool = $line->is_credit;
778 return ( $self->amount < 0 );
783 my $bool = $line->is_debit;
790 return !$self->is_credit;
793 =head3 to_api_mapping
795 This method returns the mapping for representing a Koha::Account::Line object
802 accountlines_id => 'account_line_id',
803 credit_type_code => 'credit_type',
804 debit_type_code => 'debit_type',
805 amountoutstanding => 'amount_outstanding',
806 borrowernumber => 'patron_id',
807 branchcode => 'library_id',
808 issue_id => 'checkout_id',
809 itemnumber => 'item_id',
810 manager_id => 'user_id',
811 note => 'internal_note',
815 =head2 Internal methods
824 return 'Accountline';
831 =head3 $allowed_update
835 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
839 Kyle M Hall <kyle@bywatersolutions.com >
840 Tomás Cohen Arazi <tomascohen@theke.io>
841 Martin Renvoize <martin.renvoize@ptfs-europe.com>