ef9f4f90637b02950dd1cff6b24db7fa5d6482e6
[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         }
235     );
236
237 $update_type can be any of:
238   - fine_update
239
240 Authors Note: The intention here is that this method is only used
241 to adjust accountlines where the final amount is not yet known/fixed.
242 Incrementing fines are the only existing case at the time of writing,
243 all other forms of 'adjustment' should be recorded as distinct credits
244 or debits and applied, via an offset, to the corresponding debit or credit.
245
246 =cut
247
248 sub adjust {
249     my ( $self, $params ) = @_;
250
251     my $amount       = $params->{amount};
252     my $update_type  = $params->{type};
253
254     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
255         Koha::Exceptions::Account::UnrecognisedType->throw(
256             error => 'Update type not recognised'
257         );
258     }
259
260     my $account_type = $self->accounttype;
261     unless ( $Koha::Account::Line::allowed_update->{$update_type} eq $account_type ) {
262         Koha::Exceptions::Account::UnrecognisedType->throw(
263             error => 'Update type not allowed on this accounttype'
264         );
265     }
266
267     my $schema = Koha::Database->new->schema;
268
269     $schema->txn_do(
270         sub {
271
272             my $amount_before             = $self->amount;
273             my $amount_outstanding_before = $self->amountoutstanding;
274             my $difference                = $amount - $amount_before;
275             my $new_outstanding           = $amount_outstanding_before + $difference;
276
277             my $offset_type = substr( $update_type, 0, index( $update_type, '_' ) );
278             $offset_type .= ( $difference > 0 ) ? "_increase" : "_decrease";
279
280             # Catch cases that require patron refunds
281             if ( $new_outstanding < 0 ) {
282                 my $account =
283                   Koha::Patrons->find( $self->borrowernumber )->account;
284                 my $credit = $account->add_credit(
285                     {
286                         amount      => $new_outstanding * -1,
287                         description => 'Overpayment refund',
288                         type        => 'credit',
289                         ( $update_type eq 'fine_update' ? ( item_id => $self->itemnumber ) : ()),
290                     }
291                 );
292                 $new_outstanding = 0;
293             }
294
295             # Update the account line
296             $self->set(
297                 {
298                     date              => \'NOW()',
299                     amount            => $amount,
300                     amountoutstanding => $new_outstanding
301                 }
302             )->store();
303
304             # Record the account offset
305             my $account_offset = Koha::Account::Offset->new(
306                 {
307                     debit_id => $self->id,
308                     type     => $offset_type,
309                     amount   => $difference
310                 }
311             )->store();
312
313             if ( C4::Context->preference("FinesLog") ) {
314                 logaction(
315                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
316                     $self->borrowernumber,
317                     Dumper(
318                         {   action            => $update_type,
319                             borrowernumber    => $self->borrowernumber,
320                             amount            => $amount,
321                             description       => undef,
322                             amountoutstanding => $new_outstanding,
323                             accounttype       => $self->accounttype,
324                             note              => undef,
325                             itemnumber        => $self->itemnumber,
326                             manager_id        => undef,
327                         }
328                     )
329                 ) if ( $update_type eq 'fine_update' );
330             }
331         }
332     );
333
334     return $self;
335 }
336
337 =head3 is_credit
338
339     my $bool = $line->is_credit;
340
341 =cut
342
343 sub is_credit {
344     my ($self) = @_;
345
346     return ( $self->amount < 0 );
347 }
348
349 =head3 is_debit
350
351     my $bool = $line->is_debit;
352
353 =cut
354
355 sub is_debit {
356     my ($self) = @_;
357
358     return !$self->is_credit;
359 }
360
361 =head2 Internal methods
362
363 =cut
364
365 =head3 _type
366
367 =cut
368
369 sub _type {
370     return 'Accountline';
371 }
372
373 1;
374
375 =head2 Name mappings
376
377 =head3 $allowed_update
378
379 =cut
380
381 our $allowed_update = { 'fine_update' => 'FU', };
382
383 =head1 AUTHORS
384
385 Kyle M Hall <kyle@bywatersolutions.com >
386 Tomás Cohen Arazi <tomascohen@theke.io>
387 Martin Renvoize <martin.renvoize@ptfs-europe.com>
388
389 =cut