67988f2b51e6454e0dc34a2cfeb3506a60e153bf
[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         credit_type => $type,  # credit_type_code 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 $credit_type   = $params->{credit_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     $credit_type ||=
219       $type eq 'writeoff'
220       ? 'W'
221       : 'PAYMENT';
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             credit_type_code  => $credit_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                     credit_type_code  => $credit_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_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 $credit_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                     credit_type_code  => $credit_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                             credit_type_code  => $credit_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     # check for mandatory params
469     my @mandatory = ( 'interface', 'type', 'amount' );
470     for my $param (@mandatory) {
471         unless ( defined( $params->{$param} ) ) {
472             Koha::Exceptions::MissingParameter->throw(
473                 error => "The $param parameter is mandatory" );
474         }
475     }
476
477     # amount should always be a positive value
478     my $amount = $params->{amount};
479     unless ( $amount > 0 ) {
480         Koha::Exceptions::Account::AmountNotPositive->throw(
481             error => 'Debit amount passed is not positive' );
482     }
483
484     my $description = $params->{description} // q{};
485     my $note        = $params->{note} // q{};
486     my $user_id     = $params->{user_id};
487     my $interface   = $params->{interface};
488     my $library_id  = $params->{library_id};
489     my $debit_type  = $params->{type};
490     my $item_id     = $params->{item_id};
491     my $issue_id    = $params->{issue_id};
492     my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
493
494     my $line;
495     my $schema = Koha::Database->new->schema;
496     try {
497         $schema->txn_do(
498             sub {
499
500                 # Insert the account line
501                 $line = Koha::Account::Line->new(
502                     {
503                         borrowernumber    => $self->{patron_id},
504                         date              => \'NOW()',
505                         amount            => $amount,
506                         description       => $description,
507                         debit_type_code   => $debit_type,
508                         amountoutstanding => $amount,
509                         payment_type      => undef,
510                         note              => $note,
511                         manager_id        => $user_id,
512                         interface         => $interface,
513                         itemnumber        => $item_id,
514                         issue_id          => $issue_id,
515                         branchcode        => $library_id,
516                         (
517                             $debit_type eq 'OVERDUE'
518                             ? ( status => 'UNRETURNED' )
519                             : ()
520                         ),
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     => $offset_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_$debit_type",
540                                 borrowernumber    => $self->{patron_id},
541                                 amount            => $amount,
542                                 description       => $description,
543                                 amountoutstanding => $amount,
544                                 debit_type_code   => $debit_type,
545                                 note              => $note,
546                                 itemnumber        => $item_id,
547                                 manager_id        => $user_id,
548                             }
549                         ),
550                         $interface
551                     );
552                 }
553             }
554         );
555     }
556     catch {
557         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
558             if ( $_->broken_fk eq 'debit_type_code' ) {
559                 Koha::Exceptions::Account::UnrecognisedType->throw(
560                     error => 'Type of debit not recognised' );
561             }
562             else {
563                 $_->rethrow;
564             }
565         }
566     };
567
568     return $line;
569 }
570
571 =head3 balance
572
573 my $balance = $self->balance
574
575 Return the balance (sum of amountoutstanding columns)
576
577 =cut
578
579 sub balance {
580     my ($self) = @_;
581     return $self->lines->total_outstanding;
582 }
583
584 =head3 outstanding_debits
585
586 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
587
588 It returns the debit lines with outstanding amounts for the patron.
589
590 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
591 return a list of Koha::Account::Line objects.
592
593 =cut
594
595 sub outstanding_debits {
596     my ($self) = @_;
597
598     return $self->lines->search(
599         {
600             amount            => { '>' => 0 },
601             amountoutstanding => { '>' => 0 }
602         }
603     );
604 }
605
606 =head3 outstanding_credits
607
608 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
609
610 It returns the credit lines with outstanding amounts for the patron.
611
612 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
613 return a list of Koha::Account::Line objects.
614
615 =cut
616
617 sub outstanding_credits {
618     my ($self) = @_;
619
620     return $self->lines->search(
621         {
622             amount            => { '<' => 0 },
623             amountoutstanding => { '<' => 0 }
624         }
625     );
626 }
627
628 =head3 non_issues_charges
629
630 my $non_issues_charges = $self->non_issues_charges
631
632 Calculates amount immediately owing by the patron - non-issue charges.
633
634 Charges exempt from non-issue are:
635 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
636 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
637 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
638
639 =cut
640
641 sub non_issues_charges {
642     my ($self) = @_;
643
644     #NOTE: With bug 23049 these preferences could be moved to being attached
645     #to individual debit types to give more flexability and specificity.
646     my @not_fines;
647     push @not_fines, 'RESERVE'
648       unless C4::Context->preference('HoldsInNoissuesCharge');
649     push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
650       unless C4::Context->preference('RentalsInNoissuesCharge');
651     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
652         my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
653         push @not_fines, @man_inv;
654     }
655
656     return $self->lines->search(
657         {
658             debit_type_code => { -not_in => \@not_fines }
659         },
660     )->total_outstanding;
661 }
662
663 =head3 lines
664
665 my $lines = $self->lines;
666
667 Return all credits and debits for the user, outstanding or otherwise
668
669 =cut
670
671 sub lines {
672     my ($self) = @_;
673
674     return Koha::Account::Lines->search(
675         {
676             borrowernumber => $self->{patron_id},
677         }
678     );
679 }
680
681 =head3 reconcile_balance
682
683 $account->reconcile_balance();
684
685 Find outstanding credits and use them to pay outstanding debits.
686 Currently, this implicitly uses the 'First In First Out' rule for
687 applying credits against debits.
688
689 =cut
690
691 sub reconcile_balance {
692     my ($self) = @_;
693
694     my $outstanding_debits  = $self->outstanding_debits;
695     my $outstanding_credits = $self->outstanding_credits;
696
697     while (     $outstanding_debits->total_outstanding > 0
698             and my $credit = $outstanding_credits->next )
699     {
700         # there's both outstanding debits and credits
701         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
702
703         $outstanding_debits = $self->outstanding_debits;
704
705     }
706
707     return $self;
708 }
709
710 1;
711
712 =head2 Name mappings
713
714 =head3 $offset_type
715
716 =cut
717
718 our $offset_type = {
719     'CREDIT'           => 'Manual Credit',
720     'FORGIVEN'         => 'Writeoff',
721     'LOST_RETURN'      => 'Lost Item',
722     'PAYMENT'          => 'Payment',
723     'writeoff'         => 'Writeoff',
724     'ACCOUNT'          => 'Account Fee',
725     'ACCOUNT_RENEW'    => 'Account Fee',
726     'RESERVE'          => 'Reserve Fee',
727     'PROCESSING'       => 'Processing Fee',
728     'LOST'             => 'Lost Item',
729     'RENT'             => 'Rental Fee',
730     'RENT_DAILY'       => 'Rental Fee',
731     'RENT_RENEW'       => 'Rental Fee',
732     'RENT_DAILY_RENEW' => 'Rental Fee',
733     'OVERDUE'          => 'OVERDUE',
734     'RESERVE_EXPIRED'  => 'Hold Expired'
735 };
736
737 =head3 $account_type_credit
738
739 =cut
740
741 our $account_type_credit = {
742     'CREDIT'           => 'CREDIT',
743     'FORGIVEN'         => 'FORGIVEN',
744     'LOST_RETURN'      => 'LOST_RETURN',
745     'PAYMENT'          => 'PAYMENT',
746     'writeoff'         => 'W'
747 };
748
749 =head1 AUTHORS
750
751 =encoding utf8
752
753 Kyle M Hall <kyle.m.hall@gmail.com>
754 Tomás Cohen Arazi <tomascohen@gmail.com>
755 Martin Renvoize <martin.renvoize@ptfs-europe.com>
756
757 =cut