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