Bug 23091: Make relations more explicit
[koha.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 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
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".
218
219 =cut
220
221 sub void {
222     my ($self) = @_;
223
224     # Make sure it is a payment we are voiding
225     return unless $self->amount < 0;
226
227     my @account_offsets =
228       Koha::Account::Offsets->search(
229         { credit_id => $self->id, amount => { '<' => 0 }  } );
230
231     $self->_result->result_source->schema->txn_do(
232         sub {
233             foreach my $account_offset (@account_offsets) {
234                 my $fee_paid =
235                   Koha::Account::Lines->find( $account_offset->debit_id );
236
237                 next unless $fee_paid;
238
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);
242                 $fee_paid->store();
243
244                 Koha::Account::Offset->new(
245                     {
246                         credit_id => $self->id,
247                         debit_id  => $fee_paid->id,
248                         amount    => $amount_paid,
249                         type      => 'Void Payment',
250                     }
251                 )->store();
252             }
253
254             if ( C4::Context->preference("FinesLog") ) {
255                 logaction(
256                     "FINES", 'VOID',
257                     $self->borrowernumber,
258                     Dumper(
259                         {
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,
267                             note              => $self->note,
268                             itemnumber        => $self->itemnumber,
269                             manager_id        => $self->manager_id,
270                             offsets =>
271                               [ map { $_->unblessed } @account_offsets ],
272                         }
273                     )
274                 );
275             }
276
277             $self->set(
278                 {
279                     status            => 'VOID',
280                     amountoutstanding => 0,
281                     amount            => 0,
282                 }
283             );
284             $self->store();
285         }
286     );
287
288 }
289
290 =head3 reduce
291
292   $charge_accountline->reduce({
293       reduction_type => $reduction_type
294   });
295
296 Used to 'reduce' a charge/debit by adding a credit to offset against the amount
297 outstanding.
298
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
301 returned.
302
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
306 debts.
307
308 Reduction type may be one of:
309
310 * REFUND
311 * DISCOUNT
312
313 Returns the reduction accountline (which will be a credit)
314
315 =cut
316
317 sub reduce {
318     my ( $self, $params ) = @_;
319
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' );
324     }
325     if ( $self->debit_type_code eq 'PAYOUT' ) {
326         Koha::Exceptions::Account::IsNotDebit->throw(
327             error => 'Account line ' . $self->id . 'is a payout' );
328     }
329
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" );
336         }
337     }
338
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'"
346                 );
347             }
348         }
349     }
350
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} );
359     my $reduced =
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 ("
363           . abs($original)
364           . ")" )
365       unless ( $original >= ( $params->{amount} + abs($reduced) ) );
366
367     my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' };
368
369     my $reduction;
370     $self->_result->result_source->schema->txn_do(
371         sub {
372
373             # A 'reduction' is a 'credit'
374             $reduction = Koha::Account::Line->new(
375                 {
376                     date              => \'NOW()',
377                     amount            => 0 - $params->{amount},
378                     credit_type_code  => $params->{reduction_type},
379                     status            => 'ADDED',
380                     amountoutstanding => 0 - $params->{amount},
381                     manager_id        => $params->{staff_id},
382                     borrowernumber    => $self->borrowernumber,
383                     interface         => $params->{interface},
384                     branchcode        => $params->{branch},
385                 }
386             )->store();
387
388             my $reduction_offset = Koha::Account::Offset->new(
389                 {
390                     credit_id => $reduction->accountlines_id,
391                     type      => uc( $params->{reduction_type} ),
392                     amount    => $params->{amount}
393                 }
394             )->store();
395
396             # Link reduction to charge (and apply as required)
397             my $debit_outstanding = $self->amountoutstanding;
398             if ( $debit_outstanding >= $params->{amount} ) {
399
400                 $reduction->apply(
401                     {
402                         debits      => [$self],
403                         offset_type => uc( $params->{reduction_type} )
404                     }
405                 );
406                 $reduction->status('APPLIED')->store();
407             }
408             else {
409
410         # Zero amount offset used to link original 'debit' to reduction 'credit'
411                 my $link_reduction_offset = Koha::Account::Offset->new(
412                     {
413                         credit_id => $reduction->accountlines_id,
414                         debit_id  => $self->accountlines_id,
415                         type      => uc( $params->{reduction_type} ),
416                         amount    => 0
417                     }
418                 )->store();
419             }
420
421             # Update status of original debit
422             $self->status( $status->{ $params->{reduction_type} } )->store;
423         }
424     );
425
426     $reduction->discard_changes;
427     return $reduction;
428 }
429
430 =head3 apply
431
432     my $debits = $account->outstanding_debits;
433     my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
434
435 Applies the credit to a given debits array reference.
436
437 =head4 arguments hashref
438
439 =over 4
440
441 =item debits - Koha::Account::Lines object set of debits
442
443 =item offset_type (optional) - a string indicating the offset type (valid values are those from
444 the 'account_offset_types' table)
445
446 =back
447
448 =cut
449
450 sub apply {
451     my ( $self, $params ) = @_;
452
453     my $debits      = $params->{debits};
454     my $offset_type = $params->{offset_type} // 'Credit Applied';
455
456     unless ( $self->is_credit ) {
457         Koha::Exceptions::Account::IsNotCredit->throw(
458             error => 'Account line ' . $self->id . ' is not a credit'
459         );
460     }
461
462     my $available_credit = $self->amountoutstanding * -1;
463
464     unless ( $available_credit > 0 ) {
465         Koha::Exceptions::Account::NoAvailableCredit->throw(
466             error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
467         );
468     }
469
470     my $schema = Koha::Database->new->schema;
471
472     $schema->txn_do( sub {
473         for my $debit ( @{$debits} ) {
474
475             unless ( $debit->is_debit ) {
476                 Koha::Exceptions::Account::IsNotDebit->throw(
477                     error => 'Account line ' . $debit->id . 'is not a debit'
478                 );
479             }
480             my $amount_to_cancel;
481             my $owed = $debit->amountoutstanding;
482
483             if ( $available_credit >= $owed ) {
484                 $amount_to_cancel = $owed;
485             }
486             else {    # $available_credit < $debit->amountoutstanding
487                 $amount_to_cancel = $available_credit;
488             }
489
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,
496                 }
497             )->store();
498
499             $available_credit -= $amount_to_cancel;
500
501             $self->amountoutstanding( $available_credit * -1 )->store;
502             $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
503
504             # Attempt to renew the item associated with this debit if
505             # appropriate
506             if ($debit->renewable) {
507                 $debit->renew_item($params->{interface});
508             }
509
510             # Same logic exists in Koha::Account::pay
511             if (
512                 C4::Context->preference('MarkLostItemsAsReturned') =~
513                 m|onpayment|
514                 && $debit->debit_type_code
515                 && $debit->debit_type_code eq 'LOST'
516                 && $debit->amountoutstanding == 0
517                 && $debit->itemnumber
518                 && !(
519                        $self->credit_type_code eq 'LOST_FOUND'
520                     && $self->itemnumber == $debit->itemnumber
521                 )
522               )
523             {
524                 C4::Circulation::ReturnLostItem( $self->borrowernumber,
525                     $debit->itemnumber );
526             }
527         }
528     });
529
530     return $available_credit;
531 }
532
533 =head3 payout
534
535   $credit_accountline->payout(
536     {
537         payout_type => $payout_type,
538         register_id => $register_id,
539         staff_id    => $staff_id,
540         interface   => 'intranet',
541         amount      => $amount
542     }
543   );
544
545 Used to 'pay out' a credit to a user.
546
547 Payout type may be one of any existing payment types
548
549 Returns the payout debit line that is created via this transaction.
550
551 =cut
552
553 sub payout {
554     my ( $self, $params ) = @_;
555
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' );
560     }
561
562     # Check for mandatory parameters
563     my @mandatory =
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" );
569         }
570     }
571
572     # Make sure there is outstanding credit to pay out
573     my $outstanding = -1 * $self->amountoutstanding;
574     my $amount =
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 );
582
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} ) );
589
590     my $payout;
591     $self->_result->result_source->schema->txn_do(
592         sub {
593
594             # A 'payout' is a 'debit'
595             $payout = Koha::Account::Line->new(
596                 {
597                     date              => \'NOW()',
598                     amount            => $amount,
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}
607                 }
608             )->store();
609
610             my $payout_offset = Koha::Account::Offset->new(
611                 {
612                     debit_id => $payout->accountlines_id,
613                     type     => 'PAYOUT',
614                     amount   => $amount
615                 }
616             )->store();
617
618             $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
619             $self->status('PAID')->store;
620         }
621     );
622
623     $payout->discard_changes;
624     return $payout;
625 }
626
627 =head3 adjust
628
629 This method allows updating a debit or credit on a patron's account
630
631     $account_line->adjust(
632         {
633             amount    => $amount,
634             type      => $update_type,
635             interface => $interface
636         }
637     );
638
639 $update_type can be any of:
640   - overdue_update
641
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.
647
648 =cut
649
650 sub adjust {
651     my ( $self, $params ) = @_;
652
653     my $amount       = $params->{amount};
654     my $update_type  = $params->{type};
655     my $interface    = $params->{interface};
656
657     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
658         Koha::Exceptions::Account::UnrecognisedType->throw(
659             error => 'Update type not recognised'
660         );
661     }
662
663     my $debit_type_code = $self->debit_type_code;
664     my $account_status  = $self->status;
665     unless (
666         (
667             exists(
668                 $Koha::Account::Line::allowed_update->{$update_type}
669                   ->{$debit_type_code}
670             )
671             && ( $Koha::Account::Line::allowed_update->{$update_type}
672                 ->{$debit_type_code} eq $account_status )
673         )
674       )
675     {
676         Koha::Exceptions::Account::UnrecognisedType->throw(
677             error => 'Update type not allowed on this debit_type' );
678     }
679
680     my $schema = Koha::Database->new->schema;
681
682     $schema->txn_do(
683         sub {
684
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;
689
690             my $offset_type = $debit_type_code;
691             $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
692
693             # Catch cases that require patron refunds
694             if ( $new_outstanding < 0 ) {
695                 my $account =
696                   Koha::Patrons->find( $self->borrowernumber )->account;
697                 my $credit = $account->add_credit(
698                     {
699                         amount      => $new_outstanding * -1,
700                         type        => 'OVERPAYMENT',
701                         interface   => $interface,
702                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
703                     }
704                 );
705                 $new_outstanding = 0;
706             }
707
708             # Update the account line
709             $self->set(
710                 {
711                     date              => \'NOW()',
712                     amount            => $amount,
713                     amountoutstanding => $new_outstanding,
714                 }
715             )->store();
716
717             # Record the account offset
718             my $account_offset = Koha::Account::Offset->new(
719                 {
720                     debit_id => $self->id,
721                     type     => $offset_type,
722                     amount   => $difference
723                 }
724             )->store();
725
726             if ( C4::Context->preference("FinesLog") ) {
727                 logaction(
728                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
729                     $self->borrowernumber,
730                     Dumper(
731                         {   action            => $update_type,
732                             borrowernumber    => $self->borrowernumber,
733                             amount            => $amount,
734                             description       => undef,
735                             amountoutstanding => $new_outstanding,
736                             debit_type_code   => $self->debit_type_code,
737                             note              => undef,
738                             itemnumber        => $self->itemnumber,
739                             manager_id        => undef,
740                         }
741                     )
742                 ) if ( $update_type eq 'overdue_update' );
743             }
744         }
745     );
746
747     return $self;
748 }
749
750 =head3 is_credit
751
752     my $bool = $line->is_credit;
753
754 =cut
755
756 sub is_credit {
757     my ($self) = @_;
758
759     return ( $self->amount < 0 );
760 }
761
762 =head3 is_debit
763
764     my $bool = $line->is_debit;
765
766 =cut
767
768 sub is_debit {
769     my ($self) = @_;
770
771     return !$self->is_credit;
772 }
773
774 =head3 to_api_mapping
775
776 This method returns the mapping for representing a Koha::Account::Line object
777 on the API.
778
779 =cut
780
781 sub to_api_mapping {
782     return {
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',
793     };
794
795 }
796
797 =head3 renewable
798
799     my $bool = $line->renewable;
800
801 =cut
802
803 sub renewable {
804     my ($self) = @_;
805
806     return (
807         $self->amountoutstanding == 0 &&
808         $self->debit_type_code &&
809         $self->debit_type_code eq 'OVERDUE' &&
810         $self->status &&
811         $self->status eq 'UNRETURNED'
812     ) ? 1 : 0;
813 }
814
815 =head3 renew_item
816
817     my $renew_result = $line->renew_item;
818
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
821
822 =cut
823
824 sub renew_item {
825     my ($self, $params) = @_;
826
827     my $outcome = {};
828
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
833     #
834     # - The RenewAccruingItemInOpac syspref is off
835     # AND
836     # - There is an interface param passed and it's value is 'opac'
837
838     if (
839         !C4::Context->preference('RenewAccruingItemWhenPaid') ||
840         !$self->item ||
841         !$self->patron ||
842         (
843             !C4::Context->preference('RenewAccruingItemInOpac') &&
844             $params->{interface} &&
845             $params->{interface} eq 'opac'
846         )
847     ) {
848         return;
849     }
850
851     my $itemnumber = $self->item->itemnumber;
852     my $borrowernumber = $self->patron->borrowernumber;
853     my ( $can_renew, $error ) = C4::Circulation::CanBookBeRenewed(
854         $borrowernumber,
855         $itemnumber
856     );
857     if ( $can_renew ) {
858         my $due_date = C4::Circulation::AddRenewal(
859             $borrowernumber,
860             $itemnumber,
861             $self->{branchcode},
862             undef,
863             undef,
864             1
865         );
866         return {
867             itemnumber => $itemnumber,
868             due_date   => $due_date,
869             success    => 1
870         };
871     } else {
872         return {
873             itemnumber => $itemnumber,
874             error      => $error,
875             success    => 0
876         };
877     }
878
879 }
880
881 =head3 store
882
883 Specific store method to generate credit number before saving
884
885 =cut
886
887 sub store {
888     my ($self) = @_;
889
890     my $AutoCreditNumber = C4::Context->preference('AutoCreditNumber');
891     my $credit_number_enabled = $self->is_credit && $self->credit_type->credit_number_enabled;
892
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');
896         }
897
898         my $rs = Koha::Database->new->schema->resultset($self->_type);
899
900         if ($AutoCreditNumber eq 'incremental') {
901             my $max = $rs->search({
902                 credit_number => { -regexp => '^[0-9]+$' }
903             }, {
904                 select => \'CAST(credit_number AS UNSIGNED)',
905                 as => ['credit_number'],
906             })->get_column('credit_number')->max;
907             $max //= 0;
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({
913                 -and => [
914                     credit_number => { -regexp => '[0-9]{4}$' },
915                     credit_number => { -like => "$prefix%" },
916                 ],
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;
923             if ($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({
930                     -and => [
931                         credit_number => { -regexp => '[0-9]{4}$' },
932                         credit_number => { -like => "$pattern%" },
933                     ],
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));
938             }
939         }
940     }
941
942     return $self->SUPER::store();
943 }
944
945 =head2 Internal methods
946
947 =cut
948
949 =head3 _type
950
951 =cut
952
953 sub _type {
954     return 'Accountline';
955 }
956
957 1;
958
959 =head2 Name mappings
960
961 =head3 $allowed_update
962
963 =cut
964
965 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
966
967 =head1 AUTHORS
968
969 Kyle M Hall <kyle@bywatersolutions.com >
970 Tomás Cohen Arazi <tomascohen@theke.io>
971 Martin Renvoize <martin.renvoize@ptfs-europe.com>
972
973 =cut