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