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