Bug 23049: Update existing code to use debit_type
[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::DebitType;
26 use Koha::Account::Offsets;
27 use Koha::Database;
28 use Koha::Exceptions::Account;
29 use Koha::Items;
30
31 use base qw(Koha::Object);
32
33 =encoding utf8
34
35 =head1 NAME
36
37 Koha::Account::Line - Koha accountline Object class
38
39 =head1 API
40
41 =head2 Class methods
42
43 =cut
44
45 =head3 patron
46
47 Return the patron linked to this account line
48
49 =cut
50
51 sub patron {
52     my ( $self ) = @_;
53     my $rs = $self->_result->borrowernumber;
54     return unless $rs;
55     return Koha::Patron->_new_from_dbic( $rs );
56 }
57
58 =head3 item
59
60 Return the item linked to this account line if exists
61
62 =cut
63
64 sub item {
65     my ( $self ) = @_;
66     my $rs = $self->_result->itemnumber;
67     return unless $rs;
68     return Koha::Item->_new_from_dbic( $rs );
69 }
70
71 =head3 checkout
72
73 Return the checkout linked to this account line if exists
74
75 =cut
76
77 sub checkout {
78     my ( $self ) = @_;
79     return unless $self->issue_id ;
80
81     $self->{_checkout} ||= Koha::Checkouts->find( $self->issue_id );
82     $self->{_checkout} ||= Koha::Old::Checkouts->find( $self->issue_id );
83     return $self->{_checkout};
84 }
85
86 =head3 debit_type
87
88 Return the debit_type linked to this account line
89
90 =cut
91
92 sub debit_type {
93     my ( $self ) = @_;
94     my $rs = $self->_result->debit_type_code;
95     return unless $rs;
96     return Koha::Account::DebitType->_new_from_dbic( $rs );
97 }
98
99 =head3 void
100
101   $payment_accountline->void();
102
103 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
104 created by the application of this credit upon any debits and mark the credit
105 as 'void' by updating it's status to "VOID".
106
107 =cut
108
109 sub void {
110     my ($self) = @_;
111
112     # Make sure it is a payment we are voiding
113     return unless $self->amount < 0;
114
115     my @account_offsets =
116       Koha::Account::Offsets->search(
117         { credit_id => $self->id, amount => { '<' => 0 }  } );
118
119     $self->_result->result_source->schema->txn_do(
120         sub {
121             foreach my $account_offset (@account_offsets) {
122                 my $fee_paid =
123                   Koha::Account::Lines->find( $account_offset->debit_id );
124
125                 next unless $fee_paid;
126
127                 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
128                 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
129                 $fee_paid->amountoutstanding($new_amount);
130                 $fee_paid->store();
131
132                 Koha::Account::Offset->new(
133                     {
134                         credit_id => $self->id,
135                         debit_id  => $fee_paid->id,
136                         amount    => $amount_paid,
137                         type      => 'Void Payment',
138                     }
139                 )->store();
140             }
141
142             if ( C4::Context->preference("FinesLog") ) {
143                 logaction(
144                     "FINES", 'VOID',
145                     $self->borrowernumber,
146                     Dumper(
147                         {
148                             action         => 'void_payment',
149                             borrowernumber => $self->borrowernumber,
150                             amount            => $self->amount,
151                             amountoutstanding => $self->amountoutstanding,
152                             description       => $self->description,
153                             accounttype       => $self->accounttype,
154                             payment_type      => $self->payment_type,
155                             note              => $self->note,
156                             itemnumber        => $self->itemnumber,
157                             manager_id        => $self->manager_id,
158                             offsets =>
159                               [ map { $_->unblessed } @account_offsets ],
160                         }
161                     )
162                 );
163             }
164
165             $self->set(
166                 {
167                     status            => 'VOID',
168                     amountoutstanding => 0,
169                     amount            => 0,
170                 }
171             );
172             $self->store();
173         }
174     );
175
176 }
177
178 =head3 apply
179
180     my $debits = $account->outstanding_debits;
181     my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
182
183 Applies the credit to a given debits array reference.
184
185 =head4 arguments hashref
186
187 =over 4
188
189 =item debits - Koha::Account::Lines object set of debits
190
191 =item offset_type (optional) - a string indicating the offset type (valid values are those from
192 the 'account_offset_types' table)
193
194 =back
195
196 =cut
197
198 sub apply {
199     my ( $self, $params ) = @_;
200
201     my $debits      = $params->{debits};
202     my $offset_type = $params->{offset_type} // 'Credit Applied';
203
204     unless ( $self->is_credit ) {
205         Koha::Exceptions::Account::IsNotCredit->throw(
206             error => 'Account line ' . $self->id . ' is not a credit'
207         );
208     }
209
210     my $available_credit = $self->amountoutstanding * -1;
211
212     unless ( $available_credit > 0 ) {
213         Koha::Exceptions::Account::NoAvailableCredit->throw(
214             error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
215         );
216     }
217
218     my $schema = Koha::Database->new->schema;
219
220     $schema->txn_do( sub {
221         for my $debit ( @{$debits} ) {
222
223             unless ( $debit->is_debit ) {
224                 Koha::Exceptions::Account::IsNotDebit->throw(
225                     error => 'Account line ' . $debit->id . 'is not a debit'
226                 );
227             }
228             my $amount_to_cancel;
229             my $owed = $debit->amountoutstanding;
230
231             if ( $available_credit >= $owed ) {
232                 $amount_to_cancel = $owed;
233             }
234             else {    # $available_credit < $debit->amountoutstanding
235                 $amount_to_cancel = $available_credit;
236             }
237
238             # record the account offset
239             Koha::Account::Offset->new(
240                 {   credit_id => $self->id,
241                     debit_id  => $debit->id,
242                     amount    => $amount_to_cancel * -1,
243                     type      => $offset_type,
244                 }
245             )->store();
246
247             $available_credit -= $amount_to_cancel;
248
249             $self->amountoutstanding( $available_credit * -1 )->store;
250             $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
251
252             # Same logic exists in Koha::Account::pay
253             if (   $debit->amountoutstanding == 0
254                 && $debit->itemnumber
255                 && $debit->debit_type_code
256                 && $debit->debit_type_code eq 'LOST' )
257             {
258                 C4::Circulation::ReturnLostItem( $self->borrowernumber, $debit->itemnumber );
259             }
260
261         }
262     });
263
264     return $available_credit;
265 }
266
267 =head3 adjust
268
269 This method allows updating a debit or credit on a patron's account
270
271     $account_line->adjust(
272         {
273             amount    => $amount,
274             type      => $update_type,
275             interface => $interface
276         }
277     );
278
279 $update_type can be any of:
280   - overdue_update
281
282 Authors Note: The intention here is that this method is only used
283 to adjust accountlines where the final amount is not yet known/fixed.
284 Incrementing fines are the only existing case at the time of writing,
285 all other forms of 'adjustment' should be recorded as distinct credits
286 or debits and applied, via an offset, to the corresponding debit or credit.
287
288 =cut
289
290 sub adjust {
291     my ( $self, $params ) = @_;
292
293     my $amount       = $params->{amount};
294     my $update_type  = $params->{type};
295     my $interface    = $params->{interface};
296
297     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
298         Koha::Exceptions::Account::UnrecognisedType->throw(
299             error => 'Update type not recognised'
300         );
301     }
302
303     my $debit_type_code = $self->debit_type_code;
304     my $account_status  = $self->status;
305     unless (
306         (
307             exists(
308                 $Koha::Account::Line::allowed_update->{$update_type}
309                   ->{$debit_type_code}
310             )
311             && ( $Koha::Account::Line::allowed_update->{$update_type}
312                 ->{$debit_type_code} eq $account_status )
313         )
314       )
315     {
316         Koha::Exceptions::Account::UnrecognisedType->throw(
317             error => 'Update type not allowed on this debit_type' );
318     }
319
320     my $schema = Koha::Database->new->schema;
321
322     $schema->txn_do(
323         sub {
324
325             my $amount_before             = $self->amount;
326             my $amount_outstanding_before = $self->amountoutstanding;
327             my $difference                = $amount - $amount_before;
328             my $new_outstanding           = $amount_outstanding_before + $difference;
329
330             my $offset_type = $debit_type_code;
331             $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
332
333             # Catch cases that require patron refunds
334             if ( $new_outstanding < 0 ) {
335                 my $account =
336                   Koha::Patrons->find( $self->borrowernumber )->account;
337                 my $credit = $account->add_credit(
338                     {
339                         amount      => $new_outstanding * -1,
340                         description => 'Overpayment refund',
341                         type        => 'credit',
342                         interface   => $interface,
343                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
344                     }
345                 );
346                 $new_outstanding = 0;
347             }
348
349             # Update the account line
350             $self->set(
351                 {
352                     date              => \'NOW()',
353                     amount            => $amount,
354                     amountoutstanding => $new_outstanding,
355                 }
356             )->store();
357
358             # Record the account offset
359             my $account_offset = Koha::Account::Offset->new(
360                 {
361                     debit_id => $self->id,
362                     type     => $offset_type,
363                     amount   => $difference
364                 }
365             )->store();
366
367             if ( C4::Context->preference("FinesLog") ) {
368                 logaction(
369                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
370                     $self->borrowernumber,
371                     Dumper(
372                         {   action            => $update_type,
373                             borrowernumber    => $self->borrowernumber,
374                             amount            => $amount,
375                             description       => undef,
376                             amountoutstanding => $new_outstanding,
377                             debit_type_code   => $self->debit_type_code,
378                             note              => undef,
379                             itemnumber        => $self->itemnumber,
380                             manager_id        => undef,
381                         }
382                     )
383                 ) if ( $update_type eq 'overdue_update' );
384             }
385         }
386     );
387
388     return $self;
389 }
390
391 =head3 is_credit
392
393     my $bool = $line->is_credit;
394
395 =cut
396
397 sub is_credit {
398     my ($self) = @_;
399
400     return ( $self->amount < 0 );
401 }
402
403 =head3 is_debit
404
405     my $bool = $line->is_debit;
406
407 =cut
408
409 sub is_debit {
410     my ($self) = @_;
411
412     return !$self->is_credit;
413 }
414
415 =head3 to_api_mapping
416
417 This method returns the mapping for representing a Koha::Account::Line object
418 on the API.
419
420 =cut
421
422 sub to_api_mapping {
423     return {
424         accountlines_id   => 'account_line_id',
425         accounttype       => 'account_type',
426         amountoutstanding => 'amount_outstanding',
427         borrowernumber    => 'patron_id',
428         branchcode        => 'library_id',
429         issue_id          => 'checkout_id',
430         itemnumber        => 'item_id',
431         manager_id        => 'user_id',
432         note              => 'internal_note',
433     };
434 }
435
436 =head2 Internal methods
437
438 =cut
439
440 =head3 _type
441
442 =cut
443
444 sub _type {
445     return 'Accountline';
446 }
447
448 1;
449
450 =head2 Name mappings
451
452 =head3 $allowed_update
453
454 =cut
455
456 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
457
458 =head1 AUTHORS
459
460 Kyle M Hall <kyle@bywatersolutions.com >
461 Tomás Cohen Arazi <tomascohen@theke.io>
462 Martin Renvoize <martin.renvoize@ptfs-europe.com>
463
464 =cut