Bug 24080: Add 'payout' method to Koha::Account::Line
[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 under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with Koha; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 use Modern::Perl;
19
20 use Carp;
21 use Data::Dumper;
22
23 use C4::Log qw(logaction);
24
25 use Koha::Account::CreditType;
26 use Koha::Account::DebitType;
27 use Koha::Account::Offsets;
28 use Koha::Database;
29 use Koha::Exceptions::Account;
30 use Koha::Items;
31
32 use base qw(Koha::Object);
33
34 =encoding utf8
35
36 =head1 NAME
37
38 Koha::Account::Line - Koha accountline Object class
39
40 =head1 API
41
42 =head2 Class methods
43
44 =cut
45
46 =head3 patron
47
48 Return the patron linked to this account line
49
50 =cut
51
52 sub patron {
53     my ( $self ) = @_;
54     my $rs = $self->_result->borrowernumber;
55     return unless $rs;
56     return Koha::Patron->_new_from_dbic( $rs );
57 }
58
59 =head3 item
60
61 Return the item linked to this account line if exists
62
63 =cut
64
65 sub item {
66     my ( $self ) = @_;
67     my $rs = $self->_result->itemnumber;
68     return unless $rs;
69     return Koha::Item->_new_from_dbic( $rs );
70 }
71
72 =head3 checkout
73
74 Return the checkout linked to this account line if exists
75
76 =cut
77
78 sub checkout {
79     my ( $self ) = @_;
80     return unless $self->issue_id ;
81
82     $self->{_checkout} ||= Koha::Checkouts->find( $self->issue_id );
83     $self->{_checkout} ||= Koha::Old::Checkouts->find( $self->issue_id );
84     return $self->{_checkout};
85 }
86
87 =head3 credit_type
88
89 Return the credit_type linked to this account line
90
91 =cut
92
93 sub credit_type {
94     my ( $self ) = @_;
95     my $rs = $self->_result->credit_type_code;
96     return unless $rs;
97     return Koha::Account::CreditType->_new_from_dbic( $rs );
98 }
99
100 =head3 debit_type
101
102 Return the debit_type linked to this account line
103
104 =cut
105
106 sub debit_type {
107     my ( $self ) = @_;
108     my $rs = $self->_result->debit_type_code;
109     return unless $rs;
110     return Koha::Account::DebitType->_new_from_dbic( $rs );
111 }
112
113 =head3 void
114
115   $payment_accountline->void();
116
117 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
118 created by the application of this credit upon any debits and mark the credit
119 as 'void' by updating it's status to "VOID".
120
121 =cut
122
123 sub void {
124     my ($self) = @_;
125
126     # Make sure it is a payment we are voiding
127     return unless $self->amount < 0;
128
129     my @account_offsets =
130       Koha::Account::Offsets->search(
131         { credit_id => $self->id, amount => { '<' => 0 }  } );
132
133     $self->_result->result_source->schema->txn_do(
134         sub {
135             foreach my $account_offset (@account_offsets) {
136                 my $fee_paid =
137                   Koha::Account::Lines->find( $account_offset->debit_id );
138
139                 next unless $fee_paid;
140
141                 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
142                 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
143                 $fee_paid->amountoutstanding($new_amount);
144                 $fee_paid->store();
145
146                 Koha::Account::Offset->new(
147                     {
148                         credit_id => $self->id,
149                         debit_id  => $fee_paid->id,
150                         amount    => $amount_paid,
151                         type      => 'Void Payment',
152                     }
153                 )->store();
154             }
155
156             if ( C4::Context->preference("FinesLog") ) {
157                 logaction(
158                     "FINES", 'VOID',
159                     $self->borrowernumber,
160                     Dumper(
161                         {
162                             action         => 'void_payment',
163                             borrowernumber => $self->borrowernumber,
164                             amount            => $self->amount,
165                             amountoutstanding => $self->amountoutstanding,
166                             description       => $self->description,
167                             credit_type_code  => $self->credit_type_code,
168                             payment_type      => $self->payment_type,
169                             note              => $self->note,
170                             itemnumber        => $self->itemnumber,
171                             manager_id        => $self->manager_id,
172                             offsets =>
173                               [ map { $_->unblessed } @account_offsets ],
174                         }
175                     )
176                 );
177             }
178
179             $self->set(
180                 {
181                     status            => 'VOID',
182                     amountoutstanding => 0,
183                     amount            => 0,
184                 }
185             );
186             $self->store();
187         }
188     );
189
190 }
191
192 =head3 apply
193
194     my $debits = $account->outstanding_debits;
195     my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
196
197 Applies the credit to a given debits array reference.
198
199 =head4 arguments hashref
200
201 =over 4
202
203 =item debits - Koha::Account::Lines object set of debits
204
205 =item offset_type (optional) - a string indicating the offset type (valid values are those from
206 the 'account_offset_types' table)
207
208 =back
209
210 =cut
211
212 sub apply {
213     my ( $self, $params ) = @_;
214
215     my $debits      = $params->{debits};
216     my $offset_type = $params->{offset_type} // 'Credit Applied';
217
218     unless ( $self->is_credit ) {
219         Koha::Exceptions::Account::IsNotCredit->throw(
220             error => 'Account line ' . $self->id . ' is not a credit'
221         );
222     }
223
224     my $available_credit = $self->amountoutstanding * -1;
225
226     unless ( $available_credit > 0 ) {
227         Koha::Exceptions::Account::NoAvailableCredit->throw(
228             error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
229         );
230     }
231
232     my $schema = Koha::Database->new->schema;
233
234     $schema->txn_do( sub {
235         for my $debit ( @{$debits} ) {
236
237             unless ( $debit->is_debit ) {
238                 Koha::Exceptions::Account::IsNotDebit->throw(
239                     error => 'Account line ' . $debit->id . 'is not a debit'
240                 );
241             }
242             my $amount_to_cancel;
243             my $owed = $debit->amountoutstanding;
244
245             if ( $available_credit >= $owed ) {
246                 $amount_to_cancel = $owed;
247             }
248             else {    # $available_credit < $debit->amountoutstanding
249                 $amount_to_cancel = $available_credit;
250             }
251
252             # record the account offset
253             Koha::Account::Offset->new(
254                 {   credit_id => $self->id,
255                     debit_id  => $debit->id,
256                     amount    => $amount_to_cancel * -1,
257                     type      => $offset_type,
258                 }
259             )->store();
260
261             $available_credit -= $amount_to_cancel;
262
263             $self->amountoutstanding( $available_credit * -1 )->store;
264             $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
265
266             # Same logic exists in Koha::Account::pay
267             if (   $debit->amountoutstanding == 0
268                 && $debit->itemnumber
269                 && $debit->debit_type_code
270                 && $debit->debit_type_code eq 'LOST' )
271             {
272                 C4::Circulation::ReturnLostItem( $self->borrowernumber, $debit->itemnumber );
273             }
274
275         }
276     });
277
278     return $available_credit;
279 }
280
281 =head3 payout
282
283   $credit_accountline->payout(
284     {
285         payout_type => $payout_type,
286         register_id => $register_id,
287         staff_id    => $staff_id,
288         interface   => 'intranet',
289         amount      => $amount
290     }
291   );
292
293 Used to 'pay out' a credit to a user.
294
295 Payout type may be one of any existing payment types
296
297 Returns the payout debit line that is created via this transaction.
298
299 =cut
300
301 sub payout {
302     my ( $self, $params ) = @_;
303
304     # Make sure it is a credit we are paying out
305     unless ( $self->is_credit ) {
306         Koha::Exceptions::Account::IsNotCredit->throw(
307             error => 'Account line ' . $self->id . ' is not a credit' );
308     }
309
310     # Check for mandatory parameters
311     my @mandatory =
312       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
313     for my $param (@mandatory) {
314         unless ( defined( $params->{$param} ) ) {
315             Koha::Exceptions::MissingParameter->throw(
316                 error => "The $param parameter is mandatory" );
317         }
318     }
319
320     # Make sure there is outstanding credit to pay out
321     my $outstanding = -1 * $self->amountoutstanding;
322     my $amount =
323       $params->{amount} ? $params->{amount} : $outstanding;
324     Koha::Exceptions::Account::AmountNotPositive->throw(
325         error => 'Payout amount passed is not positive' )
326       unless ( $amount > 0 );
327     Koha::Exceptions::ParameterTooHigh->throw(
328         error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
329       unless ($outstanding >= $amount );
330
331     # Make sure we record the cash register for cash transactions
332     Koha::Exceptions::Account::RegisterRequired->throw()
333       if ( C4::Context->preference("UseCashRegisters")
334         && defined( $params->{payout_type} )
335         && ( $params->{payout_type} eq 'CASH' )
336         && !defined( $params->{cash_register} ) );
337
338     my $payout;
339     $self->_result->result_source->schema->txn_do(
340         sub {
341
342             # A 'payout' is a 'debit'
343             $payout = Koha::Account::Line->new(
344                 {
345                     date              => \'NOW()',
346                     amount            => $amount,
347                     debit_type_code   => 'PAYOUT',
348                     payment_type      => $params->{payout_type},
349                     amountoutstanding => $amount,
350                     manager_id        => $params->{staff_id},
351                     borrowernumber    => $self->borrowernumber,
352                     interface         => $params->{interface},
353                     branchcode        => $params->{branch},
354                     register_id       => $params->{cash_register}
355                 }
356             )->store();
357
358             my $payout_offset = Koha::Account::Offset->new(
359                 {
360                     debit_id => $payout->accountlines_id,
361                     type     => 'PAYOUT',
362                     amount   => $amount
363                 }
364             )->store();
365
366             $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
367             $self->status('PAID')->store;
368         }
369     );
370
371     return $payout;
372 }
373
374 =head3 adjust
375
376 This method allows updating a debit or credit on a patron's account
377
378     $account_line->adjust(
379         {
380             amount    => $amount,
381             type      => $update_type,
382             interface => $interface
383         }
384     );
385
386 $update_type can be any of:
387   - overdue_update
388
389 Authors Note: The intention here is that this method is only used
390 to adjust accountlines where the final amount is not yet known/fixed.
391 Incrementing fines are the only existing case at the time of writing,
392 all other forms of 'adjustment' should be recorded as distinct credits
393 or debits and applied, via an offset, to the corresponding debit or credit.
394
395 =cut
396
397 sub adjust {
398     my ( $self, $params ) = @_;
399
400     my $amount       = $params->{amount};
401     my $update_type  = $params->{type};
402     my $interface    = $params->{interface};
403
404     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
405         Koha::Exceptions::Account::UnrecognisedType->throw(
406             error => 'Update type not recognised'
407         );
408     }
409
410     my $debit_type_code = $self->debit_type_code;
411     my $account_status  = $self->status;
412     unless (
413         (
414             exists(
415                 $Koha::Account::Line::allowed_update->{$update_type}
416                   ->{$debit_type_code}
417             )
418             && ( $Koha::Account::Line::allowed_update->{$update_type}
419                 ->{$debit_type_code} eq $account_status )
420         )
421       )
422     {
423         Koha::Exceptions::Account::UnrecognisedType->throw(
424             error => 'Update type not allowed on this debit_type' );
425     }
426
427     my $schema = Koha::Database->new->schema;
428
429     $schema->txn_do(
430         sub {
431
432             my $amount_before             = $self->amount;
433             my $amount_outstanding_before = $self->amountoutstanding;
434             my $difference                = $amount - $amount_before;
435             my $new_outstanding           = $amount_outstanding_before + $difference;
436
437             my $offset_type = $debit_type_code;
438             $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
439
440             # Catch cases that require patron refunds
441             if ( $new_outstanding < 0 ) {
442                 my $account =
443                   Koha::Patrons->find( $self->borrowernumber )->account;
444                 my $credit = $account->add_credit(
445                     {
446                         amount      => $new_outstanding * -1,
447                         description => 'Overpayment refund',
448                         type        => 'CREDIT',
449                         interface   => $interface,
450                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
451                     }
452                 );
453                 $new_outstanding = 0;
454             }
455
456             # Update the account line
457             $self->set(
458                 {
459                     date              => \'NOW()',
460                     amount            => $amount,
461                     amountoutstanding => $new_outstanding,
462                 }
463             )->store();
464
465             # Record the account offset
466             my $account_offset = Koha::Account::Offset->new(
467                 {
468                     debit_id => $self->id,
469                     type     => $offset_type,
470                     amount   => $difference
471                 }
472             )->store();
473
474             if ( C4::Context->preference("FinesLog") ) {
475                 logaction(
476                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
477                     $self->borrowernumber,
478                     Dumper(
479                         {   action            => $update_type,
480                             borrowernumber    => $self->borrowernumber,
481                             amount            => $amount,
482                             description       => undef,
483                             amountoutstanding => $new_outstanding,
484                             debit_type_code   => $self->debit_type_code,
485                             note              => undef,
486                             itemnumber        => $self->itemnumber,
487                             manager_id        => undef,
488                         }
489                     )
490                 ) if ( $update_type eq 'overdue_update' );
491             }
492         }
493     );
494
495     return $self;
496 }
497
498 =head3 is_credit
499
500     my $bool = $line->is_credit;
501
502 =cut
503
504 sub is_credit {
505     my ($self) = @_;
506
507     return ( $self->amount < 0 );
508 }
509
510 =head3 is_debit
511
512     my $bool = $line->is_debit;
513
514 =cut
515
516 sub is_debit {
517     my ($self) = @_;
518
519     return !$self->is_credit;
520 }
521
522 =head3 to_api_mapping
523
524 This method returns the mapping for representing a Koha::Account::Line object
525 on the API.
526
527 =cut
528
529 sub to_api_mapping {
530     return {
531         accountlines_id   => 'account_line_id',
532         credit_type_code  => 'credit_type',
533         debit_type_code   => 'debit_type',
534         amountoutstanding => 'amount_outstanding',
535         borrowernumber    => 'patron_id',
536         branchcode        => 'library_id',
537         issue_id          => 'checkout_id',
538         itemnumber        => 'item_id',
539         manager_id        => 'user_id',
540         note              => 'internal_note',
541     };
542 }
543
544 =head2 Internal methods
545
546 =cut
547
548 =head3 _type
549
550 =cut
551
552 sub _type {
553     return 'Accountline';
554 }
555
556 1;
557
558 =head2 Name mappings
559
560 =head3 $allowed_update
561
562 =cut
563
564 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
565
566 =head1 AUTHORS
567
568 Kyle M Hall <kyle@bywatersolutions.com >
569 Tomás Cohen Arazi <tomascohen@theke.io>
570 Martin Renvoize <martin.renvoize@ptfs-europe.com>
571
572 =cut