Bug 14385: Extend OpacHiddenItems to allow specifying exempt borrower categories
[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 = Koha::Account::Lines->search(
89         {
90             borrowernumber => $self->{patron_id}
91         },
92         {
93             order_by => 'accountno'
94         }
95     )->next();
96     my $accountno = $last ? $last->accountno + 1 : 1;
97
98     my $manager_id = $userenv ? $userenv->{number} : 0;
99
100     my @fines_paid; # List of account lines paid on with this payment
101
102     my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
103     $balance_remaining ||= 0;
104
105     my @account_offsets;
106
107     # We were passed a specific line to pay
108     foreach my $fine ( @$lines ) {
109         my $amount_to_pay =
110             $fine->amountoutstanding > $balance_remaining
111           ? $balance_remaining
112           : $fine->amountoutstanding;
113
114         my $old_amountoutstanding = $fine->amountoutstanding;
115         my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
116         $fine->amountoutstanding($new_amountoutstanding)->store();
117         $balance_remaining = $balance_remaining - $amount_to_pay;
118
119         if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
120         {
121             C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
122         }
123
124         my $account_offset = Koha::Account::Offset->new(
125             {
126                 debit_id => $fine->id,
127                 type     => $offset_type,
128                 amount   => $amount_to_pay * -1,
129             }
130         );
131         push( @account_offsets, $account_offset );
132
133         if ( C4::Context->preference("FinesLog") ) {
134             logaction(
135                 "FINES", 'MODIFY',
136                 $self->{patron_id},
137                 Dumper(
138                     {
139                         action                => 'fee_payment',
140                         borrowernumber        => $fine->borrowernumber,
141                         old_amountoutstanding => $old_amountoutstanding,
142                         new_amountoutstanding => 0,
143                         amount_paid           => $old_amountoutstanding,
144                         accountlines_id       => $fine->id,
145                         accountno             => $fine->accountno,
146                         manager_id            => $manager_id,
147                         note                  => $note,
148                     }
149                 )
150             );
151             push( @fines_paid, $fine->id );
152         }
153     }
154
155     # Were not passed a specific line to pay, or the payment was for more
156     # than the what was owed on the given line. In that case pay down other
157     # lines with remaining balance.
158     my @outstanding_fines;
159     @outstanding_fines = Koha::Account::Lines->search(
160         {
161             borrowernumber    => $self->{patron_id},
162             amountoutstanding => { '>' => 0 },
163         }
164     ) if $balance_remaining > 0;
165
166     foreach my $fine (@outstanding_fines) {
167         my $amount_to_pay =
168             $fine->amountoutstanding > $balance_remaining
169           ? $balance_remaining
170           : $fine->amountoutstanding;
171
172         my $old_amountoutstanding = $fine->amountoutstanding;
173         $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
174         $fine->store();
175
176         my $account_offset = Koha::Account::Offset->new(
177             {
178                 debit_id => $fine->id,
179                 type     => $offset_type,
180                 amount   => $amount_to_pay * -1,
181             }
182         );
183         push( @account_offsets, $account_offset );
184
185         if ( C4::Context->preference("FinesLog") ) {
186             logaction(
187                 "FINES", 'MODIFY',
188                 $self->{patron_id},
189                 Dumper(
190                     {
191                         action                => "fee_$type",
192                         borrowernumber        => $fine->borrowernumber,
193                         old_amountoutstanding => $old_amountoutstanding,
194                         new_amountoutstanding => $fine->amountoutstanding,
195                         amount_paid           => $amount_to_pay,
196                         accountlines_id       => $fine->id,
197                         accountno             => $fine->accountno,
198                         manager_id            => $manager_id,
199                         note                  => $note,
200                     }
201                 )
202             );
203             push( @fines_paid, $fine->id );
204         }
205
206         $balance_remaining = $balance_remaining - $amount_to_pay;
207         last unless $balance_remaining > 0;
208     }
209
210     $account_type ||=
211         $type eq 'writeoff' ? 'W'
212       : defined($sip)       ? "Pay$sip"
213       :                       'Pay';
214
215     $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
216
217     my $payment = Koha::Account::Line->new(
218         {
219             borrowernumber    => $self->{patron_id},
220             accountno         => $accountno,
221             date              => dt_from_string(),
222             amount            => 0 - $amount,
223             description       => $description,
224             accounttype       => $account_type,
225             payment_type      => $payment_type,
226             amountoutstanding => 0 - $balance_remaining,
227             manager_id        => $manager_id,
228             note              => $note,
229         }
230     )->store();
231
232     foreach my $o ( @account_offsets ) {
233         $o->credit_id( $payment->id() );
234         $o->store();
235     }
236
237     $library_id ||= $userenv ? $userenv->{'branch'} : undef;
238
239     UpdateStats(
240         {
241             branch         => $library_id,
242             type           => $type,
243             amount         => $amount,
244             borrowernumber => $self->{patron_id},
245             accountno      => $accountno,
246         }
247     );
248
249     if ( C4::Context->preference("FinesLog") ) {
250         logaction(
251             "FINES", 'CREATE',
252             $self->{patron_id},
253             Dumper(
254                 {
255                     action            => "create_$type",
256                     borrowernumber    => $self->{patron_id},
257                     accountno         => $accountno,
258                     amount            => 0 - $amount,
259                     amountoutstanding => 0 - $balance_remaining,
260                     accounttype       => $account_type,
261                     accountlines_paid => \@fines_paid,
262                     manager_id        => $manager_id,
263                 }
264             )
265         );
266     }
267
268     if ( C4::Context->preference('UseEmailReceipts') ) {
269         if (
270             my $letter = C4::Letters::GetPreparedLetter(
271                 module                 => 'circulation',
272                 letter_code            => uc("ACCOUNT_$type"),
273                 message_transport_type => 'email',
274                 lang    => $patron->lang,
275                 tables => {
276                     borrowers       => $self->{patron_id},
277                     branches        => $self->{library_id},
278                 },
279                 substitute => {
280                     credit => $payment,
281                     offsets => \@account_offsets,
282                 },
283               )
284           )
285         {
286             C4::Letters::EnqueueLetter(
287                 {
288                     letter                 => $letter,
289                     borrowernumber         => $self->{patron_id},
290                     message_transport_type => 'email',
291                 }
292             ) or warn "can't enqueue letter $letter";
293         }
294     }
295
296     return $payment->id;
297 }
298
299 =head3 add_credit
300
301 This method allows adding credits to a patron's account
302
303 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
304     {
305         amount       => $amount,
306         description  => $description,
307         note         => $note,
308         user_id      => $user_id,
309         library_id   => $library_id,
310         sip          => $sip,
311         payment_type => $payment_type,
312         type         => $credit_type,
313         item_id      => $item_id
314     }
315 );
316
317 $credit_type can be any of:
318   - 'credit'
319   - 'payment'
320   - 'forgiven'
321   - 'lost_item_return'
322   - 'writeoff'
323
324 =cut
325
326 sub add_credit {
327
328     my ( $self, $params ) = @_;
329
330     # amount is passed as a positive value, but we store credit as negative values
331     my $amount       = $params->{amount} * -1;
332     my $description  = $params->{description} // q{};
333     my $note         = $params->{note} // q{};
334     my $user_id      = $params->{user_id};
335     my $library_id   = $params->{library_id};
336     my $sip          = $params->{sip};
337     my $payment_type = $params->{payment_type};
338     my $type         = $params->{type} || 'payment';
339     my $item_id      = $params->{item_id};
340
341     my $schema = Koha::Database->new->schema;
342
343     my $account_type = $Koha::Account::account_type->{$type};
344     $account_type .= $sip
345         if defined $sip &&
346            $type eq 'payment';
347
348     my $line;
349
350     $schema->txn_do(
351         sub {
352             # We should remove accountno, it is no longer needed
353             my $last = Koha::Account::Lines->search( { borrowernumber => $self->{patron_id} },
354                 { order_by => 'accountno' } )->next();
355             my $accountno = $last ? $last->accountno + 1 : 1;
356
357             # Insert the account line
358             $line = Koha::Account::Line->new(
359                 {   borrowernumber    => $self->{patron_id},
360                     date              => \'NOW()',
361                     amount            => $amount,
362                     description       => $description,
363                     accounttype       => $account_type,
364                     amountoutstanding => $amount,
365                     payment_type      => $payment_type,
366                     note              => $note,
367                     manager_id        => $user_id,
368                     itemnumber        => $item_id
369                 }
370             )->store();
371
372             # Record the account offset
373             my $account_offset = Koha::Account::Offset->new(
374                 {   credit_id => $line->id,
375                     type      => $Koha::Account::offset_type->{$type},
376                     amount    => $amount
377                 }
378             )->store();
379
380             UpdateStats(
381                 {   branch         => $library_id,
382                     type           => $type,
383                     amount         => $amount,
384                     borrowernumber => $self->{patron_id},
385                     accountno      => $accountno,
386                 }
387             ) if grep { $type eq $_ } ('payment', 'writeoff') ;
388
389             if ( C4::Context->preference("FinesLog") ) {
390                 logaction(
391                     "FINES", 'CREATE',
392                     $self->{patron_id},
393                     Dumper(
394                         {   action            => "create_$type",
395                             borrowernumber    => $self->{patron_id},
396                             accountno         => $accountno,
397                             amount            => $amount,
398                             description       => $description,
399                             amountoutstanding => $amount,
400                             accounttype       => $account_type,
401                             note              => $note,
402                             itemnumber        => $item_id,
403                             manager_id        => $user_id,
404                         }
405                     )
406                 );
407             }
408         }
409     );
410
411     return $line;
412 }
413
414 =head3 balance
415
416 my $balance = $self->balance
417
418 Return the balance (sum of amountoutstanding columns)
419
420 =cut
421
422 sub balance {
423     my ($self) = @_;
424     my $fines = Koha::Account::Lines->search(
425         {
426             borrowernumber => $self->{patron_id},
427         },
428         {
429             select => [ { sum => 'amountoutstanding' } ],
430             as => ['total_amountoutstanding'],
431         }
432     );
433
434     return ( $fines->count )
435       ? $fines->next->get_column('total_amountoutstanding') + 0
436       : 0;
437 }
438
439 =head3 outstanding_debits
440
441 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
442
443 =cut
444
445 sub outstanding_debits {
446     my ($self) = @_;
447
448     my $lines = Koha::Account::Lines->search(
449         {
450             borrowernumber    => $self->{patron_id},
451             amountoutstanding => { '>' => 0 }
452         }
453     );
454
455     return $lines;
456 }
457
458 =head3 outstanding_credits
459
460 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
461
462 =cut
463
464 sub outstanding_credits {
465     my ($self) = @_;
466
467     my $lines = Koha::Account::Lines->search(
468         {
469             borrowernumber    => $self->{patron_id},
470             amountoutstanding => { '<' => 0 }
471         }
472     );
473
474     return $lines;
475 }
476
477 =head3 non_issues_charges
478
479 my $non_issues_charges = $self->non_issues_charges
480
481 Calculates amount immediately owing by the patron - non-issue charges.
482
483 Charges exempt from non-issue are:
484 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
485 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
486 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
487
488 =cut
489
490 sub non_issues_charges {
491     my ($self) = @_;
492
493     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
494     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
495
496     my @not_fines;
497     push @not_fines, 'Res'
498       unless C4::Context->preference('HoldsInNoissuesCharge');
499     push @not_fines, 'Rent'
500       unless C4::Context->preference('RentalsInNoissuesCharge');
501     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
502         my $dbh = C4::Context->dbh;
503         push @not_fines,
504           @{
505             $dbh->selectcol_arrayref(q|
506                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
507             |)
508           };
509     }
510     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
511
512     my $non_issues_charges = Koha::Account::Lines->search(
513         {
514             borrowernumber => $self->{patron_id},
515             accounttype    => { -not_in => \@not_fines }
516         },
517         {
518             select => [ { sum => 'amountoutstanding' } ],
519             as     => ['non_issues_charges'],
520         }
521     );
522     return $non_issues_charges->count
523       ? $non_issues_charges->next->get_column('non_issues_charges') + 0
524       : 0;
525 }
526
527 =head3 lines
528
529 my $lines = $self->lines;
530
531 Return all credits and debits for the user, outstanding or otherwise
532
533 =cut
534
535 sub lines {
536     my ($self) = @_;
537
538     return Koha::Account::Lines->search(
539         {
540             borrowernumber => $self->{patron_id},
541         }
542     );
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