Bug 19191: Add ability to email receipts for account payments and write-offs
[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     require C4::Letters;
267     if (
268         my $letter = C4::Letters::GetPreparedLetter(
269             module                 => 'circulation',
270             letter_code            => uc("ACCOUNT_$type"),
271             message_transport_type => 'email',
272             lang    => Koha::Patrons->find( $self->{patron_id} )->lang,
273             objects => {
274                 patron  => scalar Koha::Patrons->find( $self->{patron_id} ),
275                 library => scalar Koha::Libraries->find( $self->{library_id} ),
276                 offsets => \@account_offsets,
277                 credit  => $payment,
278             },
279           )
280       )
281     {
282         C4::Letters::EnqueueLetter(
283             {
284                 letter                 => $letter,
285                 borrowernumber         => $self->{patron_id},
286                 message_transport_type => 'email',
287             }
288         ) or warn "can't enqueue letter $letter";
289     }
290
291     return $payment->id;
292 }
293
294 =head3 add_credit
295
296 This method allows adding credits to a patron's account
297
298 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
299     {
300         amount       => $amount,
301         description  => $description,
302         note         => $note,
303         user_id      => $user_id,
304         library_id   => $library_id,
305         sip          => $sip,
306         payment_type => $payment_type,
307         type         => $credit_type,
308         item_id      => $item_id
309     }
310 );
311
312 $credit_type can be any of:
313   - 'credit'
314   - 'payment'
315   - 'forgiven'
316   - 'lost_item_return'
317   - 'writeoff'
318
319 =cut
320
321 sub add_credit {
322
323     my ( $self, $params ) = @_;
324
325     # amount is passed as a positive value, but we store credit as negative values
326     my $amount       = $params->{amount} * -1;
327     my $description  = $params->{description} // q{};
328     my $note         = $params->{note} // q{};
329     my $user_id      = $params->{user_id};
330     my $library_id   = $params->{library_id};
331     my $sip          = $params->{sip};
332     my $payment_type = $params->{payment_type};
333     my $type         = $params->{type} || 'payment';
334     my $item_id      = $params->{item_id};
335
336     my $schema = Koha::Database->new->schema;
337
338     my $account_type = $Koha::Account::account_type->{$type};
339     $account_type .= $sip
340         if defined $sip &&
341            $type eq 'payment';
342
343     my $line;
344
345     $schema->txn_do(
346         sub {
347             # We should remove accountno, it is no longer needed
348             my $last = Koha::Account::Lines->search( { borrowernumber => $self->{patron_id} },
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     my $fines = Koha::Account::Lines->search(
420         {
421             borrowernumber => $self->{patron_id},
422         },
423         {
424             select => [ { sum => 'amountoutstanding' } ],
425             as => ['total_amountoutstanding'],
426         }
427     );
428
429     return ( $fines->count )
430       ? $fines->next->get_column('total_amountoutstanding') + 0
431       : 0;
432 }
433
434 =head3 outstanding_debits
435
436 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
437
438 =cut
439
440 sub outstanding_debits {
441     my ($self) = @_;
442
443     my $lines = Koha::Account::Lines->search(
444         {
445             borrowernumber    => $self->{patron_id},
446             amountoutstanding => { '>' => 0 }
447         }
448     );
449
450     return $lines;
451 }
452
453 =head3 outstanding_credits
454
455 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
456
457 =cut
458
459 sub outstanding_credits {
460     my ($self) = @_;
461
462     my $lines = Koha::Account::Lines->search(
463         {
464             borrowernumber    => $self->{patron_id},
465             amountoutstanding => { '<' => 0 }
466         }
467     );
468
469     return $lines;
470 }
471
472 =head3 non_issues_charges
473
474 my $non_issues_charges = $self->non_issues_charges
475
476 Calculates amount immediately owing by the patron - non-issue charges.
477
478 Charges exempt from non-issue are:
479 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
480 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
481 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
482
483 =cut
484
485 sub non_issues_charges {
486     my ($self) = @_;
487
488     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
489     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
490
491     my @not_fines;
492     push @not_fines, 'Res'
493       unless C4::Context->preference('HoldsInNoissuesCharge');
494     push @not_fines, 'Rent'
495       unless C4::Context->preference('RentalsInNoissuesCharge');
496     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
497         my $dbh = C4::Context->dbh;
498         push @not_fines,
499           @{
500             $dbh->selectcol_arrayref(q|
501                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
502             |)
503           };
504     }
505     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
506
507     my $non_issues_charges = Koha::Account::Lines->search(
508         {
509             borrowernumber => $self->{patron_id},
510             accounttype    => { -not_in => \@not_fines }
511         },
512         {
513             select => [ { sum => 'amountoutstanding' } ],
514             as     => ['non_issues_charges'],
515         }
516     );
517     return $non_issues_charges->count
518       ? $non_issues_charges->next->get_column('non_issues_charges') + 0
519       : 0;
520 }
521
522 1;
523
524 =head2 Name mappings
525
526 =head3 $offset_type
527
528 =cut
529
530 our $offset_type = {
531     'credit'           => 'Manual Credit',
532     'forgiven'         => 'Writeoff',
533     'lost_item_return' => 'Lost Item Return',
534     'payment'          => 'Payment',
535     'writeoff'         => 'Writeoff'
536 };
537
538 =head3 $account_type
539
540 =cut
541
542 our $account_type = {
543     'credit'           => 'C',
544     'forgiven'         => 'FOR',
545     'lost_item_return' => 'CR',
546     'payment'          => 'Pay',
547     'writeoff'         => 'W'
548 };
549
550 =head1 AUTHOR
551
552 Kyle M Hall <kyle.m.hall@gmail.com>
553
554 =cut