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