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