Bug 13895: Add API routes for checkouts retrieval and renewal
[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 use Koha::Exceptions::Account;
36
37 =head1 NAME
38
39 Koha::Accounts - Module for managing payments and fees for patrons
40
41 =cut
42
43 sub new {
44     my ( $class, $params ) = @_;
45
46     Carp::croak("No patron id passed in!") unless $params->{patron_id};
47
48     return bless( $params, $class );
49 }
50
51 =head2 pay
52
53 This method allows payments to be made against fees/fines
54
55 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
56     {
57         amount      => $amount,
58         sip         => $sipmode,
59         note        => $note,
60         description => $description,
61         library_id  => $branchcode,
62         lines        => $lines, # Arrayref of Koha::Account::Line objects to pay
63         account_type => $type,  # accounttype code
64         offset_type => $offset_type,    # offset type code
65     }
66 );
67
68 =cut
69
70 sub pay {
71     my ( $self, $params ) = @_;
72
73     my $amount       = $params->{amount};
74     my $sip          = $params->{sip};
75     my $description  = $params->{description};
76     my $note         = $params->{note} || q{};
77     my $library_id   = $params->{library_id};
78     my $lines        = $params->{lines};
79     my $type         = $params->{type} || 'payment';
80     my $payment_type = $params->{payment_type} || undef;
81     my $account_type = $params->{account_type};
82     my $offset_type  = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
83
84     my $userenv = C4::Context->userenv;
85
86     my $patron = Koha::Patrons->find( $self->{patron_id} );
87
88     my $manager_id = $userenv ? $userenv->{number} : 0;
89
90     my @fines_paid; # List of account lines paid on with this payment
91
92     my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
93     $balance_remaining ||= 0;
94
95     my @account_offsets;
96
97     # We were passed a specific line to pay
98     foreach my $fine ( @$lines ) {
99         my $amount_to_pay =
100             $fine->amountoutstanding > $balance_remaining
101           ? $balance_remaining
102           : $fine->amountoutstanding;
103
104         my $old_amountoutstanding = $fine->amountoutstanding;
105         my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
106         $fine->amountoutstanding($new_amountoutstanding)->store();
107         $balance_remaining = $balance_remaining - $amount_to_pay;
108
109         if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
110         {
111             C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
112         }
113
114         my $account_offset = Koha::Account::Offset->new(
115             {
116                 debit_id => $fine->id,
117                 type     => $offset_type,
118                 amount   => $amount_to_pay * -1,
119             }
120         );
121         push( @account_offsets, $account_offset );
122
123         if ( C4::Context->preference("FinesLog") ) {
124             logaction(
125                 "FINES", 'MODIFY',
126                 $self->{patron_id},
127                 Dumper(
128                     {
129                         action                => 'fee_payment',
130                         borrowernumber        => $fine->borrowernumber,
131                         old_amountoutstanding => $old_amountoutstanding,
132                         new_amountoutstanding => 0,
133                         amount_paid           => $old_amountoutstanding,
134                         accountlines_id       => $fine->id,
135                         manager_id            => $manager_id,
136                         note                  => $note,
137                     }
138                 )
139             );
140             push( @fines_paid, $fine->id );
141         }
142     }
143
144     # Were not passed a specific line to pay, or the payment was for more
145     # than the what was owed on the given line. In that case pay down other
146     # lines with remaining balance.
147     my @outstanding_fines;
148     @outstanding_fines = $self->lines->search(
149         {
150             amountoutstanding => { '>' => 0 },
151         }
152     ) if $balance_remaining > 0;
153
154     foreach my $fine (@outstanding_fines) {
155         my $amount_to_pay =
156             $fine->amountoutstanding > $balance_remaining
157           ? $balance_remaining
158           : $fine->amountoutstanding;
159
160         my $old_amountoutstanding = $fine->amountoutstanding;
161         $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
162         $fine->store();
163
164         my $account_offset = Koha::Account::Offset->new(
165             {
166                 debit_id => $fine->id,
167                 type     => $offset_type,
168                 amount   => $amount_to_pay * -1,
169             }
170         );
171         push( @account_offsets, $account_offset );
172
173         if ( C4::Context->preference("FinesLog") ) {
174             logaction(
175                 "FINES", 'MODIFY',
176                 $self->{patron_id},
177                 Dumper(
178                     {
179                         action                => "fee_$type",
180                         borrowernumber        => $fine->borrowernumber,
181                         old_amountoutstanding => $old_amountoutstanding,
182                         new_amountoutstanding => $fine->amountoutstanding,
183                         amount_paid           => $amount_to_pay,
184                         accountlines_id       => $fine->id,
185                         manager_id            => $manager_id,
186                         note                  => $note,
187                     }
188                 )
189             );
190             push( @fines_paid, $fine->id );
191         }
192
193         $balance_remaining = $balance_remaining - $amount_to_pay;
194         last unless $balance_remaining > 0;
195     }
196
197     $account_type ||=
198         $type eq 'writeoff' ? 'W'
199       : defined($sip)       ? "Pay$sip"
200       :                       'Pay';
201
202     $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
203
204     my $payment = Koha::Account::Line->new(
205         {
206             borrowernumber    => $self->{patron_id},
207             date              => dt_from_string(),
208             amount            => 0 - $amount,
209             description       => $description,
210             accounttype       => $account_type,
211             payment_type      => $payment_type,
212             amountoutstanding => 0 - $balance_remaining,
213             manager_id        => $manager_id,
214             branchcode        => $library_id,
215             note              => $note,
216         }
217     )->store();
218
219     foreach my $o ( @account_offsets ) {
220         $o->credit_id( $payment->id() );
221         $o->store();
222     }
223
224     UpdateStats(
225         {
226             branch         => $library_id,
227             type           => $type,
228             amount         => $amount,
229             borrowernumber => $self->{patron_id},
230         }
231     );
232
233     if ( C4::Context->preference("FinesLog") ) {
234         logaction(
235             "FINES", 'CREATE',
236             $self->{patron_id},
237             Dumper(
238                 {
239                     action            => "create_$type",
240                     borrowernumber    => $self->{patron_id},
241                     amount            => 0 - $amount,
242                     amountoutstanding => 0 - $balance_remaining,
243                     accounttype       => $account_type,
244                     accountlines_paid => \@fines_paid,
245                     manager_id        => $manager_id,
246                 }
247             )
248         );
249     }
250
251     if ( C4::Context->preference('UseEmailReceipts') ) {
252         if (
253             my $letter = C4::Letters::GetPreparedLetter(
254                 module                 => 'circulation',
255                 letter_code            => uc("ACCOUNT_$type"),
256                 message_transport_type => 'email',
257                 lang    => $patron->lang,
258                 tables => {
259                     borrowers       => $self->{patron_id},
260                     branches        => $self->{library_id},
261                 },
262                 substitute => {
263                     credit => $payment,
264                     offsets => \@account_offsets,
265                 },
266               )
267           )
268         {
269             C4::Letters::EnqueueLetter(
270                 {
271                     letter                 => $letter,
272                     borrowernumber         => $self->{patron_id},
273                     message_transport_type => 'email',
274                 }
275             ) or warn "can't enqueue letter $letter";
276         }
277     }
278
279     return $payment->id;
280 }
281
282 =head3 add_credit
283
284 This method allows adding credits to a patron's account
285
286 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
287     {
288         amount       => $amount,
289         description  => $description,
290         note         => $note,
291         user_id      => $user_id,
292         library_id   => $library_id,
293         sip          => $sip,
294         payment_type => $payment_type,
295         type         => $credit_type,
296         item_id      => $item_id
297     }
298 );
299
300 $credit_type can be any of:
301   - 'credit'
302   - 'payment'
303   - 'forgiven'
304   - 'lost_item_return'
305   - 'writeoff'
306
307 =cut
308
309 sub add_credit {
310
311     my ( $self, $params ) = @_;
312
313     # amount is passed as a positive value, but we store credit as negative values
314     my $amount       = $params->{amount} * -1;
315     my $description  = $params->{description} // q{};
316     my $note         = $params->{note} // q{};
317     my $user_id      = $params->{user_id};
318     my $library_id   = $params->{library_id};
319     my $sip          = $params->{sip};
320     my $payment_type = $params->{payment_type};
321     my $type         = $params->{type} || 'payment';
322     my $item_id      = $params->{item_id};
323
324     my $schema = Koha::Database->new->schema;
325
326     my $account_type = $Koha::Account::account_type_credit->{$type};
327     $account_type .= $sip
328         if defined $sip &&
329            $type eq 'payment';
330
331     my $line;
332
333     $schema->txn_do(
334         sub {
335
336             # Insert the account line
337             $line = Koha::Account::Line->new(
338                 {   borrowernumber    => $self->{patron_id},
339                     date              => \'NOW()',
340                     amount            => $amount,
341                     description       => $description,
342                     accounttype       => $account_type,
343                     amountoutstanding => $amount,
344                     payment_type      => $payment_type,
345                     note              => $note,
346                     manager_id        => $user_id,
347                     branchcode        => $library_id,
348                     itemnumber        => $item_id,
349                 }
350             )->store();
351
352             # Record the account offset
353             my $account_offset = Koha::Account::Offset->new(
354                 {   credit_id => $line->id,
355                     type      => $Koha::Account::offset_type->{$type},
356                     amount    => $amount
357                 }
358             )->store();
359
360             UpdateStats(
361                 {   branch         => $library_id,
362                     type           => $type,
363                     amount         => $amount,
364                     borrowernumber => $self->{patron_id},
365                 }
366             ) if grep { $type eq $_ } ('payment', 'writeoff') ;
367
368             if ( C4::Context->preference("FinesLog") ) {
369                 logaction(
370                     "FINES", 'CREATE',
371                     $self->{patron_id},
372                     Dumper(
373                         {   action            => "create_$type",
374                             borrowernumber    => $self->{patron_id},
375                             amount            => $amount,
376                             description       => $description,
377                             amountoutstanding => $amount,
378                             accounttype       => $account_type,
379                             note              => $note,
380                             itemnumber        => $item_id,
381                             manager_id        => $user_id,
382                             branchcode        => $library_id,
383                         }
384                     )
385                 );
386             }
387         }
388     );
389
390     return $line;
391 }
392
393 =head3 add_debit
394
395 This method allows adding debits to a patron's account
396
397 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
398     {
399         amount       => $amount,
400         description  => $description,
401         note         => $note,
402         user_id      => $user_id,
403         library_id   => $library_id,
404         type         => $debit_type,
405         item_id      => $item_id,
406         issue_id     => $issue_id
407     }
408 );
409
410 $debit_type can be any of:
411   - fine
412   - lost_item
413   - new_card
414   - account
415   - sundry
416   - processing
417   - rent
418   - reserve
419   - overdue
420   - manual
421
422 =cut
423
424 sub add_debit {
425
426     my ( $self, $params ) = @_;
427
428     # amount should always be a positive value
429     my $amount       = $params->{amount};
430
431     unless ( $amount > 0 ) {
432         Koha::Exceptions::Account::AmountNotPositive->throw(
433             error => 'Debit amount passed is not positive'
434         );
435     }
436
437     my $description  = $params->{description} // q{};
438     my $note         = $params->{note} // q{};
439     my $user_id      = $params->{user_id};
440     my $library_id   = $params->{library_id};
441     my $type         = $params->{type};
442     my $item_id      = $params->{item_id};
443     my $issue_id     = $params->{issue_id};
444
445     my $schema = Koha::Database->new->schema;
446
447     unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
448         Koha::Exceptions::Account::UnrecognisedType->throw(
449             error => 'Type of debit not recognised'
450         );
451     }
452
453     my $account_type = $Koha::Account::account_type_debit->{$type};
454
455     my $line;
456
457     $schema->txn_do(
458         sub {
459
460             # Insert the account line
461             $line = Koha::Account::Line->new(
462                 {   borrowernumber    => $self->{patron_id},
463                     date              => \'NOW()',
464                     amount            => $amount,
465                     description       => $description,
466                     accounttype       => $account_type,
467                     amountoutstanding => $amount,
468                     payment_type      => undef,
469                     note              => $note,
470                     manager_id        => $user_id,
471                     itemnumber        => $item_id,
472                     issue_id          => $issue_id,
473                     branchcode        => $library_id,
474                 }
475             )->store();
476
477             # Record the account offset
478             my $account_offset = Koha::Account::Offset->new(
479                 {   debit_id => $line->id,
480                     type      => $Koha::Account::offset_type->{$type},
481                     amount    => $amount
482                 }
483             )->store();
484
485             if ( C4::Context->preference("FinesLog") ) {
486                 logaction(
487                     "FINES", 'CREATE',
488                     $self->{patron_id},
489                     Dumper(
490                         {   action            => "create_$type",
491                             borrowernumber    => $self->{patron_id},
492                             amount            => $amount,
493                             description       => $description,
494                             amountoutstanding => $amount,
495                             accounttype       => $account_type,
496                             note              => $note,
497                             itemnumber        => $item_id,
498                             manager_id        => $user_id,
499                         }
500                     )
501                 );
502             }
503         }
504     );
505
506     return $line;
507 }
508
509 =head3 balance
510
511 my $balance = $self->balance
512
513 Return the balance (sum of amountoutstanding columns)
514
515 =cut
516
517 sub balance {
518     my ($self) = @_;
519     return $self->lines->total_outstanding;
520 }
521
522 =head3 outstanding_debits
523
524 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
525
526 It returns the debit lines with outstanding amounts for the patron.
527
528 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
529 return a list of Koha::Account::Line objects.
530
531 =cut
532
533 sub outstanding_debits {
534     my ($self) = @_;
535
536     return $self->lines->search(
537         {
538             amount            => { '>' => 0 },
539             amountoutstanding => { '>' => 0 }
540         }
541     );
542 }
543
544 =head3 outstanding_credits
545
546 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
547
548 It returns the credit lines with outstanding amounts for the patron.
549
550 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
551 return a list of Koha::Account::Line objects.
552
553 =cut
554
555 sub outstanding_credits {
556     my ($self) = @_;
557
558     return $self->lines->search(
559         {
560             amount            => { '<' => 0 },
561             amountoutstanding => { '<' => 0 }
562         }
563     );
564 }
565
566 =head3 non_issues_charges
567
568 my $non_issues_charges = $self->non_issues_charges
569
570 Calculates amount immediately owing by the patron - non-issue charges.
571
572 Charges exempt from non-issue are:
573 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
574 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
575 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
576
577 =cut
578
579 sub non_issues_charges {
580     my ($self) = @_;
581
582     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
583     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
584
585     my @not_fines;
586     push @not_fines, 'Res'
587       unless C4::Context->preference('HoldsInNoissuesCharge');
588     push @not_fines, 'Rent'
589       unless C4::Context->preference('RentalsInNoissuesCharge');
590     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
591         my $dbh = C4::Context->dbh;
592         push @not_fines,
593           @{
594             $dbh->selectcol_arrayref(q|
595                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
596             |)
597           };
598     }
599     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
600
601     return $self->lines->search(
602         {
603             accounttype    => { -not_in => \@not_fines }
604         },
605     )->total_outstanding;
606 }
607
608 =head3 lines
609
610 my $lines = $self->lines;
611
612 Return all credits and debits for the user, outstanding or otherwise
613
614 =cut
615
616 sub lines {
617     my ($self) = @_;
618
619     return Koha::Account::Lines->search(
620         {
621             borrowernumber => $self->{patron_id},
622         }
623     );
624 }
625
626 =head3 reconcile_balance
627
628 $account->reconcile_balance();
629
630 Find outstanding credits and use them to pay outstanding debits.
631 Currently, this implicitly uses the 'First In First Out' rule for
632 applying credits against debits.
633
634 =cut
635
636 sub reconcile_balance {
637     my ($self) = @_;
638
639     my $outstanding_debits  = $self->outstanding_debits;
640     my $outstanding_credits = $self->outstanding_credits;
641
642     while (     $outstanding_debits->total_outstanding > 0
643             and my $credit = $outstanding_credits->next )
644     {
645         # there's both outstanding debits and credits
646         $credit->apply( { debits => $outstanding_debits } );    # applying credit, no special offset
647
648         $outstanding_debits = $self->outstanding_debits;
649
650     }
651
652     return $self;
653 }
654
655 1;
656
657 =head2 Name mappings
658
659 =head3 $offset_type
660
661 =cut
662
663 our $offset_type = {
664     'credit'           => 'Manual Credit',
665     'forgiven'         => 'Writeoff',
666     'lost_item_return' => 'Lost Item',
667     'payment'          => 'Payment',
668     'writeoff'         => 'Writeoff',
669     'account'          => 'Account Fee',
670     'reserve'          => 'Reserve Fee',
671     'processing'       => 'Processing Fee',
672     'lost_item'        => 'Lost Item',
673     'rent'             => 'Rental Fee',
674     'fine'             => 'Fine',
675     'manual_debit'     => 'Manual Debit',
676     'hold_expired'     => 'Hold Expired'
677 };
678
679 =head3 $account_type_credit
680
681 =cut
682
683 our $account_type_credit = {
684     'credit'           => 'C',
685     'forgiven'         => 'FOR',
686     'lost_item_return' => 'CR',
687     'payment'          => 'Pay',
688     'writeoff'         => 'W'
689 };
690
691 =head3 $account_type_debit
692
693 =cut
694
695 our $account_type_debit = {
696     'account'       => 'A',
697     'fine'          => 'FU',
698     'lost_item'     => 'L',
699     'new_card'      => 'N',
700     'sundry'        => 'M',
701     'processing'    => 'PF',
702     'rent'          => 'Rent',
703     'reserve'       => 'Res',
704     'overdue'       => 'O',
705     'manual_debit'  => 'M',
706     'hold_expired'  => 'HE'
707 };
708
709 =head1 AUTHORS
710
711 =encoding utf8
712
713 Kyle M Hall <kyle.m.hall@gmail.com>
714 Tomás Cohen Arazi <tomascohen@gmail.com>
715 Martin Renvoize <martin.renvoize@ptfs-europe.com>
716
717 =cut