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