Bug 21896: (QA follow-up) normalize_balance -> reconcile_balance rename
[koha-equinox.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             note              => $note,
223         }
224     )->store();
225
226     foreach my $o ( @account_offsets ) {
227         $o->credit_id( $payment->id() );
228         $o->store();
229     }
230
231     $library_id ||= $userenv ? $userenv->{'branch'} : undef;
232
233     UpdateStats(
234         {
235             branch         => $library_id,
236             type           => $type,
237             amount         => $amount,
238             borrowernumber => $self->{patron_id},
239             accountno      => $accountno,
240         }
241     );
242
243     if ( C4::Context->preference("FinesLog") ) {
244         logaction(
245             "FINES", 'CREATE',
246             $self->{patron_id},
247             Dumper(
248                 {
249                     action            => "create_$type",
250                     borrowernumber    => $self->{patron_id},
251                     accountno         => $accountno,
252                     amount            => 0 - $amount,
253                     amountoutstanding => 0 - $balance_remaining,
254                     accounttype       => $account_type,
255                     accountlines_paid => \@fines_paid,
256                     manager_id        => $manager_id,
257                 }
258             )
259         );
260     }
261
262     if ( C4::Context->preference('UseEmailReceipts') ) {
263         if (
264             my $letter = C4::Letters::GetPreparedLetter(
265                 module                 => 'circulation',
266                 letter_code            => uc("ACCOUNT_$type"),
267                 message_transport_type => 'email',
268                 lang    => $patron->lang,
269                 tables => {
270                     borrowers       => $self->{patron_id},
271                     branches        => $self->{library_id},
272                 },
273                 substitute => {
274                     credit => $payment,
275                     offsets => \@account_offsets,
276                 },
277               )
278           )
279         {
280             C4::Letters::EnqueueLetter(
281                 {
282                     letter                 => $letter,
283                     borrowernumber         => $self->{patron_id},
284                     message_transport_type => 'email',
285                 }
286             ) or warn "can't enqueue letter $letter";
287         }
288     }
289
290     return $payment->id;
291 }
292
293 =head3 add_credit
294
295 This method allows adding credits to a patron's account
296
297 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
298     {
299         amount       => $amount,
300         description  => $description,
301         note         => $note,
302         user_id      => $user_id,
303         library_id   => $library_id,
304         sip          => $sip,
305         payment_type => $payment_type,
306         type         => $credit_type,
307         item_id      => $item_id
308     }
309 );
310
311 $credit_type can be any of:
312   - 'credit'
313   - 'payment'
314   - 'forgiven'
315   - 'lost_item_return'
316   - 'writeoff'
317
318 =cut
319
320 sub add_credit {
321
322     my ( $self, $params ) = @_;
323
324     # amount is passed as a positive value, but we store credit as negative values
325     my $amount       = $params->{amount} * -1;
326     my $description  = $params->{description} // q{};
327     my $note         = $params->{note} // q{};
328     my $user_id      = $params->{user_id};
329     my $library_id   = $params->{library_id};
330     my $sip          = $params->{sip};
331     my $payment_type = $params->{payment_type};
332     my $type         = $params->{type} || 'payment';
333     my $item_id      = $params->{item_id};
334
335     my $schema = Koha::Database->new->schema;
336
337     my $account_type = $Koha::Account::account_type->{$type};
338     $account_type .= $sip
339         if defined $sip &&
340            $type eq 'payment';
341
342     my $line;
343
344     $schema->txn_do(
345         sub {
346             # We should remove accountno, it is no longer needed
347             my $last = $self->lines->search(
348                 {},
349                 { order_by => 'accountno' } )->next();
350             my $accountno = $last ? $last->accountno + 1 : 1;
351
352             # Insert the account line
353             $line = Koha::Account::Line->new(
354                 {   borrowernumber    => $self->{patron_id},
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                     itemnumber        => $item_id
364                 }
365             )->store();
366
367             # Record the account offset
368             my $account_offset = Koha::Account::Offset->new(
369                 {   credit_id => $line->id,
370                     type      => $Koha::Account::offset_type->{$type},
371                     amount    => $amount
372                 }
373             )->store();
374
375             UpdateStats(
376                 {   branch         => $library_id,
377                     type           => $type,
378                     amount         => $amount,
379                     borrowernumber => $self->{patron_id},
380                     accountno      => $accountno,
381                 }
382             ) if grep { $type eq $_ } ('payment', 'writeoff') ;
383
384             if ( C4::Context->preference("FinesLog") ) {
385                 logaction(
386                     "FINES", 'CREATE',
387                     $self->{patron_id},
388                     Dumper(
389                         {   action            => "create_$type",
390                             borrowernumber    => $self->{patron_id},
391                             accountno         => $accountno,
392                             amount            => $amount,
393                             description       => $description,
394                             amountoutstanding => $amount,
395                             accounttype       => $account_type,
396                             note              => $note,
397                             itemnumber        => $item_id,
398                             manager_id        => $user_id,
399                         }
400                     )
401                 );
402             }
403         }
404     );
405
406     return $line;
407 }
408
409 =head3 balance
410
411 my $balance = $self->balance
412
413 Return the balance (sum of amountoutstanding columns)
414
415 =cut
416
417 sub balance {
418     my ($self) = @_;
419     return $self->lines->total_outstanding;
420 }
421
422 =head3 outstanding_debits
423
424 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
425
426 =cut
427
428 sub outstanding_debits {
429     my ($self) = @_;
430
431     my $lines = $self->lines->search(
432         {
433             amountoutstanding => { '>' => 0 }
434         }
435     );
436
437     return $lines;
438 }
439
440 =head3 outstanding_credits
441
442 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
443
444 =cut
445
446 sub outstanding_credits {
447     my ($self) = @_;
448
449     my $lines = $self->lines->search(
450         {
451             amountoutstanding => { '<' => 0 }
452         }
453     );
454
455     return $lines;
456 }
457
458 =head3 non_issues_charges
459
460 my $non_issues_charges = $self->non_issues_charges
461
462 Calculates amount immediately owing by the patron - non-issue charges.
463
464 Charges exempt from non-issue are:
465 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
466 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
467 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
468
469 =cut
470
471 sub non_issues_charges {
472     my ($self) = @_;
473
474     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
475     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
476
477     my @not_fines;
478     push @not_fines, 'Res'
479       unless C4::Context->preference('HoldsInNoissuesCharge');
480     push @not_fines, 'Rent'
481       unless C4::Context->preference('RentalsInNoissuesCharge');
482     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
483         my $dbh = C4::Context->dbh;
484         push @not_fines,
485           @{
486             $dbh->selectcol_arrayref(q|
487                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
488             |)
489           };
490     }
491     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
492
493     return $self->lines->search(
494         {
495             accounttype    => { -not_in => \@not_fines }
496         },
497     )->total_outstanding;
498 }
499
500 =head3 lines
501
502 my $lines = $self->lines;
503
504 Return all credits and debits for the user, outstanding or otherwise
505
506 =cut
507
508 sub lines {
509     my ($self) = @_;
510
511     return Koha::Account::Lines->search(
512         {
513             borrowernumber => $self->{patron_id},
514         }
515     );
516 }
517
518 =head3 reconcile_balance
519
520 $account->reconcile_balance();
521
522 Find outstanding credits and use them to pay outstanding debits
523
524 =cut
525
526 sub reconcile_balance {
527     my ($self) = @_;
528
529     my $outstanding_debits  = $self->outstanding_debits;
530     my $outstanding_credits = $self->outstanding_credits;
531
532     while (     $outstanding_debits->total_outstanding > 0
533             and my $credit = $outstanding_credits->next )
534     {
535         # there's both outstanding debits and credits
536         $credit->apply( { debits => $outstanding_debits } );    # applying credit, no special offset
537
538         $outstanding_debits = $self->outstanding_debits;
539
540     }
541
542     return $self;
543 }
544
545 1;
546
547 =head2 Name mappings
548
549 =head3 $offset_type
550
551 =cut
552
553 our $offset_type = {
554     'credit'           => 'Manual Credit',
555     'forgiven'         => 'Writeoff',
556     'lost_item_return' => 'Lost Item',
557     'payment'          => 'Payment',
558     'writeoff'         => 'Writeoff'
559 };
560
561 =head3 $account_type
562
563 =cut
564
565 our $account_type = {
566     'credit'           => 'C',
567     'forgiven'         => 'FOR',
568     'lost_item_return' => 'CR',
569     'payment'          => 'Pay',
570     'writeoff'         => 'W'
571 };
572
573 =head1 AUTHOR
574
575 Kyle M Hall <kyle.m.hall@gmail.com>
576
577 =cut