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