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