Bug 21683: Remove accountlines.accountno
[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                     lastincrement     => undef,
350                 }
351             )->store();
352
353             # Record the account offset
354             my $account_offset = Koha::Account::Offset->new(
355                 {   credit_id => $line->id,
356                     type      => $Koha::Account::offset_type->{$type},
357                     amount    => $amount
358                 }
359             )->store();
360
361             UpdateStats(
362                 {   branch         => $library_id,
363                     type           => $type,
364                     amount         => $amount,
365                     borrowernumber => $self->{patron_id},
366                 }
367             ) if grep { $type eq $_ } ('payment', 'writeoff') ;
368
369             if ( C4::Context->preference("FinesLog") ) {
370                 logaction(
371                     "FINES", 'CREATE',
372                     $self->{patron_id},
373                     Dumper(
374                         {   action            => "create_$type",
375                             borrowernumber    => $self->{patron_id},
376                             amount            => $amount,
377                             description       => $description,
378                             amountoutstanding => $amount,
379                             accounttype       => $account_type,
380                             note              => $note,
381                             itemnumber        => $item_id,
382                             manager_id        => $user_id,
383                             branchcode        => $library_id,
384                         }
385                     )
386                 );
387             }
388         }
389     );
390
391     return $line;
392 }
393
394 =head3 add_debit
395
396 This method allows adding debits to a patron's account
397
398 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
399     {
400         amount       => $amount,
401         description  => $description,
402         note         => $note,
403         user_id      => $user_id,
404         library_id   => $library_id,
405         type         => $debit_type,
406         item_id      => $item_id,
407         issue_id     => $issue_id
408     }
409 );
410
411 $debit_type can be any of:
412   - fine
413   - lost_item
414   - new_card
415   - account
416   - sundry
417   - processing
418   - rent
419   - reserve
420   - overdue
421   - manual
422
423 =cut
424
425 sub add_debit {
426
427     my ( $self, $params ) = @_;
428
429     # amount should always be a positive value
430     my $amount       = $params->{amount};
431
432     unless ( $amount > 0 ) {
433         Koha::Exceptions::Account::AmountNotPositive->throw(
434             error => 'Debit amount passed is not positive'
435         );
436     }
437
438     my $description  = $params->{description} // q{};
439     my $note         = $params->{note} // q{};
440     my $user_id      = $params->{user_id};
441     my $library_id   = $params->{library_id};
442     my $type         = $params->{type};
443     my $item_id      = $params->{item_id};
444     my $issue_id     = $params->{issue_id};
445
446     my $schema = Koha::Database->new->schema;
447
448     unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
449         Koha::Exceptions::Account::UnrecognisedType->throw(
450             error => 'Type of debit not recognised'
451         );
452     }
453
454     my $account_type = $Koha::Account::account_type_debit->{$type};
455
456     my $line;
457
458     $schema->txn_do(
459         sub {
460
461             # Insert the account line
462             $line = Koha::Account::Line->new(
463                 {   borrowernumber    => $self->{patron_id},
464                     date              => \'NOW()',
465                     amount            => $amount,
466                     description       => $description,
467                     accounttype       => $account_type,
468                     amountoutstanding => $amount,
469                     payment_type      => undef,
470                     note              => $note,
471                     manager_id        => $user_id,
472                     itemnumber        => $item_id,
473                     issue_id          => $issue_id,
474                     branchcode        => $library_id,
475                     ( $type eq 'fine' ? ( lastincrement => $amount ) : ()),
476                 }
477             )->store();
478
479             # Record the account offset
480             my $account_offset = Koha::Account::Offset->new(
481                 {   debit_id => $line->id,
482                     type      => $Koha::Account::offset_type->{$type},
483                     amount    => $amount
484                 }
485             )->store();
486
487             if ( C4::Context->preference("FinesLog") ) {
488                 logaction(
489                     "FINES", 'CREATE',
490                     $self->{patron_id},
491                     Dumper(
492                         {   action            => "create_$type",
493                             borrowernumber    => $self->{patron_id},
494                             amount            => $amount,
495                             description       => $description,
496                             amountoutstanding => $amount,
497                             accounttype       => $account_type,
498                             note              => $note,
499                             itemnumber        => $item_id,
500                             manager_id        => $user_id,
501                         }
502                     )
503                 );
504             }
505         }
506     );
507
508     return $line;
509 }
510
511 =head3 balance
512
513 my $balance = $self->balance
514
515 Return the balance (sum of amountoutstanding columns)
516
517 =cut
518
519 sub balance {
520     my ($self) = @_;
521     return $self->lines->total_outstanding;
522 }
523
524 =head3 outstanding_debits
525
526 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
527
528 It returns the debit lines with outstanding amounts for the patron.
529
530 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
531 return a list of Koha::Account::Line objects.
532
533 =cut
534
535 sub outstanding_debits {
536     my ($self) = @_;
537
538     return $self->lines->search(
539         {
540             amount            => { '>' => 0 },
541             amountoutstanding => { '>' => 0 }
542         }
543     );
544 }
545
546 =head3 outstanding_credits
547
548 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
549
550 It returns the credit lines with outstanding amounts for the patron.
551
552 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
553 return a list of Koha::Account::Line objects.
554
555 =cut
556
557 sub outstanding_credits {
558     my ($self) = @_;
559
560     return $self->lines->search(
561         {
562             amount            => { '<' => 0 },
563             amountoutstanding => { '<' => 0 }
564         }
565     );
566 }
567
568 =head3 non_issues_charges
569
570 my $non_issues_charges = $self->non_issues_charges
571
572 Calculates amount immediately owing by the patron - non-issue charges.
573
574 Charges exempt from non-issue are:
575 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
576 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
577 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
578
579 =cut
580
581 sub non_issues_charges {
582     my ($self) = @_;
583
584     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
585     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
586
587     my @not_fines;
588     push @not_fines, 'Res'
589       unless C4::Context->preference('HoldsInNoissuesCharge');
590     push @not_fines, 'Rent'
591       unless C4::Context->preference('RentalsInNoissuesCharge');
592     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
593         my $dbh = C4::Context->dbh;
594         push @not_fines,
595           @{
596             $dbh->selectcol_arrayref(q|
597                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
598             |)
599           };
600     }
601     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
602
603     return $self->lines->search(
604         {
605             accounttype    => { -not_in => \@not_fines }
606         },
607     )->total_outstanding;
608 }
609
610 =head3 lines
611
612 my $lines = $self->lines;
613
614 Return all credits and debits for the user, outstanding or otherwise
615
616 =cut
617
618 sub lines {
619     my ($self) = @_;
620
621     return Koha::Account::Lines->search(
622         {
623             borrowernumber => $self->{patron_id},
624         }
625     );
626 }
627
628 =head3 reconcile_balance
629
630 $account->reconcile_balance();
631
632 Find outstanding credits and use them to pay outstanding debits.
633 Currently, this implicitly uses the 'First In First Out' rule for
634 applying credits against debits.
635
636 =cut
637
638 sub reconcile_balance {
639     my ($self) = @_;
640
641     my $outstanding_debits  = $self->outstanding_debits;
642     my $outstanding_credits = $self->outstanding_credits;
643
644     while (     $outstanding_debits->total_outstanding > 0
645             and my $credit = $outstanding_credits->next )
646     {
647         # there's both outstanding debits and credits
648         $credit->apply( { debits => $outstanding_debits } );    # applying credit, no special offset
649
650         $outstanding_debits = $self->outstanding_debits;
651
652     }
653
654     return $self;
655 }
656
657 1;
658
659 =head2 Name mappings
660
661 =head3 $offset_type
662
663 =cut
664
665 our $offset_type = {
666     'credit'           => 'Manual Credit',
667     'forgiven'         => 'Writeoff',
668     'lost_item_return' => 'Lost Item',
669     'payment'          => 'Payment',
670     'writeoff'         => 'Writeoff',
671     'account'          => 'Account Fee',
672     'reserve'          => 'Reserve Fee',
673     'processing'       => 'Processing Fee',
674     'lost_item'        => 'Lost Item',
675     'rent'             => 'Rental Fee',
676     'fine'             => 'Fine',
677     'manual_debit'     => 'Manual Debit',
678     'hold_expired'     => 'Hold Expired'
679 };
680
681 =head3 $account_type_credit
682
683 =cut
684
685 our $account_type_credit = {
686     'credit'           => 'C',
687     'forgiven'         => 'FOR',
688     'lost_item_return' => 'CR',
689     'payment'          => 'Pay',
690     'writeoff'         => 'W'
691 };
692
693 =head3 $account_type_debit
694
695 =cut
696
697 our $account_type_debit = {
698     'account'       => 'A',
699     'fine'          => 'FU',
700     'lost_item'     => 'L',
701     'new_card'      => 'N',
702     'sundry'        => 'M',
703     'processing'    => 'PF',
704     'rent'          => 'Rent',
705     'reserve'       => 'Res',
706     'overdue'       => 'O',
707     'manual_debit'  => 'M',
708     'hold_expired'  => 'HE'
709 };
710
711 =head1 AUTHORS
712
713 =encoding utf8
714
715 Kyle M Hall <kyle.m.hall@gmail.com>
716 Tomás Cohen Arazi <tomascohen@gmail.com>
717 Martin Renvoize <martin.renvoize@ptfs-europe.com>
718
719 =cut