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:
295 Returns the reduction accountline (which will be a credit)
300 my ( $self, $params ) = @_;
302 # Make sure it is a charge we are reducing
303 unless ( $self->is_debit ) {
304 Koha::Exceptions::Account::IsNotDebit->throw(
305 error => 'Account line ' . $self->id . 'is not a debit' );
307 if ( $self->debit_type_code eq 'PAYOUT' ) {
308 Koha::Exceptions::Account::IsNotDebit->throw(
309 error => 'Account line ' . $self->id . 'is a payout' );
312 # Check for mandatory parameters
313 my @mandatory = ( 'interface', 'reduction_type', 'amount' );
314 for my $param (@mandatory) {
315 unless ( defined( $params->{$param} ) ) {
316 Koha::Exceptions::MissingParameter->throw(
317 error => "The $param parameter is mandatory" );
321 # More mandatory parameters
322 if ( $params->{interface} eq 'intranet' ) {
323 my @optional = ( 'staff_id', 'branch' );
324 for my $param (@optional) {
325 unless ( defined( $params->{$param} ) ) {
326 Koha::Exceptions::MissingParameter->throw( error =>
327 "The $param parameter is mandatory when interface is set to 'intranet'"
333 # Make sure the reduction isn't more than the original
334 my $original = $self->amount;
335 Koha::Exceptions::Account::AmountNotPositive->throw(
336 error => 'Reduce amount passed is not positive' )
337 unless ( $params->{amount} > 0 );
338 Koha::Exceptions::ParameterTooHigh->throw( error =>
339 "Amount to reduce ($params->{amount}) is higher than original amount ($original)"
340 ) unless ( $original >= $params->{amount} );
342 $self->credits( { credit_type_code => [ 'REFUND' ] } )->total;
343 Koha::Exceptions::ParameterTooHigh->throw( error =>
344 "Combined reduction ($params->{amount} + $reduced) is higher than original amount ("
347 unless ( $original >= ( $params->{amount} + abs($reduced) ) );
349 my $status = { 'REFUND' => 'REFUNDED' };
352 $self->_result->result_source->schema->txn_do(
355 # A 'reduction' is a 'credit'
356 $reduction = Koha::Account::Line->new(
359 amount => 0 - $params->{amount},
360 credit_type_code => $params->{reduction_type},
362 amountoutstanding => 0 - $params->{amount},
363 manager_id => $params->{staff_id},
364 borrowernumber => $self->borrowernumber,
365 interface => $params->{interface},
366 branchcode => $params->{branch},
370 my $reduction_offset = Koha::Account::Offset->new(
372 credit_id => $reduction->accountlines_id,
373 type => uc( $params->{reduction_type} ),
374 amount => $params->{amount}
378 # Link reduction to charge (and apply as required)
379 my $debit_outstanding = $self->amountoutstanding;
380 if ( $debit_outstanding >= $params->{amount} ) {
385 offset_type => uc( $params->{reduction_type} )
388 $reduction->status('APPLIED')->store();
392 # Zero amount offset used to link original 'debit' to reduction 'credit'
393 my $link_reduction_offset = Koha::Account::Offset->new(
395 credit_id => $reduction->accountlines_id,
396 debit_id => $self->accountlines_id,
397 type => uc( $params->{reduction_type} ),
403 # Update status of original debit
404 $self->status( $status->{ $params->{reduction_type} } )->store;
408 $reduction->discard_changes;
414 my $debits = $account->outstanding_debits;
415 my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
417 Applies the credit to a given debits array reference.
419 =head4 arguments hashref
423 =item debits - Koha::Account::Lines object set of debits
425 =item offset_type (optional) - a string indicating the offset type (valid values are those from
426 the 'account_offset_types' table)
433 my ( $self, $params ) = @_;
435 my $debits = $params->{debits};
436 my $offset_type = $params->{offset_type} // 'Credit Applied';
438 unless ( $self->is_credit ) {
439 Koha::Exceptions::Account::IsNotCredit->throw(
440 error => 'Account line ' . $self->id . ' is not a credit'
444 my $available_credit = $self->amountoutstanding * -1;
446 unless ( $available_credit > 0 ) {
447 Koha::Exceptions::Account::NoAvailableCredit->throw(
448 error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
452 my $schema = Koha::Database->new->schema;
454 $schema->txn_do( sub {
455 for my $debit ( @{$debits} ) {
457 unless ( $debit->is_debit ) {
458 Koha::Exceptions::Account::IsNotDebit->throw(
459 error => 'Account line ' . $debit->id . 'is not a debit'
462 my $amount_to_cancel;
463 my $owed = $debit->amountoutstanding;
465 if ( $available_credit >= $owed ) {
466 $amount_to_cancel = $owed;
468 else { # $available_credit < $debit->amountoutstanding
469 $amount_to_cancel = $available_credit;
472 # record the account offset
473 Koha::Account::Offset->new(
474 { credit_id => $self->id,
475 debit_id => $debit->id,
476 amount => $amount_to_cancel * -1,
477 type => $offset_type,
481 $available_credit -= $amount_to_cancel;
483 $self->amountoutstanding( $available_credit * -1 )->store;
484 $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
486 # Same logic exists in Koha::Account::pay
487 if ( $debit->amountoutstanding == 0
488 && $debit->itemnumber
489 && $debit->debit_type_code
490 && $debit->debit_type_code eq 'LOST' )
492 C4::Circulation::ReturnLostItem( $self->borrowernumber, $debit->itemnumber );
498 return $available_credit;
503 $credit_accountline->payout(
505 payout_type => $payout_type,
506 register_id => $register_id,
507 staff_id => $staff_id,
508 interface => 'intranet',
513 Used to 'pay out' a credit to a user.
515 Payout type may be one of any existing payment types
517 Returns the payout debit line that is created via this transaction.
522 my ( $self, $params ) = @_;
524 # Make sure it is a credit we are paying out
525 unless ( $self->is_credit ) {
526 Koha::Exceptions::Account::IsNotCredit->throw(
527 error => 'Account line ' . $self->id . ' is not a credit' );
530 # Check for mandatory parameters
532 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
533 for my $param (@mandatory) {
534 unless ( defined( $params->{$param} ) ) {
535 Koha::Exceptions::MissingParameter->throw(
536 error => "The $param parameter is mandatory" );
540 # Make sure there is outstanding credit to pay out
541 my $outstanding = -1 * $self->amountoutstanding;
543 $params->{amount} ? $params->{amount} : $outstanding;
544 Koha::Exceptions::Account::AmountNotPositive->throw(
545 error => 'Payout amount passed is not positive' )
546 unless ( $amount > 0 );
547 Koha::Exceptions::ParameterTooHigh->throw(
548 error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
549 unless ($outstanding >= $amount );
551 # Make sure we record the cash register for cash transactions
552 Koha::Exceptions::Account::RegisterRequired->throw()
553 if ( C4::Context->preference("UseCashRegisters")
554 && defined( $params->{payout_type} )
555 && ( $params->{payout_type} eq 'CASH' )
556 && !defined( $params->{cash_register} ) );
559 $self->_result->result_source->schema->txn_do(
562 # A 'payout' is a 'debit'
563 $payout = Koha::Account::Line->new(
567 debit_type_code => 'PAYOUT',
568 payment_type => $params->{payout_type},
569 amountoutstanding => $amount,
570 manager_id => $params->{staff_id},
571 borrowernumber => $self->borrowernumber,
572 interface => $params->{interface},
573 branchcode => $params->{branch},
574 register_id => $params->{cash_register}
578 my $payout_offset = Koha::Account::Offset->new(
580 debit_id => $payout->accountlines_id,
586 $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
587 $self->status('PAID')->store;
591 $payout->discard_changes;
597 This method allows updating a debit or credit on a patron's account
599 $account_line->adjust(
602 type => $update_type,
603 interface => $interface
607 $update_type can be any of:
610 Authors Note: The intention here is that this method is only used
611 to adjust accountlines where the final amount is not yet known/fixed.
612 Incrementing fines are the only existing case at the time of writing,
613 all other forms of 'adjustment' should be recorded as distinct credits
614 or debits and applied, via an offset, to the corresponding debit or credit.
619 my ( $self, $params ) = @_;
621 my $amount = $params->{amount};
622 my $update_type = $params->{type};
623 my $interface = $params->{interface};
625 unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
626 Koha::Exceptions::Account::UnrecognisedType->throw(
627 error => 'Update type not recognised'
631 my $debit_type_code = $self->debit_type_code;
632 my $account_status = $self->status;
636 $Koha::Account::Line::allowed_update->{$update_type}
639 && ( $Koha::Account::Line::allowed_update->{$update_type}
640 ->{$debit_type_code} eq $account_status )
644 Koha::Exceptions::Account::UnrecognisedType->throw(
645 error => 'Update type not allowed on this debit_type' );
648 my $schema = Koha::Database->new->schema;
653 my $amount_before = $self->amount;
654 my $amount_outstanding_before = $self->amountoutstanding;
655 my $difference = $amount - $amount_before;
656 my $new_outstanding = $amount_outstanding_before + $difference;
658 my $offset_type = $debit_type_code;
659 $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
661 # Catch cases that require patron refunds
662 if ( $new_outstanding < 0 ) {
664 Koha::Patrons->find( $self->borrowernumber )->account;
665 my $credit = $account->add_credit(
667 amount => $new_outstanding * -1,
668 description => 'Overpayment refund',
670 interface => $interface,
671 ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
674 $new_outstanding = 0;
677 # Update the account line
682 amountoutstanding => $new_outstanding,
686 # Record the account offset
687 my $account_offset = Koha::Account::Offset->new(
689 debit_id => $self->id,
690 type => $offset_type,
691 amount => $difference
695 if ( C4::Context->preference("FinesLog") ) {
697 "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
698 $self->borrowernumber,
700 { action => $update_type,
701 borrowernumber => $self->borrowernumber,
703 description => undef,
704 amountoutstanding => $new_outstanding,
705 debit_type_code => $self->debit_type_code,
707 itemnumber => $self->itemnumber,
711 ) if ( $update_type eq 'overdue_update' );
721 my $bool = $line->is_credit;
728 return ( $self->amount < 0 );
733 my $bool = $line->is_debit;
740 return !$self->is_credit;
743 =head3 to_api_mapping
745 This method returns the mapping for representing a Koha::Account::Line object
752 accountlines_id => 'account_line_id',
753 credit_type_code => 'credit_type',
754 debit_type_code => 'debit_type',
755 amountoutstanding => 'amount_outstanding',
756 borrowernumber => 'patron_id',
757 branchcode => 'library_id',
758 issue_id => 'checkout_id',
759 itemnumber => 'item_id',
760 manager_id => 'user_id',
761 note => 'internal_note',
765 =head2 Internal methods
774 return 'Accountline';
781 =head3 $allowed_update
785 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
789 Kyle M Hall <kyle@bywatersolutions.com >
790 Tomás Cohen Arazi <tomascohen@theke.io>
791 Martin Renvoize <martin.renvoize@ptfs-europe.com>