Bug 19191: Remove need for bug 19966
[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             tables => {
274                 borrowers       => $self->{patron_id},
275                 branches        => $self->{library_id},
276             },
277             substitute => {
278                 credit => $payment,
279                 offsets => scalar Koha::Account::Offsets->search( { id => { -in => [ map { $_->id } @account_offsets ] } } ),
280             },
281           )
282       )
283     {
284         C4::Letters::EnqueueLetter(
285             {
286                 letter                 => $letter,
287                 borrowernumber         => $self->{patron_id},
288                 message_transport_type => 'email',
289             }
290         ) or warn "can't enqueue letter $letter";
291     }
292
293     return $payment->id;
294 }
295
296 =head3 add_credit
297
298 This method allows adding credits to a patron's account
299
300 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
301     {
302         amount       => $amount,
303         description  => $description,
304         note         => $note,
305         user_id      => $user_id,
306         library_id   => $library_id,
307         sip          => $sip,
308         payment_type => $payment_type,
309         type         => $credit_type,
310         item_id      => $item_id
311     }
312 );
313
314 $credit_type can be any of:
315   - 'credit'
316   - 'payment'
317   - 'forgiven'
318   - 'lost_item_return'
319   - 'writeoff'
320
321 =cut
322
323 sub add_credit {
324
325     my ( $self, $params ) = @_;
326
327     # amount is passed as a positive value, but we store credit as negative values
328     my $amount       = $params->{amount} * -1;
329     my $description  = $params->{description} // q{};
330     my $note         = $params->{note} // q{};
331     my $user_id      = $params->{user_id};
332     my $library_id   = $params->{library_id};
333     my $sip          = $params->{sip};
334     my $payment_type = $params->{payment_type};
335     my $type         = $params->{type} || 'payment';
336     my $item_id      = $params->{item_id};
337
338     my $schema = Koha::Database->new->schema;
339
340     my $account_type = $Koha::Account::account_type->{$type};
341     $account_type .= $sip
342         if defined $sip &&
343            $type eq 'payment';
344
345     my $line;
346
347     $schema->txn_do(
348         sub {
349             # We should remove accountno, it is no longer needed
350             my $last = Koha::Account::Lines->search( { borrowernumber => $self->{patron_id} },
351                 { order_by => 'accountno' } )->next();
352             my $accountno = $last ? $last->accountno + 1 : 1;
353
354             # Insert the account line
355             $line = Koha::Account::Line->new(
356                 {   borrowernumber    => $self->{patron_id},
357                     date              => \'NOW()',
358                     amount            => $amount,
359                     description       => $description,
360                     accounttype       => $account_type,
361                     amountoutstanding => $amount,
362                     payment_type      => $payment_type,
363                     note              => $note,
364                     manager_id        => $user_id,
365                     itemnumber        => $item_id
366                 }
367             )->store();
368
369             # Record the account offset
370             my $account_offset = Koha::Account::Offset->new(
371                 {   credit_id => $line->id,
372                     type      => $Koha::Account::offset_type->{$type},
373                     amount    => $amount
374                 }
375             )->store();
376
377             UpdateStats(
378                 {   branch         => $library_id,
379                     type           => $type,
380                     amount         => $amount,
381                     borrowernumber => $self->{patron_id},
382                     accountno      => $accountno,
383                 }
384             ) if grep { $type eq $_ } ('payment', 'writeoff') ;
385
386             if ( C4::Context->preference("FinesLog") ) {
387                 logaction(
388                     "FINES", 'CREATE',
389                     $self->{patron_id},
390                     Dumper(
391                         {   action            => "create_$type",
392                             borrowernumber    => $self->{patron_id},
393                             accountno         => $accountno,
394                             amount            => $amount,
395                             description       => $description,
396                             amountoutstanding => $amount,
397                             accounttype       => $account_type,
398                             note              => $note,
399                             itemnumber        => $item_id,
400                             manager_id        => $user_id,
401                         }
402                     )
403                 );
404             }
405         }
406     );
407
408     return $line;
409 }
410
411 =head3 balance
412
413 my $balance = $self->balance
414
415 Return the balance (sum of amountoutstanding columns)
416
417 =cut
418
419 sub balance {
420     my ($self) = @_;
421     my $fines = Koha::Account::Lines->search(
422         {
423             borrowernumber => $self->{patron_id},
424         },
425         {
426             select => [ { sum => 'amountoutstanding' } ],
427             as => ['total_amountoutstanding'],
428         }
429     );
430
431     return ( $fines->count )
432       ? $fines->next->get_column('total_amountoutstanding') + 0
433       : 0;
434 }
435
436 =head3 outstanding_debits
437
438 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
439
440 =cut
441
442 sub outstanding_debits {
443     my ($self) = @_;
444
445     my $lines = Koha::Account::Lines->search(
446         {
447             borrowernumber    => $self->{patron_id},
448             amountoutstanding => { '>' => 0 }
449         }
450     );
451
452     return $lines;
453 }
454
455 =head3 outstanding_credits
456
457 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
458
459 =cut
460
461 sub outstanding_credits {
462     my ($self) = @_;
463
464     my $lines = Koha::Account::Lines->search(
465         {
466             borrowernumber    => $self->{patron_id},
467             amountoutstanding => { '<' => 0 }
468         }
469     );
470
471     return $lines;
472 }
473
474 =head3 non_issues_charges
475
476 my $non_issues_charges = $self->non_issues_charges
477
478 Calculates amount immediately owing by the patron - non-issue charges.
479
480 Charges exempt from non-issue are:
481 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
482 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
483 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
484
485 =cut
486
487 sub non_issues_charges {
488     my ($self) = @_;
489
490     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
491     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
492
493     my @not_fines;
494     push @not_fines, 'Res'
495       unless C4::Context->preference('HoldsInNoissuesCharge');
496     push @not_fines, 'Rent'
497       unless C4::Context->preference('RentalsInNoissuesCharge');
498     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
499         my $dbh = C4::Context->dbh;
500         push @not_fines,
501           @{
502             $dbh->selectcol_arrayref(q|
503                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
504             |)
505           };
506     }
507     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
508
509     my $non_issues_charges = Koha::Account::Lines->search(
510         {
511             borrowernumber => $self->{patron_id},
512             accounttype    => { -not_in => \@not_fines }
513         },
514         {
515             select => [ { sum => 'amountoutstanding' } ],
516             as     => ['non_issues_charges'],
517         }
518     );
519     return $non_issues_charges->count
520       ? $non_issues_charges->next->get_column('non_issues_charges') + 0
521       : 0;
522 }
523
524 1;
525
526 =head2 Name mappings
527
528 =head3 $offset_type
529
530 =cut
531
532 our $offset_type = {
533     'credit'           => 'Manual Credit',
534     'forgiven'         => 'Writeoff',
535     'lost_item_return' => 'Lost Item Return',
536     'payment'          => 'Payment',
537     'writeoff'         => 'Writeoff'
538 };
539
540 =head3 $account_type
541
542 =cut
543
544 our $account_type = {
545     'credit'           => 'C',
546     'forgiven'         => 'FOR',
547     'lost_item_return' => 'CR',
548     'payment'          => 'Pay',
549     'writeoff'         => 'W'
550 };
551
552 =head1 AUTHOR
553
554 Kyle M Hall <kyle.m.hall@gmail.com>
555
556 =cut