Bug 22563: Update lost handling to use accountline.status
[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   - reserve
448   - manual
449
450 =cut
451
452 sub add_debit {
453
454     my ( $self, $params ) = @_;
455
456     # amount should always be a positive value
457     my $amount       = $params->{amount};
458
459     unless ( $amount > 0 ) {
460         Koha::Exceptions::Account::AmountNotPositive->throw(
461             error => 'Debit amount passed is not positive'
462         );
463     }
464
465     my $description  = $params->{description} // q{};
466     my $note         = $params->{note} // q{};
467     my $user_id      = $params->{user_id};
468     my $interface    = $params->{interface};
469     my $library_id   = $params->{library_id};
470     my $type         = $params->{type};
471     my $item_id      = $params->{item_id};
472     my $issue_id     = $params->{issue_id};
473
474     unless ( $interface ) {
475         Koha::Exceptions::MissingParameter->throw(
476             error => 'The interface parameter is mandatory'
477         );
478     }
479
480     my $schema = Koha::Database->new->schema;
481
482     unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
483         Koha::Exceptions::Account::UnrecognisedType->throw(
484             error => 'Type of debit not recognised'
485         );
486     }
487
488     my $account_type = $Koha::Account::account_type_debit->{$type};
489
490     my $line;
491
492     $schema->txn_do(
493         sub {
494
495             # Insert the account line
496             $line = Koha::Account::Line->new(
497                 {   borrowernumber    => $self->{patron_id},
498                     date              => \'NOW()',
499                     amount            => $amount,
500                     description       => $description,
501                     accounttype       => $account_type,
502                     amountoutstanding => $amount,
503                     payment_type      => undef,
504                     note              => $note,
505                     manager_id        => $user_id,
506                     interface         => $interface,
507                     itemnumber        => $item_id,
508                     issue_id          => $issue_id,
509                     branchcode        => $library_id,
510                     ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : ()),
511                 }
512             )->store();
513
514             # Record the account offset
515             my $account_offset = Koha::Account::Offset->new(
516                 {   debit_id => $line->id,
517                     type      => $Koha::Account::offset_type->{$type},
518                     amount    => $amount
519                 }
520             )->store();
521
522             if ( C4::Context->preference("FinesLog") ) {
523                 logaction(
524                     "FINES", 'CREATE',
525                     $self->{patron_id},
526                     Dumper(
527                         {   action            => "create_$type",
528                             borrowernumber    => $self->{patron_id},
529                             amount            => $amount,
530                             description       => $description,
531                             amountoutstanding => $amount,
532                             accounttype       => $account_type,
533                             note              => $note,
534                             itemnumber        => $item_id,
535                             manager_id        => $user_id,
536                         }
537                     ),
538                     $interface
539                 );
540             }
541         }
542     );
543
544     return $line;
545 }
546
547 =head3 balance
548
549 my $balance = $self->balance
550
551 Return the balance (sum of amountoutstanding columns)
552
553 =cut
554
555 sub balance {
556     my ($self) = @_;
557     return $self->lines->total_outstanding;
558 }
559
560 =head3 outstanding_debits
561
562 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
563
564 It returns the debit lines with outstanding amounts for the patron.
565
566 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
567 return a list of Koha::Account::Line objects.
568
569 =cut
570
571 sub outstanding_debits {
572     my ($self) = @_;
573
574     return $self->lines->search(
575         {
576             amount            => { '>' => 0 },
577             amountoutstanding => { '>' => 0 }
578         }
579     );
580 }
581
582 =head3 outstanding_credits
583
584 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
585
586 It returns the credit lines with outstanding amounts for the patron.
587
588 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
589 return a list of Koha::Account::Line objects.
590
591 =cut
592
593 sub outstanding_credits {
594     my ($self) = @_;
595
596     return $self->lines->search(
597         {
598             amount            => { '<' => 0 },
599             amountoutstanding => { '<' => 0 }
600         }
601     );
602 }
603
604 =head3 non_issues_charges
605
606 my $non_issues_charges = $self->non_issues_charges
607
608 Calculates amount immediately owing by the patron - non-issue charges.
609
610 Charges exempt from non-issue are:
611 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
612 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
613 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
614
615 =cut
616
617 sub non_issues_charges {
618     my ($self) = @_;
619
620     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
621     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
622
623     my @not_fines;
624     push @not_fines, 'Res'
625       unless C4::Context->preference('HoldsInNoissuesCharge');
626     push @not_fines, 'Rent'
627       unless C4::Context->preference('RentalsInNoissuesCharge');
628     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
629         my $dbh = C4::Context->dbh;
630         push @not_fines,
631           @{
632             $dbh->selectcol_arrayref(q|
633                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
634             |)
635           };
636     }
637     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
638
639     return $self->lines->search(
640         {
641             accounttype    => { -not_in => \@not_fines }
642         },
643     )->total_outstanding;
644 }
645
646 =head3 lines
647
648 my $lines = $self->lines;
649
650 Return all credits and debits for the user, outstanding or otherwise
651
652 =cut
653
654 sub lines {
655     my ($self) = @_;
656
657     return Koha::Account::Lines->search(
658         {
659             borrowernumber => $self->{patron_id},
660         }
661     );
662 }
663
664 =head3 reconcile_balance
665
666 $account->reconcile_balance();
667
668 Find outstanding credits and use them to pay outstanding debits.
669 Currently, this implicitly uses the 'First In First Out' rule for
670 applying credits against debits.
671
672 =cut
673
674 sub reconcile_balance {
675     my ($self) = @_;
676
677     my $outstanding_debits  = $self->outstanding_debits;
678     my $outstanding_credits = $self->outstanding_credits;
679
680     while (     $outstanding_debits->total_outstanding > 0
681             and my $credit = $outstanding_credits->next )
682     {
683         # there's both outstanding debits and credits
684         $credit->apply( { debits => $outstanding_debits } );    # applying credit, no special offset
685
686         $outstanding_debits = $self->outstanding_debits;
687
688     }
689
690     return $self;
691 }
692
693 1;
694
695 =head2 Name mappings
696
697 =head3 $offset_type
698
699 =cut
700
701 our $offset_type = {
702     'credit'           => 'Manual Credit',
703     'forgiven'         => 'Writeoff',
704     'lost_item_return' => 'Lost Item',
705     'payment'          => 'Payment',
706     'writeoff'         => 'Writeoff',
707     'account'          => 'Account Fee',
708     'reserve'          => 'Reserve Fee',
709     'processing'       => 'Processing Fee',
710     'lost_item'        => 'Lost Item',
711     'rent'             => 'Rental Fee',
712     'overdue'          => 'OVERDUE',
713     'manual_debit'     => 'Manual Debit',
714     'hold_expired'     => 'Hold Expired'
715 };
716
717 =head3 $account_type_credit
718
719 =cut
720
721 our $account_type_credit = {
722     'credit'           => 'C',
723     'forgiven'         => 'FOR',
724     'lost_item_return' => 'LOST_RETURN',
725     'payment'          => 'Pay',
726     'writeoff'         => 'W'
727 };
728
729 =head3 $account_type_debit
730
731 =cut
732
733 our $account_type_debit = {
734     'account'       => 'A',
735     'overdue'       => 'OVERDUE',
736     'lost_item'     => 'LOST',
737     'new_card'      => 'N',
738     'sundry'        => 'M',
739     'processing'    => 'PF',
740     'rent'          => 'Rent',
741     'reserve'       => 'Res',
742     'manual_debit'  => 'M',
743     'hold_expired'  => 'HE'
744 };
745
746 =head1 AUTHORS
747
748 =encoding utf8
749
750 Kyle M Hall <kyle.m.hall@gmail.com>
751 Tomás Cohen Arazi <tomascohen@gmail.com>
752 Martin Renvoize <martin.renvoize@ptfs-europe.com>
753
754 =cut