Bug 25596: Identify overpayment refunds for translation
[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::IsNotCredit->throw(
171             error => 'Account line ' . $self->id . ' is not a debit'
172         );
173     }
174
175     my $rs =
176       $self->_result->search_related('account_offsets_debits')
177       ->search_related( 'credit', $cond, $attr );
178     return unless $rs;
179     return Koha::Account::Lines->_new_from_dbic($rs);
180 }
181
182 =head3 debits
183
184   my $debits = $accountline->debits;
185   my $debits = $accountline->debits( $cond, $attr );
186
187 Return the debits linked to this account line if some exist.
188 Search conditions and attributes may be passed if you wish to filter
189 the resultant resultant resultset.
190
191 =cut
192
193 sub debits {
194     my ( $self, $cond, $attr ) = @_;
195
196     unless ( $self->is_credit ) {
197         Koha::Exceptions::Account::IsNotCredit->throw(
198             error => 'Account line ' . $self->id . ' is not a credit'
199         );
200     }
201
202     my $rs =
203       $self->_result->search_related('account_offsets_credits')
204       ->search_related( 'debit', $cond, $attr );
205     return unless $rs;
206     return Koha::Account::Lines->_new_from_dbic($rs);
207 }
208
209 =head3 void
210
211   $payment_accountline->void();
212
213 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
214 created by the application of this credit upon any debits and mark the credit
215 as 'void' by updating it's status to "VOID".
216
217 =cut
218
219 sub void {
220     my ($self) = @_;
221
222     # Make sure it is a payment we are voiding
223     return unless $self->amount < 0;
224
225     my @account_offsets =
226       Koha::Account::Offsets->search(
227         { credit_id => $self->id, amount => { '<' => 0 }  } );
228
229     $self->_result->result_source->schema->txn_do(
230         sub {
231             foreach my $account_offset (@account_offsets) {
232                 my $fee_paid =
233                   Koha::Account::Lines->find( $account_offset->debit_id );
234
235                 next unless $fee_paid;
236
237                 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
238                 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
239                 $fee_paid->amountoutstanding($new_amount);
240                 $fee_paid->store();
241
242                 Koha::Account::Offset->new(
243                     {
244                         credit_id => $self->id,
245                         debit_id  => $fee_paid->id,
246                         amount    => $amount_paid,
247                         type      => 'Void Payment',
248                     }
249                 )->store();
250             }
251
252             if ( C4::Context->preference("FinesLog") ) {
253                 logaction(
254                     "FINES", 'VOID',
255                     $self->borrowernumber,
256                     Dumper(
257                         {
258                             action         => 'void_payment',
259                             borrowernumber => $self->borrowernumber,
260                             amount            => $self->amount,
261                             amountoutstanding => $self->amountoutstanding,
262                             description       => $self->description,
263                             credit_type_code  => $self->credit_type_code,
264                             payment_type      => $self->payment_type,
265                             note              => $self->note,
266                             itemnumber        => $self->itemnumber,
267                             manager_id        => $self->manager_id,
268                             offsets =>
269                               [ map { $_->unblessed } @account_offsets ],
270                         }
271                     )
272                 );
273             }
274
275             $self->set(
276                 {
277                     status            => 'VOID',
278                     amountoutstanding => 0,
279                     amount            => 0,
280                 }
281             );
282             $self->store();
283         }
284     );
285
286 }
287
288 =head3 reduce
289
290   $charge_accountline->reduce({
291       reduction_type => $reduction_type
292   });
293
294 Used to 'reduce' a charge/debit by adding a credit to offset against the amount
295 outstanding.
296
297 May be used to apply a discount whilst retaining the original debit amounts or
298 to apply a full or partial refund for example when a lost item is found and
299 returned.
300
301 It will immediately be applied to the given debit unless the debit has already
302 been paid, in which case a 'zero' offset will be added to maintain a link to
303 the debit but the outstanding credit will be left so it may be applied to other
304 debts.
305
306 Reduction type may be one of:
307
308 * REFUND
309 * DISCOUNT
310
311 Returns the reduction accountline (which will be a credit)
312
313 =cut
314
315 sub reduce {
316     my ( $self, $params ) = @_;
317
318     # Make sure it is a charge we are reducing
319     unless ( $self->is_debit ) {
320         Koha::Exceptions::Account::IsNotDebit->throw(
321             error => 'Account line ' . $self->id . 'is not a debit' );
322     }
323     if ( $self->debit_type_code eq 'PAYOUT' ) {
324         Koha::Exceptions::Account::IsNotDebit->throw(
325             error => 'Account line ' . $self->id . 'is a payout' );
326     }
327
328     # Check for mandatory parameters
329     my @mandatory = ( 'interface', 'reduction_type', 'amount' );
330     for my $param (@mandatory) {
331         unless ( defined( $params->{$param} ) ) {
332             Koha::Exceptions::MissingParameter->throw(
333                 error => "The $param parameter is mandatory" );
334         }
335     }
336
337     # More mandatory parameters
338     if ( $params->{interface} eq 'intranet' ) {
339         my @optional = ( 'staff_id', 'branch' );
340         for my $param (@optional) {
341             unless ( defined( $params->{$param} ) ) {
342                 Koha::Exceptions::MissingParameter->throw( error =>
343 "The $param parameter is mandatory when interface is set to 'intranet'"
344                 );
345             }
346         }
347     }
348
349     # Make sure the reduction isn't more than the original
350     my $original = $self->amount;
351     Koha::Exceptions::Account::AmountNotPositive->throw(
352         error => 'Reduce amount passed is not positive' )
353       unless ( $params->{amount} > 0 );
354     Koha::Exceptions::ParameterTooHigh->throw( error =>
355 "Amount to reduce ($params->{amount}) is higher than original amount ($original)"
356     ) unless ( $original >= $params->{amount} );
357     my $reduced =
358       $self->credits( { credit_type_code => [ 'DISCOUNT', 'REFUND' ] } )->total;
359     Koha::Exceptions::ParameterTooHigh->throw( error =>
360 "Combined reduction ($params->{amount} + $reduced) is higher than original amount ("
361           . abs($original)
362           . ")" )
363       unless ( $original >= ( $params->{amount} + abs($reduced) ) );
364
365     my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' };
366
367     my $reduction;
368     $self->_result->result_source->schema->txn_do(
369         sub {
370
371             # A 'reduction' is a 'credit'
372             $reduction = Koha::Account::Line->new(
373                 {
374                     date              => \'NOW()',
375                     amount            => 0 - $params->{amount},
376                     credit_type_code  => $params->{reduction_type},
377                     status            => 'ADDED',
378                     amountoutstanding => 0 - $params->{amount},
379                     manager_id        => $params->{staff_id},
380                     borrowernumber    => $self->borrowernumber,
381                     interface         => $params->{interface},
382                     branchcode        => $params->{branch},
383                 }
384             )->store();
385
386             my $reduction_offset = Koha::Account::Offset->new(
387                 {
388                     credit_id => $reduction->accountlines_id,
389                     type      => uc( $params->{reduction_type} ),
390                     amount    => $params->{amount}
391                 }
392             )->store();
393
394             # Link reduction to charge (and apply as required)
395             my $debit_outstanding = $self->amountoutstanding;
396             if ( $debit_outstanding >= $params->{amount} ) {
397
398                 $reduction->apply(
399                     {
400                         debits      => [$self],
401                         offset_type => uc( $params->{reduction_type} )
402                     }
403                 );
404                 $reduction->status('APPLIED')->store();
405             }
406             else {
407
408         # Zero amount offset used to link original 'debit' to reduction 'credit'
409                 my $link_reduction_offset = Koha::Account::Offset->new(
410                     {
411                         credit_id => $reduction->accountlines_id,
412                         debit_id  => $self->accountlines_id,
413                         type      => uc( $params->{reduction_type} ),
414                         amount    => 0
415                     }
416                 )->store();
417             }
418
419             # Update status of original debit
420             $self->status( $status->{ $params->{reduction_type} } )->store;
421         }
422     );
423
424     $reduction->discard_changes;
425     return $reduction;
426 }
427
428 =head3 apply
429
430     my $debits = $account->outstanding_debits;
431     my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
432
433 Applies the credit to a given debits array reference.
434
435 =head4 arguments hashref
436
437 =over 4
438
439 =item debits - Koha::Account::Lines object set of debits
440
441 =item offset_type (optional) - a string indicating the offset type (valid values are those from
442 the 'account_offset_types' table)
443
444 =back
445
446 =cut
447
448 sub apply {
449     my ( $self, $params ) = @_;
450
451     my $debits      = $params->{debits};
452     my $offset_type = $params->{offset_type} // 'Credit Applied';
453
454     unless ( $self->is_credit ) {
455         Koha::Exceptions::Account::IsNotCredit->throw(
456             error => 'Account line ' . $self->id . ' is not a credit'
457         );
458     }
459
460     my $available_credit = $self->amountoutstanding * -1;
461
462     unless ( $available_credit > 0 ) {
463         Koha::Exceptions::Account::NoAvailableCredit->throw(
464             error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
465         );
466     }
467
468     my $schema = Koha::Database->new->schema;
469
470     $schema->txn_do( sub {
471         for my $debit ( @{$debits} ) {
472
473             unless ( $debit->is_debit ) {
474                 Koha::Exceptions::Account::IsNotDebit->throw(
475                     error => 'Account line ' . $debit->id . 'is not a debit'
476                 );
477             }
478             my $amount_to_cancel;
479             my $owed = $debit->amountoutstanding;
480
481             if ( $available_credit >= $owed ) {
482                 $amount_to_cancel = $owed;
483             }
484             else {    # $available_credit < $debit->amountoutstanding
485                 $amount_to_cancel = $available_credit;
486             }
487
488             # record the account offset
489             Koha::Account::Offset->new(
490                 {   credit_id => $self->id,
491                     debit_id  => $debit->id,
492                     amount    => $amount_to_cancel * -1,
493                     type      => $offset_type,
494                 }
495             )->store();
496
497             $available_credit -= $amount_to_cancel;
498
499             $self->amountoutstanding( $available_credit * -1 )->store;
500             $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
501
502             # Attempt to renew the item associated with this debit if
503             # appropriate
504             if ($debit->renewable) {
505                 $debit->renew_item($params->{interface});
506             }
507
508             # Same logic exists in Koha::Account::pay
509             if (
510                 C4::Context->preference('MarkLostItemsAsReturned') =~
511                 m|onpayment|
512                 && $debit->debit_type_code
513                 && $debit->debit_type_code eq 'LOST'
514                 && $debit->amountoutstanding == 0
515                 && $debit->itemnumber
516                 && !(
517                        $self->credit_type_code eq 'LOST_FOUND'
518                     && $self->itemnumber == $debit->itemnumber
519                 )
520               )
521             {
522                 C4::Circulation::ReturnLostItem( $self->borrowernumber,
523                     $debit->itemnumber );
524             }
525         }
526     });
527
528     return $available_credit;
529 }
530
531 =head3 payout
532
533   $credit_accountline->payout(
534     {
535         payout_type => $payout_type,
536         register_id => $register_id,
537         staff_id    => $staff_id,
538         interface   => 'intranet',
539         amount      => $amount
540     }
541   );
542
543 Used to 'pay out' a credit to a user.
544
545 Payout type may be one of any existing payment types
546
547 Returns the payout debit line that is created via this transaction.
548
549 =cut
550
551 sub payout {
552     my ( $self, $params ) = @_;
553
554     # Make sure it is a credit we are paying out
555     unless ( $self->is_credit ) {
556         Koha::Exceptions::Account::IsNotCredit->throw(
557             error => 'Account line ' . $self->id . ' is not a credit' );
558     }
559
560     # Check for mandatory parameters
561     my @mandatory =
562       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
563     for my $param (@mandatory) {
564         unless ( defined( $params->{$param} ) ) {
565             Koha::Exceptions::MissingParameter->throw(
566                 error => "The $param parameter is mandatory" );
567         }
568     }
569
570     # Make sure there is outstanding credit to pay out
571     my $outstanding = -1 * $self->amountoutstanding;
572     my $amount =
573       $params->{amount} ? $params->{amount} : $outstanding;
574     Koha::Exceptions::Account::AmountNotPositive->throw(
575         error => 'Payout amount passed is not positive' )
576       unless ( $amount > 0 );
577     Koha::Exceptions::ParameterTooHigh->throw(
578         error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
579       unless ($outstanding >= $amount );
580
581     # Make sure we record the cash register for cash transactions
582     Koha::Exceptions::Account::RegisterRequired->throw()
583       if ( C4::Context->preference("UseCashRegisters")
584         && defined( $params->{payout_type} )
585         && ( $params->{payout_type} eq 'CASH' )
586         && !defined( $params->{cash_register} ) );
587
588     my $payout;
589     $self->_result->result_source->schema->txn_do(
590         sub {
591
592             # A 'payout' is a 'debit'
593             $payout = Koha::Account::Line->new(
594                 {
595                     date              => \'NOW()',
596                     amount            => $amount,
597                     debit_type_code   => 'PAYOUT',
598                     payment_type      => $params->{payout_type},
599                     amountoutstanding => $amount,
600                     manager_id        => $params->{staff_id},
601                     borrowernumber    => $self->borrowernumber,
602                     interface         => $params->{interface},
603                     branchcode        => $params->{branch},
604                     register_id       => $params->{cash_register}
605                 }
606             )->store();
607
608             my $payout_offset = Koha::Account::Offset->new(
609                 {
610                     debit_id => $payout->accountlines_id,
611                     type     => 'PAYOUT',
612                     amount   => $amount
613                 }
614             )->store();
615
616             $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
617             $self->status('PAID')->store;
618         }
619     );
620
621     $payout->discard_changes;
622     return $payout;
623 }
624
625 =head3 adjust
626
627 This method allows updating a debit or credit on a patron's account
628
629     $account_line->adjust(
630         {
631             amount    => $amount,
632             type      => $update_type,
633             interface => $interface
634         }
635     );
636
637 $update_type can be any of:
638   - overdue_update
639
640 Authors Note: The intention here is that this method is only used
641 to adjust accountlines where the final amount is not yet known/fixed.
642 Incrementing fines are the only existing case at the time of writing,
643 all other forms of 'adjustment' should be recorded as distinct credits
644 or debits and applied, via an offset, to the corresponding debit or credit.
645
646 =cut
647
648 sub adjust {
649     my ( $self, $params ) = @_;
650
651     my $amount       = $params->{amount};
652     my $update_type  = $params->{type};
653     my $interface    = $params->{interface};
654
655     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
656         Koha::Exceptions::Account::UnrecognisedType->throw(
657             error => 'Update type not recognised'
658         );
659     }
660
661     my $debit_type_code = $self->debit_type_code;
662     my $account_status  = $self->status;
663     unless (
664         (
665             exists(
666                 $Koha::Account::Line::allowed_update->{$update_type}
667                   ->{$debit_type_code}
668             )
669             && ( $Koha::Account::Line::allowed_update->{$update_type}
670                 ->{$debit_type_code} eq $account_status )
671         )
672       )
673     {
674         Koha::Exceptions::Account::UnrecognisedType->throw(
675             error => 'Update type not allowed on this debit_type' );
676     }
677
678     my $schema = Koha::Database->new->schema;
679
680     $schema->txn_do(
681         sub {
682
683             my $amount_before             = $self->amount;
684             my $amount_outstanding_before = $self->amountoutstanding;
685             my $difference                = $amount - $amount_before;
686             my $new_outstanding           = $amount_outstanding_before + $difference;
687
688             my $offset_type = $debit_type_code;
689             $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
690
691             # Catch cases that require patron refunds
692             if ( $new_outstanding < 0 ) {
693                 my $account =
694                   Koha::Patrons->find( $self->borrowernumber )->account;
695                 my $credit = $account->add_credit(
696                     {
697                         amount      => $new_outstanding * -1,
698                         type        => 'OVERPAYMENT',
699                         interface   => $interface,
700                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
701                     }
702                 );
703                 $new_outstanding = 0;
704             }
705
706             # Update the account line
707             $self->set(
708                 {
709                     date              => \'NOW()',
710                     amount            => $amount,
711                     amountoutstanding => $new_outstanding,
712                 }
713             )->store();
714
715             # Record the account offset
716             my $account_offset = Koha::Account::Offset->new(
717                 {
718                     debit_id => $self->id,
719                     type     => $offset_type,
720                     amount   => $difference
721                 }
722             )->store();
723
724             if ( C4::Context->preference("FinesLog") ) {
725                 logaction(
726                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
727                     $self->borrowernumber,
728                     Dumper(
729                         {   action            => $update_type,
730                             borrowernumber    => $self->borrowernumber,
731                             amount            => $amount,
732                             description       => undef,
733                             amountoutstanding => $new_outstanding,
734                             debit_type_code   => $self->debit_type_code,
735                             note              => undef,
736                             itemnumber        => $self->itemnumber,
737                             manager_id        => undef,
738                         }
739                     )
740                 ) if ( $update_type eq 'overdue_update' );
741             }
742         }
743     );
744
745     return $self;
746 }
747
748 =head3 is_credit
749
750     my $bool = $line->is_credit;
751
752 =cut
753
754 sub is_credit {
755     my ($self) = @_;
756
757     return ( $self->amount < 0 );
758 }
759
760 =head3 is_debit
761
762     my $bool = $line->is_debit;
763
764 =cut
765
766 sub is_debit {
767     my ($self) = @_;
768
769     return !$self->is_credit;
770 }
771
772 =head3 to_api_mapping
773
774 This method returns the mapping for representing a Koha::Account::Line object
775 on the API.
776
777 =cut
778
779 sub to_api_mapping {
780     return {
781         accountlines_id   => 'account_line_id',
782         credit_type_code  => 'credit_type',
783         debit_type_code   => 'debit_type',
784         amountoutstanding => 'amount_outstanding',
785         borrowernumber    => 'patron_id',
786         branchcode        => 'library_id',
787         issue_id          => 'checkout_id',
788         itemnumber        => 'item_id',
789         manager_id        => 'user_id',
790         note              => 'internal_note',
791     };
792
793 }
794
795 =head3 renewable
796
797     my $bool = $line->renewable;
798
799 =cut
800
801 sub renewable {
802     my ($self) = @_;
803
804     return (
805         $self->amountoutstanding == 0 &&
806         $self->debit_type_code &&
807         $self->debit_type_code eq 'OVERDUE' &&
808         $self->status &&
809         $self->status eq 'UNRETURNED'
810     ) ? 1 : 0;
811 }
812
813 =head3 renew_item
814
815     my $renew_result = $line->renew_item;
816
817 Conditionally attempt to renew an item and return the outcome. This is
818 as a consequence of the fine on an item being fully paid off
819
820 =cut
821
822 sub renew_item {
823     my ($self, $params) = @_;
824
825     my $outcome = {};
826
827     # We want to reject the call to renew if any of these apply:
828     # - The RenewAccruingItemWhenPaid syspref is off
829     # - The line item doesn't have an item attached to it
830     # - The line item doesn't have a patron attached to it
831     #
832     # - The RenewAccruingItemInOpac syspref is off
833     # AND
834     # - There is an interface param passed and it's value is 'opac'
835
836     if (
837         !C4::Context->preference('RenewAccruingItemWhenPaid') ||
838         !$self->item ||
839         !$self->patron ||
840         (
841             !C4::Context->preference('RenewAccruingItemInOpac') &&
842             $params->{interface} &&
843             $params->{interface} eq 'opac'
844         )
845     ) {
846         return;
847     }
848
849     my $itemnumber = $self->item->itemnumber;
850     my $borrowernumber = $self->patron->borrowernumber;
851     my ( $can_renew, $error ) = C4::Circulation::CanBookBeRenewed(
852         $borrowernumber,
853         $itemnumber
854     );
855     if ( $can_renew ) {
856         my $due_date = C4::Circulation::AddRenewal(
857             $borrowernumber,
858             $itemnumber,
859             $self->{branchcode},
860             undef,
861             undef,
862             1
863         );
864         return {
865             itemnumber => $itemnumber,
866             due_date   => $due_date,
867             success    => 1
868         };
869     } else {
870         return {
871             itemnumber => $itemnumber,
872             error      => $error,
873             success    => 0
874         };
875     }
876
877 }
878
879 =head3 store
880
881 Specific store method to generate credit number before saving
882
883 =cut
884
885 sub store {
886     my ($self) = @_;
887
888     my $AutoCreditNumber = C4::Context->preference('AutoCreditNumber');
889     my $credit_number_enabled = $self->is_credit && $self->credit_type->credit_number_enabled;
890
891     if ($AutoCreditNumber && $credit_number_enabled && !$self->in_storage) {
892         if (defined $self->credit_number) {
893             Koha::Exceptions::Account->throw('AutoCreditNumber is enabled but credit_number is already defined');
894         }
895
896         my $rs = Koha::Database->new->schema->resultset($self->_type);
897
898         if ($AutoCreditNumber eq 'incremental') {
899             my $max = $rs->search({
900                 credit_number => { -regexp => '^[0-9]+$' }
901             }, {
902                 select => \'CAST(credit_number AS UNSIGNED)',
903                 as => ['credit_number'],
904             })->get_column('credit_number')->max;
905             $max //= 0;
906             $self->credit_number($max + 1);
907         } elsif ($AutoCreditNumber eq 'annual') {
908             my $now = dt_from_string;
909             my $prefix = sprintf('%d-', $now->year);
910             my $max = $rs->search({
911                 -and => [
912                     credit_number => { -regexp => '[0-9]{4}$' },
913                     credit_number => { -like => "$prefix%" },
914                 ],
915             })->get_column('credit_number')->max;
916             $max //= $prefix . '0000';
917             my $incr = substr($max, length $prefix);
918             $self->credit_number(sprintf('%s%04d', $prefix, $incr + 1));
919         } elsif ($AutoCreditNumber eq 'branchyyyymmincr') {
920             my $userenv = C4::Context->userenv;
921             if ($userenv) {
922                 my $branch = $userenv->{branch};
923                 my $now = dt_from_string;
924                 my $prefix = sprintf('%s%d%02d', $branch, $now->year, $now->month);
925                 my $pattern = $prefix;
926                 $pattern =~ s/([\?%_])/\\$1/g;
927                 my $max = $rs->search({
928                     -and => [
929                         credit_number => { -regexp => '[0-9]{4}$' },
930                         credit_number => { -like => "$pattern%" },
931                     ],
932                 })->get_column('credit_number')->max;
933                 $max //= $prefix . '0000';
934                 my $incr = substr($max, length $prefix);
935                 $self->credit_number(sprintf('%s%04d', $prefix, $incr + 1));
936             }
937         }
938     }
939
940     return $self->SUPER::store();
941 }
942
943 =head2 Internal methods
944
945 =cut
946
947 =head3 _type
948
949 =cut
950
951 sub _type {
952     return 'Accountline';
953 }
954
955 1;
956
957 =head2 Name mappings
958
959 =head3 $allowed_update
960
961 =cut
962
963 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
964
965 =head1 AUTHORS
966
967 Kyle M Hall <kyle@bywatersolutions.com >
968 Tomás Cohen Arazi <tomascohen@theke.io>
969 Martin Renvoize <martin.renvoize@ptfs-europe.com>
970
971 =cut