Bug 24252: Add relations 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 credit_offsets
114
115 Return the credit_offsets linked to this account line if some exist
116
117 =cut
118
119 sub credit_offsets {
120     my ( $self ) = @_;
121     my $rs = $self->_result->account_offsets_credits;
122     return unless $rs;
123     return Koha::Account::Offsets->_new_from_dbic($rs);
124 }
125
126 =head3 debit_offsets
127
128 Return the debit_offsets linked to this account line if some exist
129
130 =cut
131
132 sub debit_offsets {
133     my ( $self ) = @_;
134     my $rs = $self->_result->account_offsets_debits;
135     return unless $rs;
136     return Koha::Account::Offsets->_new_from_dbic($rs);
137 }
138
139
140 =head3 credits
141
142   my $credits = $accountline->credits;
143   my $credits = $accountline->credits( $cond, $attr );
144
145 Return the credits linked to this account line if some exist.
146 Search conditions and attributes may be passed if you wish to filter
147 the resultant resultant resultset.
148
149 =cut
150
151 sub credits {
152     my ( $self, $cond, $attr ) = @_;
153
154     unless ( $self->is_debit ) {
155         Koha::Exceptions::Account::IsNotCredit->throw(
156             error => 'Account line ' . $self->id . ' is not a debit'
157         );
158     }
159
160     my $rs =
161       $self->_result->search_related('account_offsets_debits')
162       ->search_related( 'credit', $cond, $attr );
163     return unless $rs;
164     return Koha::Account::Lines->_new_from_dbic($rs);
165 }
166
167 =head3 debits
168
169   my $debits = $accountline->debits;
170   my $debits = $accountline->debits( $cond, $attr );
171
172 Return the debits 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 debits {
179     my ( $self, $cond, $attr ) = @_;
180
181     unless ( $self->is_credit ) {
182         Koha::Exceptions::Account::IsNotCredit->throw(
183             error => 'Account line ' . $self->id . ' is not a credit'
184         );
185     }
186
187     my $rs =
188       $self->_result->search_related('account_offsets_credits')
189       ->search_related( 'debit', $cond, $attr );
190     return unless $rs;
191     return Koha::Account::Lines->_new_from_dbic($rs);
192 }
193
194 =head3 void
195
196   $payment_accountline->void();
197
198 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
199 created by the application of this credit upon any debits and mark the credit
200 as 'void' by updating it's status to "VOID".
201
202 =cut
203
204 sub void {
205     my ($self) = @_;
206
207     # Make sure it is a payment we are voiding
208     return unless $self->amount < 0;
209
210     my @account_offsets =
211       Koha::Account::Offsets->search(
212         { credit_id => $self->id, amount => { '<' => 0 }  } );
213
214     $self->_result->result_source->schema->txn_do(
215         sub {
216             foreach my $account_offset (@account_offsets) {
217                 my $fee_paid =
218                   Koha::Account::Lines->find( $account_offset->debit_id );
219
220                 next unless $fee_paid;
221
222                 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
223                 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
224                 $fee_paid->amountoutstanding($new_amount);
225                 $fee_paid->store();
226
227                 Koha::Account::Offset->new(
228                     {
229                         credit_id => $self->id,
230                         debit_id  => $fee_paid->id,
231                         amount    => $amount_paid,
232                         type      => 'Void Payment',
233                     }
234                 )->store();
235             }
236
237             if ( C4::Context->preference("FinesLog") ) {
238                 logaction(
239                     "FINES", 'VOID',
240                     $self->borrowernumber,
241                     Dumper(
242                         {
243                             action         => 'void_payment',
244                             borrowernumber => $self->borrowernumber,
245                             amount            => $self->amount,
246                             amountoutstanding => $self->amountoutstanding,
247                             description       => $self->description,
248                             credit_type_code  => $self->credit_type_code,
249                             payment_type      => $self->payment_type,
250                             note              => $self->note,
251                             itemnumber        => $self->itemnumber,
252                             manager_id        => $self->manager_id,
253                             offsets =>
254                               [ map { $_->unblessed } @account_offsets ],
255                         }
256                     )
257                 );
258             }
259
260             $self->set(
261                 {
262                     status            => 'VOID',
263                     amountoutstanding => 0,
264                     amount            => 0,
265                 }
266             );
267             $self->store();
268         }
269     );
270
271 }
272
273 =head3 apply
274
275     my $debits = $account->outstanding_debits;
276     my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
277
278 Applies the credit to a given debits array reference.
279
280 =head4 arguments hashref
281
282 =over 4
283
284 =item debits - Koha::Account::Lines object set of debits
285
286 =item offset_type (optional) - a string indicating the offset type (valid values are those from
287 the 'account_offset_types' table)
288
289 =back
290
291 =cut
292
293 sub apply {
294     my ( $self, $params ) = @_;
295
296     my $debits      = $params->{debits};
297     my $offset_type = $params->{offset_type} // 'Credit Applied';
298
299     unless ( $self->is_credit ) {
300         Koha::Exceptions::Account::IsNotCredit->throw(
301             error => 'Account line ' . $self->id . ' is not a credit'
302         );
303     }
304
305     my $available_credit = $self->amountoutstanding * -1;
306
307     unless ( $available_credit > 0 ) {
308         Koha::Exceptions::Account::NoAvailableCredit->throw(
309             error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
310         );
311     }
312
313     my $schema = Koha::Database->new->schema;
314
315     $schema->txn_do( sub {
316         for my $debit ( @{$debits} ) {
317
318             unless ( $debit->is_debit ) {
319                 Koha::Exceptions::Account::IsNotDebit->throw(
320                     error => 'Account line ' . $debit->id . 'is not a debit'
321                 );
322             }
323             my $amount_to_cancel;
324             my $owed = $debit->amountoutstanding;
325
326             if ( $available_credit >= $owed ) {
327                 $amount_to_cancel = $owed;
328             }
329             else {    # $available_credit < $debit->amountoutstanding
330                 $amount_to_cancel = $available_credit;
331             }
332
333             # record the account offset
334             Koha::Account::Offset->new(
335                 {   credit_id => $self->id,
336                     debit_id  => $debit->id,
337                     amount    => $amount_to_cancel * -1,
338                     type      => $offset_type,
339                 }
340             )->store();
341
342             $available_credit -= $amount_to_cancel;
343
344             $self->amountoutstanding( $available_credit * -1 )->store;
345             $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
346
347             # Same logic exists in Koha::Account::pay
348             if (   $debit->amountoutstanding == 0
349                 && $debit->itemnumber
350                 && $debit->debit_type_code
351                 && $debit->debit_type_code eq 'LOST' )
352             {
353                 C4::Circulation::ReturnLostItem( $self->borrowernumber, $debit->itemnumber );
354             }
355
356         }
357     });
358
359     return $available_credit;
360 }
361
362 =head3 payout
363
364   $credit_accountline->payout(
365     {
366         payout_type => $payout_type,
367         register_id => $register_id,
368         staff_id    => $staff_id,
369         interface   => 'intranet',
370         amount      => $amount
371     }
372   );
373
374 Used to 'pay out' a credit to a user.
375
376 Payout type may be one of any existing payment types
377
378 Returns the payout debit line that is created via this transaction.
379
380 =cut
381
382 sub payout {
383     my ( $self, $params ) = @_;
384
385     # Make sure it is a credit we are paying out
386     unless ( $self->is_credit ) {
387         Koha::Exceptions::Account::IsNotCredit->throw(
388             error => 'Account line ' . $self->id . ' is not a credit' );
389     }
390
391     # Check for mandatory parameters
392     my @mandatory =
393       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
394     for my $param (@mandatory) {
395         unless ( defined( $params->{$param} ) ) {
396             Koha::Exceptions::MissingParameter->throw(
397                 error => "The $param parameter is mandatory" );
398         }
399     }
400
401     # Make sure there is outstanding credit to pay out
402     my $outstanding = -1 * $self->amountoutstanding;
403     my $amount =
404       $params->{amount} ? $params->{amount} : $outstanding;
405     Koha::Exceptions::Account::AmountNotPositive->throw(
406         error => 'Payout amount passed is not positive' )
407       unless ( $amount > 0 );
408     Koha::Exceptions::ParameterTooHigh->throw(
409         error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
410       unless ($outstanding >= $amount );
411
412     # Make sure we record the cash register for cash transactions
413     Koha::Exceptions::Account::RegisterRequired->throw()
414       if ( C4::Context->preference("UseCashRegisters")
415         && defined( $params->{payout_type} )
416         && ( $params->{payout_type} eq 'CASH' )
417         && !defined( $params->{cash_register} ) );
418
419     my $payout;
420     $self->_result->result_source->schema->txn_do(
421         sub {
422
423             # A 'payout' is a 'debit'
424             $payout = Koha::Account::Line->new(
425                 {
426                     date              => \'NOW()',
427                     amount            => $amount,
428                     debit_type_code   => 'PAYOUT',
429                     payment_type      => $params->{payout_type},
430                     amountoutstanding => $amount,
431                     manager_id        => $params->{staff_id},
432                     borrowernumber    => $self->borrowernumber,
433                     interface         => $params->{interface},
434                     branchcode        => $params->{branch},
435                     register_id       => $params->{cash_register}
436                 }
437             )->store();
438
439             my $payout_offset = Koha::Account::Offset->new(
440                 {
441                     debit_id => $payout->accountlines_id,
442                     type     => 'PAYOUT',
443                     amount   => $amount
444                 }
445             )->store();
446
447             $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
448             $self->status('PAID')->store;
449         }
450     );
451
452     return $payout;
453 }
454
455 =head3 adjust
456
457 This method allows updating a debit or credit on a patron's account
458
459     $account_line->adjust(
460         {
461             amount    => $amount,
462             type      => $update_type,
463             interface => $interface
464         }
465     );
466
467 $update_type can be any of:
468   - overdue_update
469
470 Authors Note: The intention here is that this method is only used
471 to adjust accountlines where the final amount is not yet known/fixed.
472 Incrementing fines are the only existing case at the time of writing,
473 all other forms of 'adjustment' should be recorded as distinct credits
474 or debits and applied, via an offset, to the corresponding debit or credit.
475
476 =cut
477
478 sub adjust {
479     my ( $self, $params ) = @_;
480
481     my $amount       = $params->{amount};
482     my $update_type  = $params->{type};
483     my $interface    = $params->{interface};
484
485     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
486         Koha::Exceptions::Account::UnrecognisedType->throw(
487             error => 'Update type not recognised'
488         );
489     }
490
491     my $debit_type_code = $self->debit_type_code;
492     my $account_status  = $self->status;
493     unless (
494         (
495             exists(
496                 $Koha::Account::Line::allowed_update->{$update_type}
497                   ->{$debit_type_code}
498             )
499             && ( $Koha::Account::Line::allowed_update->{$update_type}
500                 ->{$debit_type_code} eq $account_status )
501         )
502       )
503     {
504         Koha::Exceptions::Account::UnrecognisedType->throw(
505             error => 'Update type not allowed on this debit_type' );
506     }
507
508     my $schema = Koha::Database->new->schema;
509
510     $schema->txn_do(
511         sub {
512
513             my $amount_before             = $self->amount;
514             my $amount_outstanding_before = $self->amountoutstanding;
515             my $difference                = $amount - $amount_before;
516             my $new_outstanding           = $amount_outstanding_before + $difference;
517
518             my $offset_type = $debit_type_code;
519             $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
520
521             # Catch cases that require patron refunds
522             if ( $new_outstanding < 0 ) {
523                 my $account =
524                   Koha::Patrons->find( $self->borrowernumber )->account;
525                 my $credit = $account->add_credit(
526                     {
527                         amount      => $new_outstanding * -1,
528                         description => 'Overpayment refund',
529                         type        => 'CREDIT',
530                         interface   => $interface,
531                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
532                     }
533                 );
534                 $new_outstanding = 0;
535             }
536
537             # Update the account line
538             $self->set(
539                 {
540                     date              => \'NOW()',
541                     amount            => $amount,
542                     amountoutstanding => $new_outstanding,
543                 }
544             )->store();
545
546             # Record the account offset
547             my $account_offset = Koha::Account::Offset->new(
548                 {
549                     debit_id => $self->id,
550                     type     => $offset_type,
551                     amount   => $difference
552                 }
553             )->store();
554
555             if ( C4::Context->preference("FinesLog") ) {
556                 logaction(
557                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
558                     $self->borrowernumber,
559                     Dumper(
560                         {   action            => $update_type,
561                             borrowernumber    => $self->borrowernumber,
562                             amount            => $amount,
563                             description       => undef,
564                             amountoutstanding => $new_outstanding,
565                             debit_type_code   => $self->debit_type_code,
566                             note              => undef,
567                             itemnumber        => $self->itemnumber,
568                             manager_id        => undef,
569                         }
570                     )
571                 ) if ( $update_type eq 'overdue_update' );
572             }
573         }
574     );
575
576     return $self;
577 }
578
579 =head3 is_credit
580
581     my $bool = $line->is_credit;
582
583 =cut
584
585 sub is_credit {
586     my ($self) = @_;
587
588     return ( $self->amount < 0 );
589 }
590
591 =head3 is_debit
592
593     my $bool = $line->is_debit;
594
595 =cut
596
597 sub is_debit {
598     my ($self) = @_;
599
600     return !$self->is_credit;
601 }
602
603 =head3 to_api_mapping
604
605 This method returns the mapping for representing a Koha::Account::Line object
606 on the API.
607
608 =cut
609
610 sub to_api_mapping {
611     return {
612         accountlines_id   => 'account_line_id',
613         credit_type_code  => 'credit_type',
614         debit_type_code   => 'debit_type',
615         amountoutstanding => 'amount_outstanding',
616         borrowernumber    => 'patron_id',
617         branchcode        => 'library_id',
618         issue_id          => 'checkout_id',
619         itemnumber        => 'item_id',
620         manager_id        => 'user_id',
621         note              => 'internal_note',
622     };
623 }
624
625 =head2 Internal methods
626
627 =cut
628
629 =head3 _type
630
631 =cut
632
633 sub _type {
634     return 'Accountline';
635 }
636
637 1;
638
639 =head2 Name mappings
640
641 =head3 $allowed_update
642
643 =cut
644
645 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
646
647 =head1 AUTHORS
648
649 Kyle M Hall <kyle@bywatersolutions.com >
650 Tomás Cohen Arazi <tomascohen@theke.io>
651 Martin Renvoize <martin.renvoize@ptfs-europe.com>
652
653 =cut