bd233e3b36dd76a7de8692ae280f57694f053ff7
[srvgit] / Koha / Account / Line.pm
1 package Koha::Account::Line;
2
3 # This file is part of Koha.
4 #
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.
9 #
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.
14 #
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>.
17
18 use Modern::Perl;
19
20 use Carp;
21 use Data::Dumper;
22
23 use C4::Log qw(logaction);
24 use C4::Overdues qw(GetFine);
25
26 use Koha::Account::CreditType;
27 use Koha::Account::DebitType;
28 use Koha::Account::Offsets;
29 use Koha::Database;
30 use Koha::DateUtils;
31 use Koha::Exceptions::Account;
32 use Koha::Items;
33
34 use base qw(Koha::Object);
35
36 =encoding utf8
37
38 =head1 NAME
39
40 Koha::Account::Line - Koha accountline Object class
41
42 =head1 API
43
44 =head2 Class methods
45
46 =cut
47
48 =head3 patron
49
50 Return the patron linked to this account line
51
52 =cut
53
54 sub patron {
55     my ( $self ) = @_;
56     my $rs = $self->_result->borrowernumber;
57     return unless $rs;
58     return Koha::Patron->_new_from_dbic( $rs );
59 }
60
61 =head3 item
62
63 Return the item linked to this account line if exists
64
65 =cut
66
67 sub item {
68     my ( $self ) = @_;
69     my $rs = $self->_result->itemnumber;
70     return unless $rs;
71     return Koha::Item->_new_from_dbic( $rs );
72 }
73
74 =head3 checkout
75
76 Return the checkout linked to this account line if exists
77
78 =cut
79
80 sub checkout {
81     my ( $self ) = @_;
82     return unless $self->issue_id ;
83
84     $self->{_checkout} ||= Koha::Checkouts->find( $self->issue_id );
85     $self->{_checkout} ||= Koha::Old::Checkouts->find( $self->issue_id );
86     return $self->{_checkout};
87 }
88
89 =head3 library
90
91 Returns a Koha::Library object representing where the accountline was recorded
92
93 =cut
94
95 sub library {
96     my ( $self ) = @_;
97     my $rs = $self->_result->library;
98     return unless $rs;
99     return Koha::Library->_new_from_dbic($rs);
100 }
101
102 =head3 credit_type
103
104 Return the credit_type linked to this account line
105
106 =cut
107
108 sub credit_type {
109     my ( $self ) = @_;
110     my $rs = $self->_result->credit_type_code;
111     return unless $rs;
112     return Koha::Account::CreditType->_new_from_dbic( $rs );
113 }
114
115 =head3 debit_type
116
117 Return the debit_type linked to this account line
118
119 =cut
120
121 sub debit_type {
122     my ( $self ) = @_;
123     my $rs = $self->_result->debit_type_code;
124     return unless $rs;
125     return Koha::Account::DebitType->_new_from_dbic( $rs );
126 }
127
128 =head3 credit_offsets
129
130 Return the credit_offsets linked to this account line if some exist
131
132 =cut
133
134 sub credit_offsets {
135     my ( $self ) = @_;
136     my $rs = $self->_result->account_offsets_credits;
137     return unless $rs;
138     return Koha::Account::Offsets->_new_from_dbic($rs);
139 }
140
141 =head3 debit_offsets
142
143 Return the debit_offsets linked to this account line if some exist
144
145 =cut
146
147 sub debit_offsets {
148     my ( $self ) = @_;
149     my $rs = $self->_result->account_offsets_debits;
150     return unless $rs;
151     return Koha::Account::Offsets->_new_from_dbic($rs);
152 }
153
154
155 =head3 credits
156
157   my $credits = $accountline->credits;
158   my $credits = $accountline->credits( $cond, $attr );
159
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.
163
164 =cut
165
166 sub credits {
167     my ( $self, $cond, $attr ) = @_;
168
169     unless ( $self->is_debit ) {
170         Koha::Exceptions::Account::IsNotDebit->throw(
171             error => 'Account line ' . $self->id . ' is not a debit'
172         );
173     }
174
175     my $cond_m = { map { "credit.".$_ => $cond->{$_} } keys %{$cond}};
176     my $rs =
177       $self->_result->search_related('account_offsets_debits')
178       ->search_related( 'credit', $cond_m, $attr );
179     return unless $rs;
180     return Koha::Account::Lines->_new_from_dbic($rs);
181 }
182
183 =head3 debits
184
185   my $debits = $accountline->debits;
186   my $debits = $accountline->debits( $cond, $attr );
187
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.
191
192 =cut
193
194 sub debits {
195     my ( $self, $cond, $attr ) = @_;
196
197     unless ( $self->is_credit ) {
198         Koha::Exceptions::Account::IsNotCredit->throw(
199             error => 'Account line ' . $self->id . ' is not a credit'
200         );
201     }
202
203     my $cond_m = { map { "debit.".$_ => $cond->{$_} } keys %{$cond}};
204     my $rs =
205       $self->_result->search_related('account_offsets_credits')
206       ->search_related( 'debit', $cond_m, $attr );
207     return unless $rs;
208     return Koha::Account::Lines->_new_from_dbic($rs);
209 }
210
211 =head3 void
212
213   $payment_accountline->void({
214       interface => $interface,
215       [ staff_id => $staff_id, branch => $branchcode ]
216   });
217
218 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
219 created by the application of this credit upon any debits and mark the credit
220 as 'void' by updating it's status to "VOID".
221
222 =cut
223
224 sub void {
225     my ($self, $params) = @_;
226
227     # Make sure it is a credit we are voiding
228     unless ( $self->is_credit ) {
229         Koha::Exceptions::Account::IsNotCredit->throw(
230             error => 'Account line ' . $self->id . 'is not a credit' );
231     }
232
233     # Make sure it is not already voided
234     if ( $self->status && $self->status eq 'VOID' ) {
235         Koha::Exceptions::Account->throw(
236             error => 'Account line ' . $self->id . 'is already void' );
237     }
238
239     # Check for mandatory parameters
240     my @mandatory = ( 'interface' );
241     for my $param (@mandatory) {
242         unless ( defined( $params->{$param} ) ) {
243             Koha::Exceptions::MissingParameter->throw(
244                 error => "The $param parameter is mandatory" );
245         }
246     }
247
248     # More mandatory parameters
249     if ( $params->{interface} eq 'intranet' ) {
250         my @optional = ( 'staff_id', 'branch' );
251         for my $param (@optional) {
252             unless ( defined( $params->{$param} ) ) {
253                 Koha::Exceptions::MissingParameter->throw( error =>
254 "The $param parameter is mandatory when interface is set to 'intranet'"
255                 );
256             }
257         }
258     }
259
260     # Find any applied offsets for the credit so we may reverse them
261     my @account_offsets =
262       Koha::Account::Offsets->search(
263         { credit_id => $self->id, amount => { '<' => 0 }  } );
264
265     my $void;
266     $self->_result->result_source->schema->txn_do(
267         sub {
268
269             # A 'void' is a 'debit'
270             $void = Koha::Account::Line->new(
271                 {
272                     borrowernumber    => $self->borrowernumber,
273                     date              => \'NOW()',
274                     debit_type_code   => 'VOID',
275                     amount            => $self->amount * -1,
276                     amountoutstanding => $self->amount * -1,
277                     manager_id        => $params->{staff_id},
278                     interface         => $params->{interface},
279                     branchcode        => $params->{branch},
280                 }
281             )->store();
282
283             # Record the creation offset
284             Koha::Account::Offset->new(
285                 {
286                     debit_id => $void->id,
287                     type     => 'VOID',
288                     amount   => $self->amount * -1
289                 }
290             )->store();
291
292             # Reverse any applied payments
293             foreach my $account_offset (@account_offsets) {
294                 my $fee_paid =
295                   Koha::Account::Lines->find( $account_offset->debit_id );
296
297                 next unless $fee_paid;
298
299                 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
300                 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
301                 $fee_paid->amountoutstanding($new_amount);
302                 $fee_paid->store();
303
304                 Koha::Account::Offset->new(
305                     {
306                         credit_id => $self->id,
307                         debit_id  => $fee_paid->id,
308                         amount    => $amount_paid,
309                         type      => 'VOID',
310                     }
311                 )->store();
312             }
313
314             # Link void to payment
315             $self->set({
316                 amountoutstanding => $self->amount,
317                 status => 'VOID'
318             })->store();
319             $self->apply({ debits => [$void]});
320
321             if ( C4::Context->preference("FinesLog") ) {
322                 logaction(
323                     "FINES", 'VOID',
324                     $self->borrowernumber,
325                     Dumper(
326                         {
327                             action         => 'void_payment',
328                             borrowernumber => $self->borrowernumber,
329                             amount            => $self->amount,
330                             amountoutstanding => $self->amountoutstanding,
331                             description       => $self->description,
332                             credit_type_code  => $self->credit_type_code,
333                             payment_type      => $self->payment_type,
334                             note              => $self->note,
335                             itemnumber        => $self->itemnumber,
336                             manager_id        => $self->manager_id,
337                             offsets =>
338                               [ map { $_->unblessed } @account_offsets ],
339                         }
340                     )
341                 );
342             }
343         }
344     );
345
346     $void->discard_changes;
347     return $void;
348 }
349
350 =head3 cancel
351
352   $debit_accountline->cancel();
353
354 Cancel a charge. It will mark the debit as 'cancelled' by updating its
355 status to 'CANCELLED'.
356
357 Charges that have been fully or partially paid cannot be cancelled.
358
359 Returns the cancellation accountline.
360
361 =cut
362
363 sub cancel {
364     my ( $self, $params ) = @_;
365
366     # Make sure it is a charge we are reducing
367     unless ( $self->is_debit ) {
368         Koha::Exceptions::Account::IsNotDebit->throw(
369             error => 'Account line ' . $self->id . 'is not a debit' );
370     }
371     if ( $self->debit_type_code eq 'PAYOUT' ) {
372         Koha::Exceptions::Account::IsNotDebit->throw(
373             error => 'Account line ' . $self->id . 'is a payout' );
374     }
375
376     # Make sure it is not already cancelled
377     if ( $self->status && $self->status eq 'CANCELLED' ) {
378         Koha::Exceptions::Account->throw(
379             error => 'Account line ' . $self->id . 'is already cancelled' );
380     }
381
382     # Make sure it has not be paid yet
383     if ( $self->amount != $self->amountoutstanding ) {
384         Koha::Exceptions::Account->throw(
385             error => 'Account line ' . $self->id . 'is already offset' );
386     }
387
388     # Check for mandatory parameters
389     my @mandatory = ( 'staff_id', 'branch' );
390     for my $param (@mandatory) {
391         unless ( defined( $params->{$param} ) ) {
392             Koha::Exceptions::MissingParameter->throw(
393                 error => "The $param parameter is mandatory" );
394         }
395     }
396
397     my $cancellation;
398     $self->_result->result_source->schema->txn_do(
399         sub {
400
401             # A 'cancellation' is a 'credit'
402             $cancellation = Koha::Account::Line->new(
403                 {
404                     date              => \'NOW()',
405                     amount            => 0 - $self->amount,
406                     credit_type_code  => 'CANCELLATION',
407                     status            => 'ADDED',
408                     amountoutstanding => 0 - $self->amount,
409                     manager_id        => $params->{staff_id},
410                     borrowernumber    => $self->borrowernumber,
411                     interface         => 'intranet',
412                     branchcode        => $params->{branch},
413                 }
414             )->store();
415
416             my $cancellation_offset = Koha::Account::Offset->new(
417                 {
418                     credit_id => $cancellation->accountlines_id,
419                     type      => 'CANCELLATION',
420                     amount    => $self->amount
421                 }
422             )->store();
423
424             # Link cancellation to charge
425             $cancellation->apply(
426                 {
427                     debits      => [$self],
428                     offset_type => 'CANCELLATION'
429                 }
430             );
431             $cancellation->status('APPLIED')->store();
432
433             # Update status of original debit
434             $self->status('CANCELLED')->store;
435         }
436     );
437
438     $cancellation->discard_changes;
439     return $cancellation;
440 }
441
442 =head3 reduce
443
444   $charge_accountline->reduce({
445       reduction_type => $reduction_type
446   });
447
448 Used to 'reduce' a charge/debit by adding a credit to offset against the amount
449 outstanding.
450
451 May be used to apply a discount whilst retaining the original debit amounts or
452 to apply a full or partial refund for example when a lost item is found and
453 returned.
454
455 It will immediately be applied to the given debit unless the debit has already
456 been paid, in which case a 'zero' offset will be added to maintain a link to
457 the debit but the outstanding credit will be left so it may be applied to other
458 debts.
459
460 Reduction type may be one of:
461
462 * REFUND
463 * DISCOUNT
464
465 Returns the reduction accountline (which will be a credit)
466
467 =cut
468
469 sub reduce {
470     my ( $self, $params ) = @_;
471
472     # Make sure it is a charge we are reducing
473     unless ( $self->is_debit ) {
474         Koha::Exceptions::Account::IsNotDebit->throw(
475             error => 'Account line ' . $self->id . 'is not a debit' );
476     }
477     if ( $self->debit_type_code eq 'PAYOUT' ) {
478         Koha::Exceptions::Account::IsNotDebit->throw(
479             error => 'Account line ' . $self->id . 'is a payout' );
480     }
481
482     # Check for mandatory parameters
483     my @mandatory = ( 'interface', 'reduction_type', 'amount' );
484     for my $param (@mandatory) {
485         unless ( defined( $params->{$param} ) ) {
486             Koha::Exceptions::MissingParameter->throw(
487                 error => "The $param parameter is mandatory" );
488         }
489     }
490
491     # More mandatory parameters
492     if ( $params->{interface} eq 'intranet' ) {
493         my @optional = ( 'staff_id', 'branch' );
494         for my $param (@optional) {
495             unless ( defined( $params->{$param} ) ) {
496                 Koha::Exceptions::MissingParameter->throw( error =>
497 "The $param parameter is mandatory when interface is set to 'intranet'"
498                 );
499             }
500         }
501     }
502
503     # Make sure the reduction isn't more than the original
504     my $original = $self->amount;
505     Koha::Exceptions::Account::AmountNotPositive->throw(
506         error => 'Reduce amount passed is not positive' )
507       unless ( $params->{amount} > 0 );
508     Koha::Exceptions::ParameterTooHigh->throw( error =>
509 "Amount to reduce ($params->{amount}) is higher than original amount ($original)"
510     ) unless ( $original >= $params->{amount} );
511     my $reduced =
512       $self->credits( { credit_type_code => [ 'DISCOUNT', 'REFUND' ] } )->total;
513     Koha::Exceptions::ParameterTooHigh->throw( error =>
514 "Combined reduction ($params->{amount} + $reduced) is higher than original amount ("
515           . abs($original)
516           . ")" )
517       unless ( $original >= ( $params->{amount} + abs($reduced) ) );
518
519     my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' };
520
521     my $reduction;
522     $self->_result->result_source->schema->txn_do(
523         sub {
524
525             # A 'reduction' is a 'credit'
526             $reduction = Koha::Account::Line->new(
527                 {
528                     date              => \'NOW()',
529                     amount            => 0 - $params->{amount},
530                     credit_type_code  => $params->{reduction_type},
531                     status            => 'ADDED',
532                     amountoutstanding => 0 - $params->{amount},
533                     manager_id        => $params->{staff_id},
534                     borrowernumber    => $self->borrowernumber,
535                     interface         => $params->{interface},
536                     branchcode        => $params->{branch},
537                 }
538             )->store();
539
540             my $reduction_offset = Koha::Account::Offset->new(
541                 {
542                     credit_id => $reduction->accountlines_id,
543                     type      => uc( $params->{reduction_type} ),
544                     amount    => $params->{amount}
545                 }
546             )->store();
547
548             # Link reduction to charge (and apply as required)
549             my $debit_outstanding = $self->amountoutstanding;
550             if ( $debit_outstanding >= $params->{amount} ) {
551
552                 $reduction->apply(
553                     {
554                         debits      => [$self],
555                         offset_type => uc( $params->{reduction_type} )
556                     }
557                 );
558                 $reduction->status('APPLIED')->store();
559             }
560             else {
561
562                 # Zero amount offset used to link original 'debit' to
563                 # reduction 'credit'
564                 my $link_reduction_offset = Koha::Account::Offset->new(
565                     {
566                         credit_id => $reduction->accountlines_id,
567                         debit_id  => $self->accountlines_id,
568                         type      => uc( $params->{reduction_type} ),
569                         amount    => 0
570                     }
571                 )->store();
572             }
573
574             # Update status of original debit
575             $self->status( $status->{ $params->{reduction_type} } )->store;
576         }
577     );
578
579     $reduction->discard_changes;
580     return $reduction;
581 }
582
583 =head3 apply
584
585     my $debits = $account->outstanding_debits;
586     my $credit = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
587
588 Applies the credit to a given debits array reference.
589
590 =head4 arguments hashref
591
592 =over 4
593
594 =item debits - Koha::Account::Lines object set of debits
595
596 =item offset_type (optional) - a string indicating the offset type (valid values are those from
597 the 'account_offset_types' table)
598
599 =back
600
601 =cut
602
603 sub apply {
604     my ( $self, $params ) = @_;
605
606     my $debits      = $params->{debits};
607     my $offset_type = $params->{offset_type} // 'Credit Applied';
608
609     unless ( $self->is_credit ) {
610         Koha::Exceptions::Account::IsNotCredit->throw(
611             error => 'Account line ' . $self->id . ' is not a credit'
612         );
613     }
614
615     my $available_credit = $self->amountoutstanding * -1;
616
617     unless ( $available_credit > 0 ) {
618         Koha::Exceptions::Account::NoAvailableCredit->throw(
619             error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
620         );
621     }
622
623     my $schema = Koha::Database->new->schema;
624
625     $schema->txn_do( sub {
626         for my $debit ( @{$debits} ) {
627
628             unless ( $debit->is_debit ) {
629                 Koha::Exceptions::Account::IsNotDebit->throw(
630                     error => 'Account line ' . $debit->id . 'is not a debit'
631                 );
632             }
633             my $amount_to_cancel;
634             my $owed = $debit->amountoutstanding;
635
636             if ( $available_credit >= $owed ) {
637                 $amount_to_cancel = $owed;
638             }
639             else {    # $available_credit < $debit->amountoutstanding
640                 $amount_to_cancel = $available_credit;
641             }
642
643             # record the account offset
644             Koha::Account::Offset->new(
645                 {   credit_id => $self->id,
646                     debit_id  => $debit->id,
647                     amount    => $amount_to_cancel * -1,
648                     type      => $offset_type,
649                 }
650             )->store();
651
652             $available_credit -= $amount_to_cancel;
653
654             $self->amountoutstanding( $available_credit * -1 )->store;
655             $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
656
657             # Attempt to renew the item associated with this debit if
658             # appropriate
659             if ( $self->credit_type_code ne 'FORGIVEN' && $debit->is_renewable ) {
660                 my $outcome = $debit->renew_item( { interface => $params->{interface} } );
661                 $self->add_message(
662                     {
663                         type    => 'info',
664                         message => 'renewal',
665                         payload => $outcome
666                     }
667                 ) if $outcome;
668             }
669             $debit->discard_changes; # Refresh values from DB to clear floating point remainders
670
671             # Same logic exists in Koha::Account::pay
672             if (
673                 C4::Context->preference('MarkLostItemsAsReturned') =~
674                 m|onpayment|
675                 && $debit->debit_type_code
676                 && $debit->debit_type_code eq 'LOST'
677                 && $debit->amountoutstanding == 0
678                 && $debit->itemnumber
679                 && !(
680                        $self->credit_type_code eq 'LOST_FOUND'
681                     && $self->itemnumber == $debit->itemnumber
682                 )
683               )
684             {
685                 C4::Circulation::ReturnLostItem( $self->borrowernumber,
686                     $debit->itemnumber );
687             }
688
689             last if $available_credit == 0;
690         }
691     });
692
693     return $self;
694 }
695
696 =head3 payout
697
698   $credit_accountline->payout(
699     {
700         payout_type => $payout_type,
701         register_id => $register_id,
702         staff_id    => $staff_id,
703         interface   => 'intranet',
704         amount      => $amount
705     }
706   );
707
708 Used to 'pay out' a credit to a user.
709
710 Payout type may be one of any existing payment types
711
712 Returns the payout debit line that is created via this transaction.
713
714 =cut
715
716 sub payout {
717     my ( $self, $params ) = @_;
718
719     # Make sure it is a credit we are paying out
720     unless ( $self->is_credit ) {
721         Koha::Exceptions::Account::IsNotCredit->throw(
722             error => 'Account line ' . $self->id . ' is not a credit' );
723     }
724
725     # Check for mandatory parameters
726     my @mandatory =
727       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
728     for my $param (@mandatory) {
729         unless ( defined( $params->{$param} ) ) {
730             Koha::Exceptions::MissingParameter->throw(
731                 error => "The $param parameter is mandatory" );
732         }
733     }
734
735     # Make sure there is outstanding credit to pay out
736     my $outstanding = -1 * $self->amountoutstanding;
737     my $amount =
738       $params->{amount} ? $params->{amount} : $outstanding;
739     Koha::Exceptions::Account::AmountNotPositive->throw(
740         error => 'Payout amount passed is not positive' )
741       unless ( $amount > 0 );
742     Koha::Exceptions::ParameterTooHigh->throw(
743         error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
744       unless ($outstanding >= $amount );
745
746     # Make sure we record the cash register for cash transactions
747     Koha::Exceptions::Account::RegisterRequired->throw()
748       if ( C4::Context->preference("UseCashRegisters")
749         && defined( $params->{payout_type} )
750         && ( $params->{payout_type} eq 'CASH' )
751         && !defined( $params->{cash_register} ) );
752
753     my $payout;
754     $self->_result->result_source->schema->txn_do(
755         sub {
756
757             # A 'payout' is a 'debit'
758             $payout = Koha::Account::Line->new(
759                 {
760                     date              => \'NOW()',
761                     amount            => $amount,
762                     debit_type_code   => 'PAYOUT',
763                     payment_type      => $params->{payout_type},
764                     amountoutstanding => $amount,
765                     manager_id        => $params->{staff_id},
766                     borrowernumber    => $self->borrowernumber,
767                     interface         => $params->{interface},
768                     branchcode        => $params->{branch},
769                     register_id       => $params->{cash_register}
770                 }
771             )->store();
772
773             my $payout_offset = Koha::Account::Offset->new(
774                 {
775                     debit_id => $payout->accountlines_id,
776                     type     => 'PAYOUT',
777                     amount   => $amount
778                 }
779             )->store();
780
781             $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
782             $self->status('PAID')->store;
783         }
784     );
785
786     $payout->discard_changes;
787     return $payout;
788 }
789
790 =head3 adjust
791
792 This method allows updating a debit or credit on a patron's account
793
794     $account_line->adjust(
795         {
796             amount    => $amount,
797             type      => $update_type,
798             interface => $interface
799         }
800     );
801
802 $update_type can be any of:
803   - overdue_update
804
805 Authors Note: The intention here is that this method is only used
806 to adjust accountlines where the final amount is not yet known/fixed.
807 Incrementing fines are the only existing case at the time of writing,
808 all other forms of 'adjustment' should be recorded as distinct credits
809 or debits and applied, via an offset, to the corresponding debit or credit.
810
811 =cut
812
813 sub adjust {
814     my ( $self, $params ) = @_;
815
816     my $amount       = $params->{amount};
817     my $update_type  = $params->{type};
818     my $interface    = $params->{interface};
819
820     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
821         Koha::Exceptions::Account::UnrecognisedType->throw(
822             error => 'Update type not recognised'
823         );
824     }
825
826     my $debit_type_code = $self->debit_type_code;
827     my $account_status  = $self->status;
828     unless (
829         (
830             exists(
831                 $Koha::Account::Line::allowed_update->{$update_type}
832                   ->{$debit_type_code}
833             )
834             && ( $Koha::Account::Line::allowed_update->{$update_type}
835                 ->{$debit_type_code} eq $account_status )
836         )
837       )
838     {
839         Koha::Exceptions::Account::UnrecognisedType->throw(
840             error => 'Update type not allowed on this debit_type' );
841     }
842
843     my $schema = Koha::Database->new->schema;
844
845     $schema->txn_do(
846         sub {
847
848             my $amount_before             = $self->amount;
849             my $amount_outstanding_before = $self->amountoutstanding;
850             my $difference                = $amount - $amount_before;
851             my $new_outstanding           = $amount_outstanding_before + $difference;
852
853             my $offset_type = $debit_type_code;
854             $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
855
856             # Catch cases that require patron refunds
857             if ( $new_outstanding < 0 ) {
858                 my $account =
859                   Koha::Patrons->find( $self->borrowernumber )->account;
860                 my $credit = $account->add_credit(
861                     {
862                         amount      => $new_outstanding * -1,
863                         type        => 'OVERPAYMENT',
864                         interface   => $interface,
865                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
866                     }
867                 );
868                 $new_outstanding = 0;
869             }
870
871             # Update the account line
872             $self->set(
873                 {
874                     date              => \'NOW()',
875                     amount            => $amount,
876                     amountoutstanding => $new_outstanding,
877                 }
878             )->store();
879
880             # Record the account offset
881             my $account_offset = Koha::Account::Offset->new(
882                 {
883                     debit_id => $self->id,
884                     type     => $offset_type,
885                     amount   => $difference
886                 }
887             )->store();
888
889             if ( C4::Context->preference("FinesLog") ) {
890                 logaction(
891                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
892                     $self->borrowernumber,
893                     Dumper(
894                         {   action            => $update_type,
895                             borrowernumber    => $self->borrowernumber,
896                             amount            => $amount,
897                             description       => undef,
898                             amountoutstanding => $new_outstanding,
899                             debit_type_code   => $self->debit_type_code,
900                             note              => undef,
901                             itemnumber        => $self->itemnumber,
902                             manager_id        => undef,
903                         }
904                     )
905                 ) if ( $update_type eq 'overdue_update' );
906             }
907         }
908     );
909
910     return $self;
911 }
912
913 =head3 is_credit
914
915     my $bool = $line->is_credit;
916
917 =cut
918
919 sub is_credit {
920     my ($self) = @_;
921
922     return defined $self->credit_type_code;
923 }
924
925 =head3 is_debit
926
927     my $bool = $line->is_debit;
928
929 =cut
930
931 sub is_debit {
932     my ($self) = @_;
933
934     return !$self->is_credit;
935 }
936
937 =head3 to_api_mapping
938
939 This method returns the mapping for representing a Koha::Account::Line object
940 on the API.
941
942 =cut
943
944 sub to_api_mapping {
945     return {
946         accountlines_id   => 'account_line_id',
947         credit_number     => undef,
948         credit_type_code  => 'credit_type',
949         debit_type_code   => 'debit_type',
950         amountoutstanding => 'amount_outstanding',
951         borrowernumber    => 'patron_id',
952         branchcode        => 'library_id',
953         issue_id          => 'checkout_id',
954         itemnumber        => 'item_id',
955         manager_id        => 'user_id',
956         note              => 'internal_note',
957         register_id       => 'cash_register_id',
958     };
959
960 }
961
962 =head3 is_renewable
963
964     my $bool = $line->is_renewable;
965
966 =cut
967
968 sub is_renewable {
969     my ($self) = @_;
970
971     return (
972         $self->amountoutstanding == 0 &&
973         $self->debit_type_code &&
974         $self->debit_type_code eq 'OVERDUE' &&
975         $self->status &&
976         $self->status eq 'UNRETURNED' &&
977         $self->item &&
978         $self->patron
979     ) ? 1 : 0;
980 }
981
982 =head3 renew_item
983
984     my $renew_result = $line->renew_item;
985
986 Conditionally attempt to renew an item and return the outcome. This is
987 as a consequence of the fine on an item being fully paid off.
988 Caller must call is_renewable before.
989
990 =cut
991
992 sub renew_item {
993     my ($self, $params) = @_;
994
995     my $outcome = {};
996
997     # We want to reject the call to renew if:
998     # - The RenewAccruingItemWhenPaid syspref is off
999     # OR
1000     # - The RenewAccruingItemInOpac syspref is off
1001     # - There is an interface param passed and it's value is 'opac'
1002
1003     if (
1004         !C4::Context->preference('RenewAccruingItemWhenPaid') ||
1005         (
1006             !C4::Context->preference('RenewAccruingItemInOpac') &&
1007             $params->{interface} &&
1008             $params->{interface} eq 'opac'
1009         )
1010     ) {
1011         return;
1012     }
1013
1014     my $itemnumber = $self->item->itemnumber;
1015     my $borrowernumber = $self->patron->borrowernumber;
1016     my ( $can_renew, $error ) = C4::Circulation::CanBookBeRenewed(
1017         $borrowernumber,
1018         $itemnumber
1019     );
1020     if ( $can_renew ) {
1021         my $due_date = C4::Circulation::AddRenewal(
1022             $borrowernumber,
1023             $itemnumber,
1024             $self->{branchcode},
1025             undef,
1026             undef,
1027             1
1028         );
1029         return {
1030             itemnumber => $itemnumber,
1031             due_date   => $due_date,
1032             success    => 1
1033         };
1034     } else {
1035         return {
1036             itemnumber => $itemnumber,
1037             error      => $error,
1038             success    => 0
1039         };
1040     }
1041
1042 }
1043
1044 =head3 store
1045
1046 Specific store method to generate credit number before saving
1047
1048 =cut
1049
1050 sub store {
1051     my ($self) = @_;
1052
1053     my $AutoCreditNumber = C4::Context->preference('AutoCreditNumber');
1054     my $credit_number_enabled = $self->is_credit && $self->credit_type->credit_number_enabled;
1055
1056     if ($AutoCreditNumber && $credit_number_enabled && !$self->in_storage) {
1057         if (defined $self->credit_number) {
1058             Koha::Exceptions::Account->throw('AutoCreditNumber is enabled but credit_number is already defined');
1059         }
1060
1061         my $rs = Koha::Database->new->schema->resultset($self->_type);
1062
1063         if ($AutoCreditNumber eq 'incremental') {
1064             my $max = $rs->search({
1065                 credit_number => { -regexp => '^[0-9]+$' }
1066             }, {
1067                 select => \'CAST(credit_number AS UNSIGNED)',
1068                 as => ['credit_number'],
1069             })->get_column('credit_number')->max;
1070             $max //= 0;
1071             $self->credit_number($max + 1);
1072         } elsif ($AutoCreditNumber eq 'annual') {
1073             my $now = dt_from_string;
1074             my $prefix = sprintf('%d-', $now->year);
1075             my $max = $rs->search({
1076                 -and => [
1077                     credit_number => { -regexp => '[0-9]{4}$' },
1078                     credit_number => { -like => "$prefix%" },
1079                 ],
1080             })->get_column('credit_number')->max;
1081             $max //= $prefix . '0000';
1082             my $incr = substr($max, length $prefix);
1083             $self->credit_number(sprintf('%s%04d', $prefix, $incr + 1));
1084         } elsif ($AutoCreditNumber eq 'branchyyyymmincr') {
1085             my $userenv = C4::Context->userenv;
1086             if ($userenv) {
1087                 my $branch = $userenv->{branch};
1088                 my $now = dt_from_string;
1089                 my $prefix = sprintf('%s%d%02d', $branch, $now->year, $now->month);
1090                 my $pattern = $prefix;
1091                 $pattern =~ s/([\?%_])/\\$1/g;
1092                 my $max = $rs->search({
1093                     -and => [
1094                         credit_number => { -regexp => '[0-9]{4}$' },
1095                         credit_number => { -like => "$pattern%" },
1096                     ],
1097                 })->get_column('credit_number')->max;
1098                 $max //= $prefix . '0000';
1099                 my $incr = substr($max, length $prefix);
1100                 $self->credit_number(sprintf('%s%04d', $prefix, $incr + 1));
1101             }
1102         }
1103     }
1104
1105     return $self->SUPER::store();
1106 }
1107
1108 =head2 Internal methods
1109
1110 =cut
1111
1112 =head3 _type
1113
1114 =cut
1115
1116 sub _type {
1117     return 'Accountline';
1118 }
1119
1120 1;
1121
1122 =head2 Name mappings
1123
1124 =head3 $allowed_update
1125
1126 =cut
1127
1128 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
1129
1130 =head1 AUTHORS
1131
1132 Kyle M Hall <kyle@bywatersolutions.com >
1133 Tomás Cohen Arazi <tomascohen@theke.io>
1134 Martin Renvoize <martin.renvoize@ptfs-europe.com>
1135
1136 =cut