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