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