Bug 21756: (QA follow-up) Add `Account Fee` mapping
[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::Account;
36
37 =head1 NAME
38
39 Koha::Accounts - Module for managing payments and fees for patrons
40
41 =cut
42
43 sub new {
44     my ( $class, $params ) = @_;
45
46     Carp::croak("No patron id passed in!") unless $params->{patron_id};
47
48     return bless( $params, $class );
49 }
50
51 =head2 pay
52
53 This method allows payments to be made against fees/fines
54
55 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
56     {
57         amount      => $amount,
58         sip         => $sipmode,
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 $sip          = $params->{sip};
75     my $description  = $params->{description};
76     my $note         = $params->{note} || q{};
77     my $library_id   = $params->{library_id};
78     my $lines        = $params->{lines};
79     my $type         = $params->{type} || 'payment';
80     my $payment_type = $params->{payment_type} || undef;
81     my $account_type = $params->{account_type};
82     my $offset_type  = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
83
84     my $userenv = C4::Context->userenv;
85
86     my $patron = Koha::Patrons->find( $self->{patron_id} );
87
88     # We should remove accountno, it is no longer needed
89     my $last = $self->lines->search(
90         {},
91         { order_by => 'accountno' } )->next();
92     my $accountno = $last ? $last->accountno + 1 : 1;
93
94     my $manager_id = $userenv ? $userenv->{number} : 0;
95
96     my @fines_paid; # List of account lines paid on with this payment
97
98     my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
99     $balance_remaining ||= 0;
100
101     my @account_offsets;
102
103     # We were passed a specific line to pay
104     foreach my $fine ( @$lines ) {
105         my $amount_to_pay =
106             $fine->amountoutstanding > $balance_remaining
107           ? $balance_remaining
108           : $fine->amountoutstanding;
109
110         my $old_amountoutstanding = $fine->amountoutstanding;
111         my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
112         $fine->amountoutstanding($new_amountoutstanding)->store();
113         $balance_remaining = $balance_remaining - $amount_to_pay;
114
115         if ( $fine->itemnumber && $fine->accounttype && ( $fine->accounttype eq 'Rep' || $fine->accounttype eq 'L' ) )
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                         accountno             => $fine->accountno,
142                         manager_id            => $manager_id,
143                         note                  => $note,
144                     }
145                 )
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         my $account_offset = Koha::Account::Offset->new(
172             {
173                 debit_id => $fine->id,
174                 type     => $offset_type,
175                 amount   => $amount_to_pay * -1,
176             }
177         );
178         push( @account_offsets, $account_offset );
179
180         if ( C4::Context->preference("FinesLog") ) {
181             logaction(
182                 "FINES", 'MODIFY',
183                 $self->{patron_id},
184                 Dumper(
185                     {
186                         action                => "fee_$type",
187                         borrowernumber        => $fine->borrowernumber,
188                         old_amountoutstanding => $old_amountoutstanding,
189                         new_amountoutstanding => $fine->amountoutstanding,
190                         amount_paid           => $amount_to_pay,
191                         accountlines_id       => $fine->id,
192                         accountno             => $fine->accountno,
193                         manager_id            => $manager_id,
194                         note                  => $note,
195                     }
196                 )
197             );
198             push( @fines_paid, $fine->id );
199         }
200
201         $balance_remaining = $balance_remaining - $amount_to_pay;
202         last unless $balance_remaining > 0;
203     }
204
205     $account_type ||=
206         $type eq 'writeoff' ? 'W'
207       : defined($sip)       ? "Pay$sip"
208       :                       'Pay';
209
210     $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
211
212     my $payment = Koha::Account::Line->new(
213         {
214             borrowernumber    => $self->{patron_id},
215             accountno         => $accountno,
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             branchcode        => $library_id,
224             note              => $note,
225         }
226     )->store();
227
228     foreach my $o ( @account_offsets ) {
229         $o->credit_id( $payment->id() );
230         $o->store();
231     }
232
233     UpdateStats(
234         {
235             branch         => $library_id,
236             type           => $type,
237             amount         => $amount,
238             borrowernumber => $self->{patron_id},
239             accountno      => $accountno,
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                     accountno         => $accountno,
252                     amount            => 0 - $amount,
253                     amountoutstanding => 0 - $balance_remaining,
254                     accounttype       => $account_type,
255                     accountlines_paid => \@fines_paid,
256                     manager_id        => $manager_id,
257                 }
258             )
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         library_id   => $library_id,
304         sip          => $sip,
305         payment_type => $payment_type,
306         type         => $credit_type,
307         item_id      => $item_id
308     }
309 );
310
311 $credit_type can be any of:
312   - 'credit'
313   - 'payment'
314   - 'forgiven'
315   - 'lost_item_return'
316   - 'writeoff'
317
318 =cut
319
320 sub add_credit {
321
322     my ( $self, $params ) = @_;
323
324     # amount is passed as a positive value, but we store credit as negative values
325     my $amount       = $params->{amount} * -1;
326     my $description  = $params->{description} // q{};
327     my $note         = $params->{note} // q{};
328     my $user_id      = $params->{user_id};
329     my $library_id   = $params->{library_id};
330     my $sip          = $params->{sip};
331     my $payment_type = $params->{payment_type};
332     my $type         = $params->{type} || 'payment';
333     my $item_id      = $params->{item_id};
334
335     my $schema = Koha::Database->new->schema;
336
337     my $account_type = $Koha::Account::account_type_credit->{$type};
338     $account_type .= $sip
339         if defined $sip &&
340            $type eq 'payment';
341
342     my $line;
343
344     $schema->txn_do(
345         sub {
346             # We should remove accountno, it is no longer needed
347             my $last = $self->lines->search(
348                 {},
349                 { order_by => 'accountno' } )->next();
350             my $accountno = $last ? $last->accountno + 1 : 1;
351
352             # Insert the account line
353             $line = Koha::Account::Line->new(
354                 {   borrowernumber    => $self->{patron_id},
355                     accountno         => $accountno,
356                     date              => \'NOW()',
357                     amount            => $amount,
358                     description       => $description,
359                     accounttype       => $account_type,
360                     amountoutstanding => $amount,
361                     payment_type      => $payment_type,
362                     note              => $note,
363                     manager_id        => $user_id,
364                     branchcode        => $library_id,
365                     itemnumber        => $item_id,
366                     lastincrement     => undef,
367                 }
368             )->store();
369
370             # Record the account offset
371             my $account_offset = Koha::Account::Offset->new(
372                 {   credit_id => $line->id,
373                     type      => $Koha::Account::offset_type->{$type},
374                     amount    => $amount
375                 }
376             )->store();
377
378             UpdateStats(
379                 {   branch         => $library_id,
380                     type           => $type,
381                     amount         => $amount,
382                     borrowernumber => $self->{patron_id},
383                     accountno      => $accountno,
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                             accountno         => $accountno,
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                 );
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         library_id   => $library_id,
424         type         => $debit_type,
425         item_id      => $item_id,
426         issue_id     => $issue_id
427     }
428 );
429
430 $debit_type can be any of:
431   - fine
432   - lost_item
433   - new_card
434   - account
435   - sundry
436   - processing
437   - rent
438   - reserve
439   - overdue
440   - manual
441
442 =cut
443
444 sub add_debit {
445
446     my ( $self, $params ) = @_;
447
448     # amount should always be a positive value
449     my $amount       = $params->{amount};
450
451     unless ( $amount > 0 ) {
452         Koha::Exceptions::Account::AmountNotPositive->throw(
453             error => 'Debit amount passed is not positive'
454         );
455     }
456
457     my $description  = $params->{description} // q{};
458     my $note         = $params->{note} // q{};
459     my $user_id      = $params->{user_id};
460     my $library_id   = $params->{library_id};
461     my $type         = $params->{type};
462     my $item_id      = $params->{item_id};
463     my $issue_id     = $params->{issue_id};
464
465     my $schema = Koha::Database->new->schema;
466
467     unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
468         Koha::Exceptions::Account::UnrecognisedType->throw(
469             error => 'Type of debit not recognised'
470         );
471     }
472
473     my $account_type = $Koha::Account::account_type_debit->{$type};
474
475     my $line;
476
477     $schema->txn_do(
478         sub {
479             # We should remove accountno, it is no longer needed
480             my $last = Koha::Account::Lines->search( { borrowernumber => $self->{patron_id} },
481                 { order_by => 'accountno' } )->next();
482             my $accountno = $last ? $last->accountno + 1 : 1;
483
484             # Insert the account line
485             $line = Koha::Account::Line->new(
486                 {   borrowernumber    => $self->{patron_id},
487                     date              => \'NOW()',
488                     amount            => $amount,
489                     description       => $description,
490                     accounttype       => $account_type,
491                     amountoutstanding => $amount,
492                     payment_type      => undef,
493                     note              => $note,
494                     manager_id        => $user_id,
495                     itemnumber        => $item_id,
496                     issue_id          => $issue_id,
497                     branchcode        => $library_id,
498                     ( $type eq 'fine' ? ( lastincrement => $amount ) : ()),
499                 }
500             )->store();
501
502             # Record the account offset
503             my $account_offset = Koha::Account::Offset->new(
504                 {   debit_id => $line->id,
505                     type      => $Koha::Account::offset_type->{$type},
506                     amount    => $amount
507                 }
508             )->store();
509
510             if ( C4::Context->preference("FinesLog") ) {
511                 logaction(
512                     "FINES", 'CREATE',
513                     $self->{patron_id},
514                     Dumper(
515                         {   action            => "create_$type",
516                             borrowernumber    => $self->{patron_id},
517                             accountno         => $accountno,
518                             amount            => $amount,
519                             description       => $description,
520                             amountoutstanding => $amount,
521                             accounttype       => $account_type,
522                             note              => $note,
523                             itemnumber        => $item_id,
524                             manager_id        => $user_id,
525                         }
526                     )
527                 );
528             }
529         }
530     );
531
532     return $line;
533 }
534
535 =head3 balance
536
537 my $balance = $self->balance
538
539 Return the balance (sum of amountoutstanding columns)
540
541 =cut
542
543 sub balance {
544     my ($self) = @_;
545     return $self->lines->total_outstanding;
546 }
547
548 =head3 outstanding_debits
549
550 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
551
552 It returns the debit lines with outstanding amounts for the patron.
553
554 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
555 return a list of Koha::Account::Line objects.
556
557 =cut
558
559 sub outstanding_debits {
560     my ($self) = @_;
561
562     return $self->lines->search(
563         {
564             amount            => { '>' => 0 },
565             amountoutstanding => { '>' => 0 }
566         }
567     );
568 }
569
570 =head3 outstanding_credits
571
572 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
573
574 It returns the credit lines with outstanding amounts for the patron.
575
576 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
577 return a list of Koha::Account::Line objects.
578
579 =cut
580
581 sub outstanding_credits {
582     my ($self) = @_;
583
584     return $self->lines->search(
585         {
586             amount            => { '<' => 0 },
587             amountoutstanding => { '<' => 0 }
588         }
589     );
590 }
591
592 =head3 non_issues_charges
593
594 my $non_issues_charges = $self->non_issues_charges
595
596 Calculates amount immediately owing by the patron - non-issue charges.
597
598 Charges exempt from non-issue are:
599 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
600 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
601 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
602
603 =cut
604
605 sub non_issues_charges {
606     my ($self) = @_;
607
608     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
609     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
610
611     my @not_fines;
612     push @not_fines, 'Res'
613       unless C4::Context->preference('HoldsInNoissuesCharge');
614     push @not_fines, 'Rent'
615       unless C4::Context->preference('RentalsInNoissuesCharge');
616     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
617         my $dbh = C4::Context->dbh;
618         push @not_fines,
619           @{
620             $dbh->selectcol_arrayref(q|
621                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
622             |)
623           };
624     }
625     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
626
627     return $self->lines->search(
628         {
629             accounttype    => { -not_in => \@not_fines }
630         },
631     )->total_outstanding;
632 }
633
634 =head3 lines
635
636 my $lines = $self->lines;
637
638 Return all credits and debits for the user, outstanding or otherwise
639
640 =cut
641
642 sub lines {
643     my ($self) = @_;
644
645     return Koha::Account::Lines->search(
646         {
647             borrowernumber => $self->{patron_id},
648         }
649     );
650 }
651
652 =head3 reconcile_balance
653
654 $account->reconcile_balance();
655
656 Find outstanding credits and use them to pay outstanding debits.
657 Currently, this implicitly uses the 'First In First Out' rule for
658 applying credits against debits.
659
660 =cut
661
662 sub reconcile_balance {
663     my ($self) = @_;
664
665     my $outstanding_debits  = $self->outstanding_debits;
666     my $outstanding_credits = $self->outstanding_credits;
667
668     while (     $outstanding_debits->total_outstanding > 0
669             and my $credit = $outstanding_credits->next )
670     {
671         # there's both outstanding debits and credits
672         $credit->apply( { debits => $outstanding_debits } );    # applying credit, no special offset
673
674         $outstanding_debits = $self->outstanding_debits;
675
676     }
677
678     return $self;
679 }
680
681 1;
682
683 =head2 Name mappings
684
685 =head3 $offset_type
686
687 =cut
688
689 our $offset_type = {
690     'credit'           => 'Manual Credit',
691     'forgiven'         => 'Writeoff',
692     'lost_item_return' => 'Lost Item',
693     'payment'          => 'Payment',
694     'writeoff'         => 'Writeoff',
695     'account'          => 'Account Fee',
696     'reserve'          => 'Reserve Fee',
697     'processing'       => 'Processing Fee',
698     'lost_item'        => 'Lost Item',
699     'rent'             => 'Rental Fee',
700     'fine'             => 'Fine',
701     'manual_debit'     => 'Manual Debit',
702     'hold_expired'     => 'Hold Expired'
703 };
704
705 =head3 $account_type_credit
706
707 =cut
708
709 our $account_type_credit = {
710     'credit'           => 'C',
711     'forgiven'         => 'FOR',
712     'lost_item_return' => 'CR',
713     'payment'          => 'Pay',
714     'writeoff'         => 'W'
715 };
716
717 =head3 $account_type_debit
718
719 =cut
720
721 our $account_type_debit = {
722     'account'       => 'A',
723     'fine'          => 'FU',
724     'lost_item'     => 'L',
725     'new_card'      => 'N',
726     'sundry'        => 'M',
727     'processing'    => 'PF',
728     'rent'          => 'Rent',
729     'reserve'       => 'Res',
730     'overdue'       => 'O',
731     'manual_debit'  => 'M',
732     'hold_expired'  => 'HE'
733 };
734
735 =head1 AUTHORS
736
737 =encoding utf8
738
739 Kyle M Hall <kyle.m.hall@gmail.com>
740 Tomás Cohen Arazi <tomascohen@gmail.com>
741 Martin Renvoize <martin.renvoize@ptfs-europe.com>
742
743 =cut