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