Bug 22501: (QA follow-up) use $raw for the note in the intranet
[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             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 It returns the debit lines with outstanding amounts for the patron.
427
428 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
429 return a list of Koha::Account::Line objects.
430
431 =cut
432
433 sub outstanding_debits {
434     my ($self) = @_;
435
436     return $self->lines->search(
437         {
438             amount            => { '>' => 0 },
439             amountoutstanding => { '>' => 0 }
440         }
441     );
442 }
443
444 =head3 outstanding_credits
445
446 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
447
448 It returns the credit lines with outstanding amounts for the patron.
449
450 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
451 return a list of Koha::Account::Line objects.
452
453 =cut
454
455 sub outstanding_credits {
456     my ($self) = @_;
457
458     return $self->lines->search(
459         {
460             amount            => { '<' => 0 },
461             amountoutstanding => { '<' => 0 }
462         }
463     );
464 }
465
466 =head3 non_issues_charges
467
468 my $non_issues_charges = $self->non_issues_charges
469
470 Calculates amount immediately owing by the patron - non-issue charges.
471
472 Charges exempt from non-issue are:
473 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
474 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
475 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
476
477 =cut
478
479 sub non_issues_charges {
480     my ($self) = @_;
481
482     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
483     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
484
485     my @not_fines;
486     push @not_fines, 'Res'
487       unless C4::Context->preference('HoldsInNoissuesCharge');
488     push @not_fines, 'Rent'
489       unless C4::Context->preference('RentalsInNoissuesCharge');
490     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
491         my $dbh = C4::Context->dbh;
492         push @not_fines,
493           @{
494             $dbh->selectcol_arrayref(q|
495                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
496             |)
497           };
498     }
499     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
500
501     return $self->lines->search(
502         {
503             accounttype    => { -not_in => \@not_fines }
504         },
505     )->total_outstanding;
506 }
507
508 =head3 lines
509
510 my $lines = $self->lines;
511
512 Return all credits and debits for the user, outstanding or otherwise
513
514 =cut
515
516 sub lines {
517     my ($self) = @_;
518
519     return Koha::Account::Lines->search(
520         {
521             borrowernumber => $self->{patron_id},
522         }
523     );
524 }
525
526 =head3 reconcile_balance
527
528 $account->reconcile_balance();
529
530 Find outstanding credits and use them to pay outstanding debits.
531 Currently, this implicitly uses the 'First In First Out' rule for
532 applying credits against debits.
533
534 =cut
535
536 sub reconcile_balance {
537     my ($self) = @_;
538
539     my $outstanding_debits  = $self->outstanding_debits;
540     my $outstanding_credits = $self->outstanding_credits;
541
542     while (     $outstanding_debits->total_outstanding > 0
543             and my $credit = $outstanding_credits->next )
544     {
545         # there's both outstanding debits and credits
546         $credit->apply( { debits => $outstanding_debits } );    # applying credit, no special offset
547
548         $outstanding_debits = $self->outstanding_debits;
549
550     }
551
552     return $self;
553 }
554
555 1;
556
557 =head2 Name mappings
558
559 =head3 $offset_type
560
561 =cut
562
563 our $offset_type = {
564     'credit'           => 'Manual Credit',
565     'forgiven'         => 'Writeoff',
566     'lost_item_return' => 'Lost Item',
567     'payment'          => 'Payment',
568     'writeoff'         => 'Writeoff'
569 };
570
571 =head3 $account_type
572
573 =cut
574
575 our $account_type = {
576     'credit'           => 'C',
577     'forgiven'         => 'FOR',
578     'lost_item_return' => 'CR',
579     'payment'          => 'Pay',
580     'writeoff'         => 'W'
581 };
582
583 =head1 AUTHOR
584
585 Kyle M Hall <kyle.m.hall@gmail.com>
586
587 =cut