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