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