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