Bug 21747: (follow-up) Intelligent rename of offsets
[koha-ffzg.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_update
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::allowed_update->{$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             my $offset_type = substr( $update_type, 0, index( $update_type, '_' ) );
263             $offset_type .= ( $difference > 0 ) ? "_increase" : "_decrease";
264
265             # Catch cases that require patron refunds
266             if ( $new_outstanding < 0 ) {
267                 my $account =
268                   Koha::Patrons->find( $self->borrowernumber )->account;
269                 my $credit = $account->add_credit(
270                     {
271                         amount      => $new_outstanding * -1,
272                         description => 'Overpayment refund',
273                         type        => 'credit',
274                         ( $update_type eq 'fine_update' ? ( item_id => $self->itemnumber ) : ()),
275                     }
276                 );
277                 $new_outstanding = 0;
278             }
279
280             # Update the account line
281             $self->set(
282                 {
283                     date              => \'NOW()',
284                     amount            => $amount,
285                     amountoutstanding => $new_outstanding,
286                     ( $update_type eq 'fine_update' ? ( lastincrement => $difference ) : ()),
287                 }
288             )->store();
289
290             # Record the account offset
291             my $account_offset = Koha::Account::Offset->new(
292                 {
293                     debit_id => $self->id,
294                     type     => $offset_type,
295                     amount   => $difference
296                 }
297             )->store();
298
299             if ( C4::Context->preference("FinesLog") ) {
300                 logaction(
301                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
302                     $self->borrowernumber,
303                     Dumper(
304                         {   action            => $update_type,
305                             borrowernumber    => $self->borrowernumber,
306                             accountno         => $self->accountno,
307                             amount            => $amount,
308                             description       => undef,
309                             amountoutstanding => $new_outstanding,
310                             accounttype       => $self->accounttype,
311                             note              => undef,
312                             itemnumber        => $self->itemnumber,
313                             manager_id        => undef,
314                         }
315                     )
316                 ) if ( $update_type eq 'fine_update' );
317             }
318         }
319     );
320
321     return $self;
322 }
323
324 =head3 is_credit
325
326     my $bool = $line->is_credit;
327
328 =cut
329
330 sub is_credit {
331     my ($self) = @_;
332
333     return ( $self->amount < 0 );
334 }
335
336 =head3 is_debit
337
338     my $bool = $line->is_debit;
339
340 =cut
341
342 sub is_debit {
343     my ($self) = @_;
344
345     return !$self->is_credit;
346 }
347
348 =head2 Internal methods
349
350 =cut
351
352 =head3 _type
353
354 =cut
355
356 sub _type {
357     return 'Accountline';
358 }
359
360 1;
361
362 =head2 Name mappings
363
364 =head3 $allowed_update
365
366 =cut
367
368 our $allowed_update = { 'fine_update' => '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