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