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