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