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