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