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;
31 use Koha::Exceptions::Account;
34 use base qw(Koha::Object);
40 Koha::Account::Line - Koha accountline Object class
50 Return the patron linked to this account line
56 my $rs = $self->_result->borrowernumber;
58 return Koha::Patron->_new_from_dbic( $rs );
63 Return the item linked to this account line if exists
69 my $rs = $self->_result->itemnumber;
71 return Koha::Item->_new_from_dbic( $rs );
76 Return the checkout linked to this account line if exists
82 return unless $self->issue_id ;
84 $self->{_checkout} ||= Koha::Checkouts->find( $self->issue_id );
85 $self->{_checkout} ||= Koha::Old::Checkouts->find( $self->issue_id );
86 return $self->{_checkout};
91 Returns a Koha::Library object representing where the accountline was recorded
97 my $rs = $self->_result->library;
99 return Koha::Library->_new_from_dbic($rs);
104 Return the credit_type linked to this account line
110 my $rs = $self->_result->credit_type_code;
112 return Koha::Account::CreditType->_new_from_dbic( $rs );
117 Return the debit_type linked to this account line
123 my $rs = $self->_result->debit_type_code;
125 return Koha::Account::DebitType->_new_from_dbic( $rs );
128 =head3 credit_offsets
130 Return the credit_offsets linked to this account line if some exist
136 my $rs = $self->_result->account_offsets_credits;
138 return Koha::Account::Offsets->_new_from_dbic($rs);
143 Return the debit_offsets linked to this account line if some exist
149 my $rs = $self->_result->account_offsets_debits;
151 return Koha::Account::Offsets->_new_from_dbic($rs);
157 my $credits = $accountline->credits;
158 my $credits = $accountline->credits( $cond, $attr );
160 Return the credits linked to this account line if some exist.
161 Search conditions and attributes may be passed if you wish to filter
162 the resultant resultant resultset.
167 my ( $self, $cond, $attr ) = @_;
169 unless ( $self->is_debit ) {
170 Koha::Exceptions::Account::IsNotDebit->throw(
171 error => 'Account line ' . $self->id . ' is not a debit'
175 my $cond_m = { map { "credit.".$_ => $cond->{$_} } keys %{$cond}};
177 $self->_result->search_related('account_offsets_debits')
178 ->search_related( 'credit', $cond_m, $attr );
180 return Koha::Account::Lines->_new_from_dbic($rs);
185 my $debits = $accountline->debits;
186 my $debits = $accountline->debits( $cond, $attr );
188 Return the debits linked to this account line if some exist.
189 Search conditions and attributes may be passed if you wish to filter
190 the resultant resultant resultset.
195 my ( $self, $cond, $attr ) = @_;
197 unless ( $self->is_credit ) {
198 Koha::Exceptions::Account::IsNotCredit->throw(
199 error => 'Account line ' . $self->id . ' is not a credit'
203 my $cond_m = { map { "debit.".$_ => $cond->{$_} } keys %{$cond}};
205 $self->_result->search_related('account_offsets_credits')
206 ->search_related( 'debit', $cond_m, $attr );
208 return Koha::Account::Lines->_new_from_dbic($rs);
213 $payment_accountline->void();
215 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
216 created by the application of this credit upon any debits and mark the credit
217 as 'void' by updating it's status to "VOID".
224 # Make sure it is a payment we are voiding
225 return unless $self->amount < 0;
227 my @account_offsets =
228 Koha::Account::Offsets->search(
229 { credit_id => $self->id, amount => { '<' => 0 } } );
231 $self->_result->result_source->schema->txn_do(
233 foreach my $account_offset (@account_offsets) {
235 Koha::Account::Lines->find( $account_offset->debit_id );
237 next unless $fee_paid;
239 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
240 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
241 $fee_paid->amountoutstanding($new_amount);
244 Koha::Account::Offset->new(
246 credit_id => $self->id,
247 debit_id => $fee_paid->id,
248 amount => $amount_paid,
249 type => 'Void Payment',
254 if ( C4::Context->preference("FinesLog") ) {
257 $self->borrowernumber,
260 action => 'void_payment',
261 borrowernumber => $self->borrowernumber,
262 amount => $self->amount,
263 amountoutstanding => $self->amountoutstanding,
264 description => $self->description,
265 credit_type_code => $self->credit_type_code,
266 payment_type => $self->payment_type,
268 itemnumber => $self->itemnumber,
269 manager_id => $self->manager_id,
271 [ map { $_->unblessed } @account_offsets ],
280 amountoutstanding => 0,
292 $charge_accountline->reduce({
293 reduction_type => $reduction_type
296 Used to 'reduce' a charge/debit by adding a credit to offset against the amount
299 May be used to apply a discount whilst retaining the original debit amounts or
300 to apply a full or partial refund for example when a lost item is found and
303 It will immediately be applied to the given debit unless the debit has already
304 been paid, in which case a 'zero' offset will be added to maintain a link to
305 the debit but the outstanding credit will be left so it may be applied to other
308 Reduction type may be one of:
313 Returns the reduction accountline (which will be a credit)
318 my ( $self, $params ) = @_;
320 # Make sure it is a charge we are reducing
321 unless ( $self->is_debit ) {
322 Koha::Exceptions::Account::IsNotDebit->throw(
323 error => 'Account line ' . $self->id . 'is not a debit' );
325 if ( $self->debit_type_code eq 'PAYOUT' ) {
326 Koha::Exceptions::Account::IsNotDebit->throw(
327 error => 'Account line ' . $self->id . 'is a payout' );
330 # Check for mandatory parameters
331 my @mandatory = ( 'interface', 'reduction_type', 'amount' );
332 for my $param (@mandatory) {
333 unless ( defined( $params->{$param} ) ) {
334 Koha::Exceptions::MissingParameter->throw(
335 error => "The $param parameter is mandatory" );
339 # More mandatory parameters
340 if ( $params->{interface} eq 'intranet' ) {
341 my @optional = ( 'staff_id', 'branch' );
342 for my $param (@optional) {
343 unless ( defined( $params->{$param} ) ) {
344 Koha::Exceptions::MissingParameter->throw( error =>
345 "The $param parameter is mandatory when interface is set to 'intranet'"
351 # Make sure the reduction isn't more than the original
352 my $original = $self->amount;
353 Koha::Exceptions::Account::AmountNotPositive->throw(
354 error => 'Reduce amount passed is not positive' )
355 unless ( $params->{amount} > 0 );
356 Koha::Exceptions::ParameterTooHigh->throw( error =>
357 "Amount to reduce ($params->{amount}) is higher than original amount ($original)"
358 ) unless ( $original >= $params->{amount} );
360 $self->credits( { credit_type_code => [ 'DISCOUNT', 'REFUND' ] } )->total;
361 Koha::Exceptions::ParameterTooHigh->throw( error =>
362 "Combined reduction ($params->{amount} + $reduced) is higher than original amount ("
365 unless ( $original >= ( $params->{amount} + abs($reduced) ) );
367 my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' };
370 $self->_result->result_source->schema->txn_do(
373 # A 'reduction' is a 'credit'
374 $reduction = Koha::Account::Line->new(
377 amount => 0 - $params->{amount},
378 credit_type_code => $params->{reduction_type},
380 amountoutstanding => 0 - $params->{amount},
381 manager_id => $params->{staff_id},
382 borrowernumber => $self->borrowernumber,
383 interface => $params->{interface},
384 branchcode => $params->{branch},
388 my $reduction_offset = Koha::Account::Offset->new(
390 credit_id => $reduction->accountlines_id,
391 type => uc( $params->{reduction_type} ),
392 amount => $params->{amount}
396 # Link reduction to charge (and apply as required)
397 my $debit_outstanding = $self->amountoutstanding;
398 if ( $debit_outstanding >= $params->{amount} ) {
403 offset_type => uc( $params->{reduction_type} )
406 $reduction->status('APPLIED')->store();
410 # Zero amount offset used to link original 'debit' to reduction 'credit'
411 my $link_reduction_offset = Koha::Account::Offset->new(
413 credit_id => $reduction->accountlines_id,
414 debit_id => $self->accountlines_id,
415 type => uc( $params->{reduction_type} ),
421 # Update status of original debit
422 $self->status( $status->{ $params->{reduction_type} } )->store;
426 $reduction->discard_changes;
432 my $debits = $account->outstanding_debits;
433 my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
435 Applies the credit to a given debits array reference.
437 =head4 arguments hashref
441 =item debits - Koha::Account::Lines object set of debits
443 =item offset_type (optional) - a string indicating the offset type (valid values are those from
444 the 'account_offset_types' table)
451 my ( $self, $params ) = @_;
453 my $debits = $params->{debits};
454 my $offset_type = $params->{offset_type} // 'Credit Applied';
456 unless ( $self->is_credit ) {
457 Koha::Exceptions::Account::IsNotCredit->throw(
458 error => 'Account line ' . $self->id . ' is not a credit'
462 my $available_credit = $self->amountoutstanding * -1;
464 unless ( $available_credit > 0 ) {
465 Koha::Exceptions::Account::NoAvailableCredit->throw(
466 error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
470 my $schema = Koha::Database->new->schema;
472 $schema->txn_do( sub {
473 for my $debit ( @{$debits} ) {
475 unless ( $debit->is_debit ) {
476 Koha::Exceptions::Account::IsNotDebit->throw(
477 error => 'Account line ' . $debit->id . 'is not a debit'
480 my $amount_to_cancel;
481 my $owed = $debit->amountoutstanding;
483 if ( $available_credit >= $owed ) {
484 $amount_to_cancel = $owed;
486 else { # $available_credit < $debit->amountoutstanding
487 $amount_to_cancel = $available_credit;
490 # record the account offset
491 Koha::Account::Offset->new(
492 { credit_id => $self->id,
493 debit_id => $debit->id,
494 amount => $amount_to_cancel * -1,
495 type => $offset_type,
499 $available_credit -= $amount_to_cancel;
501 $self->amountoutstanding( $available_credit * -1 )->store;
502 $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
504 # Attempt to renew the item associated with this debit if
506 if ($debit->renewable) {
507 $debit->renew_item($params->{interface});
510 # Same logic exists in Koha::Account::pay
512 C4::Context->preference('MarkLostItemsAsReturned') =~
514 && $debit->debit_type_code
515 && $debit->debit_type_code eq 'LOST'
516 && $debit->amountoutstanding == 0
517 && $debit->itemnumber
519 $self->credit_type_code eq 'LOST_FOUND'
520 && $self->itemnumber == $debit->itemnumber
524 C4::Circulation::ReturnLostItem( $self->borrowernumber,
525 $debit->itemnumber );
530 return $available_credit;
535 $credit_accountline->payout(
537 payout_type => $payout_type,
538 register_id => $register_id,
539 staff_id => $staff_id,
540 interface => 'intranet',
545 Used to 'pay out' a credit to a user.
547 Payout type may be one of any existing payment types
549 Returns the payout debit line that is created via this transaction.
554 my ( $self, $params ) = @_;
556 # Make sure it is a credit we are paying out
557 unless ( $self->is_credit ) {
558 Koha::Exceptions::Account::IsNotCredit->throw(
559 error => 'Account line ' . $self->id . ' is not a credit' );
562 # Check for mandatory parameters
564 ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
565 for my $param (@mandatory) {
566 unless ( defined( $params->{$param} ) ) {
567 Koha::Exceptions::MissingParameter->throw(
568 error => "The $param parameter is mandatory" );
572 # Make sure there is outstanding credit to pay out
573 my $outstanding = -1 * $self->amountoutstanding;
575 $params->{amount} ? $params->{amount} : $outstanding;
576 Koha::Exceptions::Account::AmountNotPositive->throw(
577 error => 'Payout amount passed is not positive' )
578 unless ( $amount > 0 );
579 Koha::Exceptions::ParameterTooHigh->throw(
580 error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
581 unless ($outstanding >= $amount );
583 # Make sure we record the cash register for cash transactions
584 Koha::Exceptions::Account::RegisterRequired->throw()
585 if ( C4::Context->preference("UseCashRegisters")
586 && defined( $params->{payout_type} )
587 && ( $params->{payout_type} eq 'CASH' )
588 && !defined( $params->{cash_register} ) );
591 $self->_result->result_source->schema->txn_do(
594 # A 'payout' is a 'debit'
595 $payout = Koha::Account::Line->new(
599 debit_type_code => 'PAYOUT',
600 payment_type => $params->{payout_type},
601 amountoutstanding => $amount,
602 manager_id => $params->{staff_id},
603 borrowernumber => $self->borrowernumber,
604 interface => $params->{interface},
605 branchcode => $params->{branch},
606 register_id => $params->{cash_register}
610 my $payout_offset = Koha::Account::Offset->new(
612 debit_id => $payout->accountlines_id,
618 $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
619 $self->status('PAID')->store;
623 $payout->discard_changes;
629 This method allows updating a debit or credit on a patron's account
631 $account_line->adjust(
634 type => $update_type,
635 interface => $interface
639 $update_type can be any of:
642 Authors Note: The intention here is that this method is only used
643 to adjust accountlines where the final amount is not yet known/fixed.
644 Incrementing fines are the only existing case at the time of writing,
645 all other forms of 'adjustment' should be recorded as distinct credits
646 or debits and applied, via an offset, to the corresponding debit or credit.
651 my ( $self, $params ) = @_;
653 my $amount = $params->{amount};
654 my $update_type = $params->{type};
655 my $interface = $params->{interface};
657 unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
658 Koha::Exceptions::Account::UnrecognisedType->throw(
659 error => 'Update type not recognised'
663 my $debit_type_code = $self->debit_type_code;
664 my $account_status = $self->status;
668 $Koha::Account::Line::allowed_update->{$update_type}
671 && ( $Koha::Account::Line::allowed_update->{$update_type}
672 ->{$debit_type_code} eq $account_status )
676 Koha::Exceptions::Account::UnrecognisedType->throw(
677 error => 'Update type not allowed on this debit_type' );
680 my $schema = Koha::Database->new->schema;
685 my $amount_before = $self->amount;
686 my $amount_outstanding_before = $self->amountoutstanding;
687 my $difference = $amount - $amount_before;
688 my $new_outstanding = $amount_outstanding_before + $difference;
690 my $offset_type = $debit_type_code;
691 $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
693 # Catch cases that require patron refunds
694 if ( $new_outstanding < 0 ) {
696 Koha::Patrons->find( $self->borrowernumber )->account;
697 my $credit = $account->add_credit(
699 amount => $new_outstanding * -1,
700 type => 'OVERPAYMENT',
701 interface => $interface,
702 ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
705 $new_outstanding = 0;
708 # Update the account line
713 amountoutstanding => $new_outstanding,
717 # Record the account offset
718 my $account_offset = Koha::Account::Offset->new(
720 debit_id => $self->id,
721 type => $offset_type,
722 amount => $difference
726 if ( C4::Context->preference("FinesLog") ) {
728 "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
729 $self->borrowernumber,
731 { action => $update_type,
732 borrowernumber => $self->borrowernumber,
734 description => undef,
735 amountoutstanding => $new_outstanding,
736 debit_type_code => $self->debit_type_code,
738 itemnumber => $self->itemnumber,
742 ) if ( $update_type eq 'overdue_update' );
752 my $bool = $line->is_credit;
759 return ( $self->amount < 0 );
764 my $bool = $line->is_debit;
771 return !$self->is_credit;
774 =head3 to_api_mapping
776 This method returns the mapping for representing a Koha::Account::Line object
783 accountlines_id => 'account_line_id',
784 credit_type_code => 'credit_type',
785 debit_type_code => 'debit_type',
786 amountoutstanding => 'amount_outstanding',
787 borrowernumber => 'patron_id',
788 branchcode => 'library_id',
789 issue_id => 'checkout_id',
790 itemnumber => 'item_id',
791 manager_id => 'user_id',
792 note => 'internal_note',
799 my $bool = $line->renewable;
807 $self->amountoutstanding == 0 &&
808 $self->debit_type_code &&
809 $self->debit_type_code eq 'OVERDUE' &&
811 $self->status eq 'UNRETURNED'
817 my $renew_result = $line->renew_item;
819 Conditionally attempt to renew an item and return the outcome. This is
820 as a consequence of the fine on an item being fully paid off
825 my ($self, $params) = @_;
829 # We want to reject the call to renew if any of these apply:
830 # - The RenewAccruingItemWhenPaid syspref is off
831 # - The line item doesn't have an item attached to it
832 # - The line item doesn't have a patron attached to it
834 # - The RenewAccruingItemInOpac syspref is off
836 # - There is an interface param passed and it's value is 'opac'
839 !C4::Context->preference('RenewAccruingItemWhenPaid') ||
843 !C4::Context->preference('RenewAccruingItemInOpac') &&
844 $params->{interface} &&
845 $params->{interface} eq 'opac'
851 my $itemnumber = $self->item->itemnumber;
852 my $borrowernumber = $self->patron->borrowernumber;
853 my ( $can_renew, $error ) = C4::Circulation::CanBookBeRenewed(
858 my $due_date = C4::Circulation::AddRenewal(
867 itemnumber => $itemnumber,
868 due_date => $due_date,
873 itemnumber => $itemnumber,
883 Specific store method to generate credit number before saving
890 my $AutoCreditNumber = C4::Context->preference('AutoCreditNumber');
891 my $credit_number_enabled = $self->is_credit && $self->credit_type->credit_number_enabled;
893 if ($AutoCreditNumber && $credit_number_enabled && !$self->in_storage) {
894 if (defined $self->credit_number) {
895 Koha::Exceptions::Account->throw('AutoCreditNumber is enabled but credit_number is already defined');
898 my $rs = Koha::Database->new->schema->resultset($self->_type);
900 if ($AutoCreditNumber eq 'incremental') {
901 my $max = $rs->search({
902 credit_number => { -regexp => '^[0-9]+$' }
904 select => \'CAST(credit_number AS UNSIGNED)',
905 as => ['credit_number'],
906 })->get_column('credit_number')->max;
908 $self->credit_number($max + 1);
909 } elsif ($AutoCreditNumber eq 'annual') {
910 my $now = dt_from_string;
911 my $prefix = sprintf('%d-', $now->year);
912 my $max = $rs->search({
914 credit_number => { -regexp => '[0-9]{4}$' },
915 credit_number => { -like => "$prefix%" },
917 })->get_column('credit_number')->max;
918 $max //= $prefix . '0000';
919 my $incr = substr($max, length $prefix);
920 $self->credit_number(sprintf('%s%04d', $prefix, $incr + 1));
921 } elsif ($AutoCreditNumber eq 'branchyyyymmincr') {
922 my $userenv = C4::Context->userenv;
924 my $branch = $userenv->{branch};
925 my $now = dt_from_string;
926 my $prefix = sprintf('%s%d%02d', $branch, $now->year, $now->month);
927 my $pattern = $prefix;
928 $pattern =~ s/([\?%_])/\\$1/g;
929 my $max = $rs->search({
931 credit_number => { -regexp => '[0-9]{4}$' },
932 credit_number => { -like => "$pattern%" },
934 })->get_column('credit_number')->max;
935 $max //= $prefix . '0000';
936 my $incr = substr($max, length $prefix);
937 $self->credit_number(sprintf('%s%04d', $prefix, $incr + 1));
942 return $self->SUPER::store();
945 =head2 Internal methods
954 return 'Accountline';
961 =head3 $allowed_update
965 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
969 Kyle M Hall <kyle@bywatersolutions.com >
970 Tomás Cohen Arazi <tomascohen@theke.io>
971 Martin Renvoize <martin.renvoize@ptfs-europe.com>