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