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);
25 use Koha::Account::CreditType;
26 use Koha::Account::DebitType;
27 use Koha::Account::Offsets;
29 use Koha::Exceptions::Account;
32 use base qw(Koha::Object);
38 Koha::Account::Line - Koha accountline Object class
48 Return the patron linked to this account line
54 my $rs = $self->_result->borrowernumber;
56 return Koha::Patron->_new_from_dbic( $rs );
61 Return the item linked to this account line if exists
67 my $rs = $self->_result->itemnumber;
69 return Koha::Item->_new_from_dbic( $rs );
74 Return the checkout linked to this account line if exists
80 return unless $self->issue_id ;
82 $self->{_checkout} ||= Koha::Checkouts->find( $self->issue_id );
83 $self->{_checkout} ||= Koha::Old::Checkouts->find( $self->issue_id );
84 return $self->{_checkout};
89 Return the credit_type linked to this account line
95 my $rs = $self->_result->credit_type_code;
97 return Koha::Account::CreditType->_new_from_dbic( $rs );
102 Return the debit_type linked to this account line
108 my $rs = $self->_result->debit_type_code;
110 return Koha::Account::DebitType->_new_from_dbic( $rs );
113 =head3 credit_offsets
115 Return the credit_offsets linked to this account line if some exist
121 my $rs = $self->_result->account_offsets_credits;
123 return Koha::Account::Offsets->_new_from_dbic($rs);
128 Return the debit_offsets linked to this account line if some exist
134 my $rs = $self->_result->account_offsets_debits;
136 return Koha::Account::Offsets->_new_from_dbic($rs);
142 my $credits = $accountline->credits;
143 my $credits = $accountline->credits( $cond, $attr );
145 Return the credits linked to this account line if some exist.
146 Search conditions and attributes may be passed if you wish to filter
147 the resultant resultant resultset.
152 my ( $self, $cond, $attr ) = @_;
154 unless ( $self->is_debit ) {
155 Koha::Exceptions::Account::IsNotCredit->throw(
156 error => 'Account line ' . $self->id . ' is not a debit'
161 $self->_result->search_related('account_offsets_debits')
162 ->search_related( 'credit', $cond, $attr );
164 return Koha::Account::Lines->_new_from_dbic($rs);
169 my $debits = $accountline->debits;
170 my $debits = $accountline->debits( $cond, $attr );
172 Return the debits linked to this account line if some exist.
173 Search conditions and attributes may be passed if you wish to filter
174 the resultant resultant resultset.
179 my ( $self, $cond, $attr ) = @_;
181 unless ( $self->is_credit ) {
182 Koha::Exceptions::Account::IsNotCredit->throw(
183 error => 'Account line ' . $self->id . ' is not a credit'
188 $self->_result->search_related('account_offsets_credits')
189 ->search_related( 'debit', $cond, $attr );
191 return Koha::Account::Lines->_new_from_dbic($rs);
196 $payment_accountline->void();
198 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
199 created by the application of this credit upon any debits and mark the credit
200 as 'void' by updating it's status to "VOID".
207 # Make sure it is a payment we are voiding
208 return unless $self->amount < 0;
210 my @account_offsets =
211 Koha::Account::Offsets->search(
212 { credit_id => $self->id, amount => { '<' => 0 } } );
214 $self->_result->result_source->schema->txn_do(
216 foreach my $account_offset (@account_offsets) {
218 Koha::Account::Lines->find( $account_offset->debit_id );
220 next unless $fee_paid;
222 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
223 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
224 $fee_paid->amountoutstanding($new_amount);
227 Koha::Account::Offset->new(
229 credit_id => $self->id,
230 debit_id => $fee_paid->id,
231 amount => $amount_paid,
232 type => 'Void Payment',
237 if ( C4::Context->preference("FinesLog") ) {
240 $self->borrowernumber,
243 action => 'void_payment',
244 borrowernumber => $self->borrowernumber,
245 amount => $self->amount,
246 amountoutstanding => $self->amountoutstanding,
247 description => $self->description,
248 credit_type_code => $self->credit_type_code,
249 payment_type => $self->payment_type,
251 itemnumber => $self->itemnumber,
252 manager_id => $self->manager_id,
254 [ map { $_->unblessed } @account_offsets ],
263 amountoutstanding => 0,
275 $charge_accountline->reduce({
276 reduction_type => $reduction_type
279 Used to 'reduce' a charge/debit by adding a credit to offset against the amount
282 May be used to apply a discount whilst retaining the original debit amounts or
283 to apply a full or partial refund for example when a lost item is found and
286 It will immediately be applied to the given debit unless the debit has already
287 been paid, in which case a 'zero' offset will be added to maintain a link to
288 the debit but the outstanding credit will be left so it may be applied to other
291 Reduction type may be one of:
296 Returns the reduction accountline (which will be a credit)
301 my ( $self, $params ) = @_;
303 # Make sure it is a charge we are reducing
304 unless ( $self->is_debit ) {
305 Koha::Exceptions::Account::IsNotDebit->throw(
306 error => 'Account line ' . $self->id . 'is not a debit' );
308 if ( $self->debit_type_code eq 'PAYOUT' ) {
309 Koha::Exceptions::Account::IsNotDebit->throw(
310 error => 'Account line ' . $self->id . 'is a payout' );
313 # Check for mandatory parameters
314 my @mandatory = ( 'interface', 'reduction_type', 'amount' );
315 for my $param (@mandatory) {
316 unless ( defined( $params->{$param} ) ) {
317 Koha::Exceptions::MissingParameter->throw(
318 error => "The $param parameter is mandatory" );
322 # More mandatory parameters
323 if ( $params->{interface} eq 'intranet' ) {
324 my @optional = ( 'staff_id', 'branch' );
325 for my $param (@optional) {
326 unless ( defined( $params->{$param} ) ) {
327 Koha::Exceptions::MissingParameter->throw( error =>
328 "The $param parameter is mandatory when interface is set to 'intranet'"
334 # Make sure the reduction isn't more than the original
335 my $original = $self->amount;
336 Koha::Exceptions::Account::AmountNotPositive->throw(
337 error => 'Reduce amount passed is not positive' )
338 unless ( $params->{amount} > 0 );
339 Koha::Exceptions::ParameterTooHigh->throw( error =>
340 "Amount to reduce ($params->{amount}) is higher than original amount ($original)"
341 ) unless ( $original >= $params->{amount} );
343 $self->credits( { credit_type_code => [ 'DISCOUNT', 'REFUND' ] } )->total;
344 Koha::Exceptions::ParameterTooHigh->throw( error =>
345 "Combined reduction ($params->{amount} + $reduced) is higher than original amount ("
348 unless ( $original >= ( $params->{amount} + abs($reduced) ) );
350 my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' };
353 $self->_result->result_source->schema->txn_do(
356 # A 'reduction' is a 'credit'
357 $reduction = Koha::Account::Line->new(
360 amount => 0 - $params->{amount},
361 credit_type_code => $params->{reduction_type},
363 amountoutstanding => 0 - $params->{amount},
364 manager_id => $params->{staff_id},
365 borrowernumber => $self->borrowernumber,
366 interface => $params->{interface},
367 branchcode => $params->{branch},
371 my $reduction_offset = Koha::Account::Offset->new(
373 credit_id => $reduction->accountlines_id,
374 type => uc( $params->{reduction_type} ),
375 amount => $params->{amount}
379 # Link reduction to charge (and apply as required)
380 my $debit_outstanding = $self->amountoutstanding;
381 if ( $debit_outstanding >= $params->{amount} ) {
386 offset_type => uc( $params->{reduction_type} )
389 $reduction->status('APPLIED')->store();
393 # Zero amount offset used to link original 'debit' to reduction 'credit'
394 my $link_reduction_offset = Koha::Account::Offset->new(
396 credit_id => $reduction->accountlines_id,
397 debit_id => $self->accountlines_id,
398 type => uc( $params->{reduction_type} ),
404 # Update status of original debit
405 $self->status( $status->{ $params->{reduction_type} } )->store;
409 $reduction->discard_changes;
415 my $debits = $account->outstanding_debits;
416 my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
418 Applies the credit to a given debits array reference.
420 =head4 arguments hashref
424 =item debits - Koha::Account::Lines object set of debits
426 =item offset_type (optional) - a string indicating the offset type (valid values are those from
427 the 'account_offset_types' table)
434 my ( $self, $params ) = @_;
436 my $debits = $params->{debits};
437 my $offset_type = $params->{offset_type} // 'Credit Applied';
439 unless ( $self->is_credit ) {
440 Koha::Exceptions::Account::IsNotCredit->throw(
441 error => 'Account line ' . $self->id . ' is not a credit'
445 my $available_credit = $self->amountoutstanding * -1;
447 unless ( $available_credit > 0 ) {
448 Koha::Exceptions::Account::NoAvailableCredit->throw(
449 error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
453 my $schema = Koha::Database->new->schema;
455 $schema->txn_do( sub {
456 for my $debit ( @{$debits} ) {
458 unless ( $debit->is_debit ) {
459 Koha::Exceptions::Account::IsNotDebit->throw(
460 error => 'Account line ' . $debit->id . 'is not a debit'
463 my $amount_to_cancel;
464 my $owed = $debit->amountoutstanding;
466 if ( $available_credit >= $owed ) {
467 $amount_to_cancel = $owed;
469 else { # $available_credit < $debit->amountoutstanding
470 $amount_to_cancel = $available_credit;
473 # record the account offset
474 Koha::Account::Offset->new(
475 { credit_id => $self->id,
476 debit_id => $debit->id,
477 amount => $amount_to_cancel * -1,
478 type => $offset_type,
482 $available_credit -= $amount_to_cancel;
484 $self->amountoutstanding( $available_credit * -1 )->store;
485 $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
487 # Same logic exists in Koha::Account::pay
488 if ( $debit->amountoutstanding == 0
489 && $debit->itemnumber
490 && $debit->debit_type_code
491 && $debit->debit_type_code eq 'LOST' )
493 C4::Circulation::ReturnLostItem( $self->borrowernumber, $debit->itemnumber );
499 return $available_credit;
504 $credit_accountline->payout(
506 payout_type => $payout_type,
507 register_id => $register_id,
508 staff_id => $staff_id,
509 interface => 'intranet',
514 Used to 'pay out' a credit to a user.
516 Payout type may be one of any existing payment types
518 Returns the payout debit line that is created via this transaction.
523 my ( $self, $params ) = @_;
525 # Make sure it is a credit we are paying out
526 unless ( $self->is_credit ) {
527 Koha::Exceptions::Account::IsNotCredit->throw(
528 error => 'Account line ' . $self->id . ' is not a credit' );
531 # Check for mandatory parameters
533 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
534 for my $param (@mandatory) {
535 unless ( defined( $params->{$param} ) ) {
536 Koha::Exceptions::MissingParameter->throw(
537 error => "The $param parameter is mandatory" );
541 # Make sure there is outstanding credit to pay out
542 my $outstanding = -1 * $self->amountoutstanding;
544 $params->{amount} ? $params->{amount} : $outstanding;
545 Koha::Exceptions::Account::AmountNotPositive->throw(
546 error => 'Payout amount passed is not positive' )
547 unless ( $amount > 0 );
548 Koha::Exceptions::ParameterTooHigh->throw(
549 error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
550 unless ($outstanding >= $amount );
552 # Make sure we record the cash register for cash transactions
553 Koha::Exceptions::Account::RegisterRequired->throw()
554 if ( C4::Context->preference("UseCashRegisters")
555 && defined( $params->{payout_type} )
556 && ( $params->{payout_type} eq 'CASH' )
557 && !defined( $params->{cash_register} ) );
560 $self->_result->result_source->schema->txn_do(
563 # A 'payout' is a 'debit'
564 $payout = Koha::Account::Line->new(
568 debit_type_code => 'PAYOUT',
569 payment_type => $params->{payout_type},
570 amountoutstanding => $amount,
571 manager_id => $params->{staff_id},
572 borrowernumber => $self->borrowernumber,
573 interface => $params->{interface},
574 branchcode => $params->{branch},
575 register_id => $params->{cash_register}
579 my $payout_offset = Koha::Account::Offset->new(
581 debit_id => $payout->accountlines_id,
587 $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
588 $self->status('PAID')->store;
592 $payout->discard_changes;
598 This method allows updating a debit or credit on a patron's account
600 $account_line->adjust(
603 type => $update_type,
604 interface => $interface
608 $update_type can be any of:
611 Authors Note: The intention here is that this method is only used
612 to adjust accountlines where the final amount is not yet known/fixed.
613 Incrementing fines are the only existing case at the time of writing,
614 all other forms of 'adjustment' should be recorded as distinct credits
615 or debits and applied, via an offset, to the corresponding debit or credit.
620 my ( $self, $params ) = @_;
622 my $amount = $params->{amount};
623 my $update_type = $params->{type};
624 my $interface = $params->{interface};
626 unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
627 Koha::Exceptions::Account::UnrecognisedType->throw(
628 error => 'Update type not recognised'
632 my $debit_type_code = $self->debit_type_code;
633 my $account_status = $self->status;
637 $Koha::Account::Line::allowed_update->{$update_type}
640 && ( $Koha::Account::Line::allowed_update->{$update_type}
641 ->{$debit_type_code} eq $account_status )
645 Koha::Exceptions::Account::UnrecognisedType->throw(
646 error => 'Update type not allowed on this debit_type' );
649 my $schema = Koha::Database->new->schema;
654 my $amount_before = $self->amount;
655 my $amount_outstanding_before = $self->amountoutstanding;
656 my $difference = $amount - $amount_before;
657 my $new_outstanding = $amount_outstanding_before + $difference;
659 my $offset_type = $debit_type_code;
660 $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
662 # Catch cases that require patron refunds
663 if ( $new_outstanding < 0 ) {
665 Koha::Patrons->find( $self->borrowernumber )->account;
666 my $credit = $account->add_credit(
668 amount => $new_outstanding * -1,
669 description => 'Overpayment refund',
671 interface => $interface,
672 ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
675 $new_outstanding = 0;
678 # Update the account line
683 amountoutstanding => $new_outstanding,
687 # Record the account offset
688 my $account_offset = Koha::Account::Offset->new(
690 debit_id => $self->id,
691 type => $offset_type,
692 amount => $difference
696 if ( C4::Context->preference("FinesLog") ) {
698 "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
699 $self->borrowernumber,
701 { action => $update_type,
702 borrowernumber => $self->borrowernumber,
704 description => undef,
705 amountoutstanding => $new_outstanding,
706 debit_type_code => $self->debit_type_code,
708 itemnumber => $self->itemnumber,
712 ) if ( $update_type eq 'overdue_update' );
722 my $bool = $line->is_credit;
729 return ( $self->amount < 0 );
734 my $bool = $line->is_debit;
741 return !$self->is_credit;
744 =head3 to_api_mapping
746 This method returns the mapping for representing a Koha::Account::Line object
753 accountlines_id => 'account_line_id',
754 credit_type_code => 'credit_type',
755 debit_type_code => 'debit_type',
756 amountoutstanding => 'amount_outstanding',
757 borrowernumber => 'patron_id',
758 branchcode => 'library_id',
759 issue_id => 'checkout_id',
760 itemnumber => 'item_id',
761 manager_id => 'user_id',
762 note => 'internal_note',
766 =head2 Internal methods
775 return 'Accountline';
782 =head3 $allowed_update
786 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
790 Kyle M Hall <kyle@bywatersolutions.com >
791 Tomás Cohen Arazi <tomascohen@theke.io>
792 Martin Renvoize <martin.renvoize@ptfs-europe.com>