Bug 19604: Fix perlcritic "Loop iterator is not lexical"
[koha.git] / Koha / Account.pm
1 package Koha::Account;
2
3 # Copyright 2016 ByWater Solutions
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Carp;
23 use Data::Dumper;
24 use List::MoreUtils qw( uniq );
25
26 use C4::Log qw( logaction );
27 use C4::Stats qw( UpdateStats );
28
29 use Koha::Patrons;
30 use Koha::Account::Lines;
31 use Koha::Account::Offsets;
32 use Koha::DateUtils qw( dt_from_string );
33
34 =head1 NAME
35
36 Koha::Accounts - Module for managing payments and fees for patrons
37
38 =cut
39
40 sub new {
41     my ( $class, $params ) = @_;
42
43     Carp::croak("No patron id passed in!") unless $params->{patron_id};
44
45     return bless( $params, $class );
46 }
47
48 =head2 pay
49
50 This method allows payments to be made against fees/fines
51
52 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
53     {
54         amount      => $amount,
55         sip         => $sipmode,
56         note        => $note,
57         description => $description,
58         library_id  => $branchcode,
59         lines        => $lines, # Arrayref of Koha::Account::Line objects to pay
60         account_type => $type,  # accounttype code
61         offset_type => $offset_type,    # offset type code
62     }
63 );
64
65 =cut
66
67 sub pay {
68     my ( $self, $params ) = @_;
69
70     my $amount       = $params->{amount};
71     my $sip          = $params->{sip};
72     my $description  = $params->{description};
73     my $note         = $params->{note} || q{};
74     my $library_id   = $params->{library_id};
75     my $lines        = $params->{lines};
76     my $type         = $params->{type} || 'payment';
77     my $payment_type = $params->{payment_type} || undef;
78     my $account_type = $params->{account_type};
79     my $offset_type  = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
80
81     my $userenv = C4::Context->userenv;
82
83     my $patron = Koha::Patrons->find( $self->{patron_id} );
84
85     # We should remove accountno, it is no longer needed
86     my $last = Koha::Account::Lines->search(
87         {
88             borrowernumber => $self->{patron_id}
89         },
90         {
91             order_by => 'accountno'
92         }
93     )->next();
94     my $accountno = $last ? $last->accountno + 1 : 1;
95
96     my $manager_id = $userenv ? $userenv->{number} : 0;
97
98     my @fines_paid; # List of account lines paid on with this payment
99
100     my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
101     $balance_remaining ||= 0;
102
103     my @account_offsets;
104
105     # We were passed a specific line to pay
106     foreach my $fine ( @$lines ) {
107         my $amount_to_pay =
108             $fine->amountoutstanding > $balance_remaining
109           ? $balance_remaining
110           : $fine->amountoutstanding;
111
112         my $old_amountoutstanding = $fine->amountoutstanding;
113         my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
114         $fine->amountoutstanding($new_amountoutstanding)->store();
115         $balance_remaining = $balance_remaining - $amount_to_pay;
116
117         if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
118         {
119             C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
120         }
121
122         my $account_offset = Koha::Account::Offset->new(
123             {
124                 debit_id => $fine->id,
125                 type     => $offset_type,
126                 amount   => $amount_to_pay * -1,
127             }
128         );
129         push( @account_offsets, $account_offset );
130
131         if ( C4::Context->preference("FinesLog") ) {
132             logaction(
133                 "FINES", 'MODIFY',
134                 $self->{patron_id},
135                 Dumper(
136                     {
137                         action                => 'fee_payment',
138                         borrowernumber        => $fine->borrowernumber,
139                         old_amountoutstanding => $old_amountoutstanding,
140                         new_amountoutstanding => 0,
141                         amount_paid           => $old_amountoutstanding,
142                         accountlines_id       => $fine->id,
143                         accountno             => $fine->accountno,
144                         manager_id            => $manager_id,
145                         note                  => $note,
146                     }
147                 )
148             );
149             push( @fines_paid, $fine->id );
150         }
151     }
152
153     # Were not passed a specific line to pay, or the payment was for more
154     # than the what was owed on the given line. In that case pay down other
155     # lines with remaining balance.
156     my @outstanding_fines;
157     @outstanding_fines = Koha::Account::Lines->search(
158         {
159             borrowernumber    => $self->{patron_id},
160             amountoutstanding => { '>' => 0 },
161         }
162     ) if $balance_remaining > 0;
163
164     foreach my $fine (@outstanding_fines) {
165         my $amount_to_pay =
166             $fine->amountoutstanding > $balance_remaining
167           ? $balance_remaining
168           : $fine->amountoutstanding;
169
170         my $old_amountoutstanding = $fine->amountoutstanding;
171         $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
172         $fine->store();
173
174         my $account_offset = Koha::Account::Offset->new(
175             {
176                 debit_id => $fine->id,
177                 type     => $offset_type,
178                 amount   => $amount_to_pay * -1,
179             }
180         );
181         push( @account_offsets, $account_offset );
182
183         if ( C4::Context->preference("FinesLog") ) {
184             logaction(
185                 "FINES", 'MODIFY',
186                 $self->{patron_id},
187                 Dumper(
188                     {
189                         action                => "fee_$type",
190                         borrowernumber        => $fine->borrowernumber,
191                         old_amountoutstanding => $old_amountoutstanding,
192                         new_amountoutstanding => $fine->amountoutstanding,
193                         amount_paid           => $amount_to_pay,
194                         accountlines_id       => $fine->id,
195                         accountno             => $fine->accountno,
196                         manager_id            => $manager_id,
197                         note                  => $note,
198                     }
199                 )
200             );
201             push( @fines_paid, $fine->id );
202         }
203
204         $balance_remaining = $balance_remaining - $amount_to_pay;
205         last unless $balance_remaining > 0;
206     }
207
208     $account_type ||=
209         $type eq 'writeoff' ? 'W'
210       : defined($sip)       ? "Pay$sip"
211       :                       'Pay';
212
213     $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
214
215     my $payment = Koha::Account::Line->new(
216         {
217             borrowernumber    => $self->{patron_id},
218             accountno         => $accountno,
219             date              => dt_from_string(),
220             amount            => 0 - $amount,
221             description       => $description,
222             accounttype       => $account_type,
223             payment_type      => $payment_type,
224             amountoutstanding => 0 - $balance_remaining,
225             manager_id        => $manager_id,
226             note              => $note,
227         }
228     )->store();
229
230     foreach my $o ( @account_offsets ) {
231         $o->credit_id( $payment->id() );
232         $o->store();
233     }
234
235     $library_id ||= $userenv ? $userenv->{'branch'} : undef;
236
237     UpdateStats(
238         {
239             branch         => $library_id,
240             type           => $type,
241             amount         => $amount,
242             borrowernumber => $self->{patron_id},
243             accountno      => $accountno,
244         }
245     );
246
247     if ( C4::Context->preference("FinesLog") ) {
248         logaction(
249             "FINES", 'CREATE',
250             $self->{patron_id},
251             Dumper(
252                 {
253                     action            => "create_$type",
254                     borrowernumber    => $self->{patron_id},
255                     accountno         => $accountno,
256                     amount            => 0 - $amount,
257                     amountoutstanding => 0 - $balance_remaining,
258                     accounttype       => $account_type,
259                     accountlines_paid => \@fines_paid,
260                     manager_id        => $manager_id,
261                 }
262             )
263         );
264     }
265
266     if ( C4::Context->preference('UseEmailReceipts') ) {
267         require C4::Letters;
268         if (
269             my $letter = C4::Letters::GetPreparedLetter(
270                 module                 => 'circulation',
271                 letter_code            => uc("ACCOUNT_$type"),
272                 message_transport_type => 'email',
273                 lang    => $patron->lang,
274                 tables => {
275                     borrowers       => $self->{patron_id},
276                     branches        => $self->{library_id},
277                 },
278                 substitute => {
279                     credit => $payment,
280                     offsets => \@account_offsets,
281                 },
282               )
283           )
284         {
285             C4::Letters::EnqueueLetter(
286                 {
287                     letter                 => $letter,
288                     borrowernumber         => $self->{patron_id},
289                     message_transport_type => 'email',
290                 }
291             ) or warn "can't enqueue letter $letter";
292         }
293     }
294
295     return $payment->id;
296 }
297
298 =head3 add_credit
299
300 This method allows adding credits to a patron's account
301
302 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
303     {
304         amount       => $amount,
305         description  => $description,
306         note         => $note,
307         user_id      => $user_id,
308         library_id   => $library_id,
309         sip          => $sip,
310         payment_type => $payment_type,
311         type         => $credit_type,
312         item_id      => $item_id
313     }
314 );
315
316 $credit_type can be any of:
317   - 'credit'
318   - 'payment'
319   - 'forgiven'
320   - 'lost_item_return'
321   - 'writeoff'
322
323 =cut
324
325 sub add_credit {
326
327     my ( $self, $params ) = @_;
328
329     # amount is passed as a positive value, but we store credit as negative values
330     my $amount       = $params->{amount} * -1;
331     my $description  = $params->{description} // q{};
332     my $note         = $params->{note} // q{};
333     my $user_id      = $params->{user_id};
334     my $library_id   = $params->{library_id};
335     my $sip          = $params->{sip};
336     my $payment_type = $params->{payment_type};
337     my $type         = $params->{type} || 'payment';
338     my $item_id      = $params->{item_id};
339
340     my $schema = Koha::Database->new->schema;
341
342     my $account_type = $Koha::Account::account_type->{$type};
343     $account_type .= $sip
344         if defined $sip &&
345            $type eq 'payment';
346
347     my $line;
348
349     $schema->txn_do(
350         sub {
351             # We should remove accountno, it is no longer needed
352             my $last = Koha::Account::Lines->search( { borrowernumber => $self->{patron_id} },
353                 { order_by => 'accountno' } )->next();
354             my $accountno = $last ? $last->accountno + 1 : 1;
355
356             # Insert the account line
357             $line = Koha::Account::Line->new(
358                 {   borrowernumber    => $self->{patron_id},
359                     date              => \'NOW()',
360                     amount            => $amount,
361                     description       => $description,
362                     accounttype       => $account_type,
363                     amountoutstanding => $amount,
364                     payment_type      => $payment_type,
365                     note              => $note,
366                     manager_id        => $user_id,
367                     itemnumber        => $item_id
368                 }
369             )->store();
370
371             # Record the account offset
372             my $account_offset = Koha::Account::Offset->new(
373                 {   credit_id => $line->id,
374                     type      => $Koha::Account::offset_type->{$type},
375                     amount    => $amount
376                 }
377             )->store();
378
379             UpdateStats(
380                 {   branch         => $library_id,
381                     type           => $type,
382                     amount         => $amount,
383                     borrowernumber => $self->{patron_id},
384                     accountno      => $accountno,
385                 }
386             ) if grep { $type eq $_ } ('payment', 'writeoff') ;
387
388             if ( C4::Context->preference("FinesLog") ) {
389                 logaction(
390                     "FINES", 'CREATE',
391                     $self->{patron_id},
392                     Dumper(
393                         {   action            => "create_$type",
394                             borrowernumber    => $self->{patron_id},
395                             accountno         => $accountno,
396                             amount            => $amount,
397                             description       => $description,
398                             amountoutstanding => $amount,
399                             accounttype       => $account_type,
400                             note              => $note,
401                             itemnumber        => $item_id,
402                             manager_id        => $user_id,
403                         }
404                     )
405                 );
406             }
407         }
408     );
409
410     return $line;
411 }
412
413 =head3 balance
414
415 my $balance = $self->balance
416
417 Return the balance (sum of amountoutstanding columns)
418
419 =cut
420
421 sub balance {
422     my ($self) = @_;
423     my $fines = Koha::Account::Lines->search(
424         {
425             borrowernumber => $self->{patron_id},
426         },
427         {
428             select => [ { sum => 'amountoutstanding' } ],
429             as => ['total_amountoutstanding'],
430         }
431     );
432
433     return ( $fines->count )
434       ? $fines->next->get_column('total_amountoutstanding') + 0
435       : 0;
436 }
437
438 =head3 outstanding_debits
439
440 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
441
442 =cut
443
444 sub outstanding_debits {
445     my ($self) = @_;
446
447     my $lines = Koha::Account::Lines->search(
448         {
449             borrowernumber    => $self->{patron_id},
450             amountoutstanding => { '>' => 0 }
451         }
452     );
453
454     return $lines;
455 }
456
457 =head3 outstanding_credits
458
459 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
460
461 =cut
462
463 sub outstanding_credits {
464     my ($self) = @_;
465
466     my $lines = Koha::Account::Lines->search(
467         {
468             borrowernumber    => $self->{patron_id},
469             amountoutstanding => { '<' => 0 }
470         }
471     );
472
473     return $lines;
474 }
475
476 =head3 non_issues_charges
477
478 my $non_issues_charges = $self->non_issues_charges
479
480 Calculates amount immediately owing by the patron - non-issue charges.
481
482 Charges exempt from non-issue are:
483 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
484 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
485 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
486
487 =cut
488
489 sub non_issues_charges {
490     my ($self) = @_;
491
492     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
493     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
494
495     my @not_fines;
496     push @not_fines, 'Res'
497       unless C4::Context->preference('HoldsInNoissuesCharge');
498     push @not_fines, 'Rent'
499       unless C4::Context->preference('RentalsInNoissuesCharge');
500     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
501         my $dbh = C4::Context->dbh;
502         push @not_fines,
503           @{
504             $dbh->selectcol_arrayref(q|
505                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
506             |)
507           };
508     }
509     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
510
511     my $non_issues_charges = Koha::Account::Lines->search(
512         {
513             borrowernumber => $self->{patron_id},
514             accounttype    => { -not_in => \@not_fines }
515         },
516         {
517             select => [ { sum => 'amountoutstanding' } ],
518             as     => ['non_issues_charges'],
519         }
520     );
521     return $non_issues_charges->count
522       ? $non_issues_charges->next->get_column('non_issues_charges') + 0
523       : 0;
524 }
525
526 1;
527
528 =head2 Name mappings
529
530 =head3 $offset_type
531
532 =cut
533
534 our $offset_type = {
535     'credit'           => 'Manual Credit',
536     'forgiven'         => 'Writeoff',
537     'lost_item_return' => 'Lost Item Return',
538     'payment'          => 'Payment',
539     'writeoff'         => 'Writeoff'
540 };
541
542 =head3 $account_type
543
544 =cut
545
546 our $account_type = {
547     'credit'           => 'C',
548     'forgiven'         => 'FOR',
549     'lost_item_return' => 'CR',
550     'payment'          => 'Pay',
551     'writeoff'         => 'W'
552 };
553
554 =head1 AUTHOR
555
556 Kyle M Hall <kyle.m.hall@gmail.com>
557
558 =cut