Increment version for 22.11.12 release
[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 qw( Dumper );
24 use Try::Tiny qw( catch try );
25
26 use C4::Circulation qw( ReturnLostItem CanBookBeRenewed AddRenewal );
27 use C4::Letters;
28 use C4::Log qw( logaction );
29 use C4::Stats qw( UpdateStats );
30 use C4::Overdues qw(GetFine);
31
32 use Koha::Patrons;
33 use Koha::Account::Lines;
34 use Koha::Account::Offsets;
35 use Koha::Account::DebitTypes;
36 use Koha::Exceptions;
37 use Koha::Exceptions::Account;
38 use Koha::Plugins;
39
40 =head1 NAME
41
42 Koha::Accounts - Module for managing payments and fees for patrons
43
44 =cut
45
46 sub new {
47     my ( $class, $params ) = @_;
48
49     Carp::croak("No patron id passed in!") unless $params->{patron_id};
50
51     return bless( $params, $class );
52 }
53
54 =head2 pay
55
56 This method allows payments to be made against fees/fines
57
58 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
59     {
60         amount      => $amount,
61         note        => $note,
62         description => $description,
63         library_id  => $branchcode,
64         lines       => $lines, # Arrayref of Koha::Account::Line objects to pay
65         credit_type => $type,  # credit_type_code code
66         item_id     => $itemnumber,     # pass the itemnumber if this is a credit pertianing to a specific item (i.e LOST_FOUND)
67     }
68 );
69
70 =cut
71
72 sub pay {
73     my ( $self, $params ) = @_;
74
75     my $amount        = $params->{amount};
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 $cash_register = $params->{cash_register};
83     my $item_id       = $params->{item_id};
84
85     my $userenv = C4::Context->userenv;
86
87     unless ( $type eq 'WRITEOFF' ) {
88         Koha::Exceptions::Account::PaymentTypeRequired->throw()
89             if ( C4::Context->preference("RequirePaymentType")
90             && !defined($payment_type) );
91
92         my $av = Koha::AuthorisedValues->search_with_library_limits(
93             { category => 'PAYMENT_TYPE', authorised_value => $payment_type } );
94
95         if ( !$av->count && C4::Context->preference("RequirePaymentType") ) {
96             Koha::Exceptions::Account::InvalidPaymentType->throw( error => 'Invalid payment type' );
97         }
98     }
99     my $manager_id = $userenv ? $userenv->{number} : undef;
100     my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
101     my $payment = $self->payin_amount(
102         {
103             interface     => $interface,
104             type          => $type,
105             amount        => $amount,
106             payment_type  => $payment_type,
107             cash_register => $cash_register,
108             user_id       => $manager_id,
109             library_id    => $library_id,
110             item_id       => $item_id,
111             description   => $description,
112             note          => $note,
113             debits        => $lines
114         }
115     );
116
117     # NOTE: Pay historically always applied as much credit as it could to all
118     # existing outstanding debits, whether passed specific debits or otherwise.
119     if ( $payment->amountoutstanding ) {
120         $payment =
121           $payment->apply(
122             { debits => [ $self->outstanding_debits->as_list ] } );
123     }
124
125     my $patron = Koha::Patrons->find( $self->{patron_id} );
126     my @account_offsets = $payment->credit_offsets({ type => 'APPLY' })->as_list;
127     if ( C4::Context->preference('UseEmailReceipts') ) {
128         if (
129             my $letter = C4::Letters::GetPreparedLetter(
130                 module                 => 'circulation',
131                 letter_code            => uc("ACCOUNT_$type"),
132                 message_transport_type => 'email',
133                 lang    => $patron->lang,
134                 tables => {
135                     borrowers       => $self->{patron_id},
136                     branches        => $library_id,
137                 },
138                 substitute => {
139                     credit => $payment,
140                     offsets => \@account_offsets,
141                 },
142               )
143           )
144         {
145             C4::Letters::EnqueueLetter(
146                 {
147                     letter                 => $letter,
148                     borrowernumber         => $self->{patron_id},
149                     message_transport_type => 'email',
150                 }
151             ) or warn "can't enqueue letter $letter";
152         }
153     }
154
155     my $renew_outcomes = [];
156     for my $message ( @{$payment->object_messages} ) {
157         push @{$renew_outcomes}, $message->payload;
158     }
159
160     return { payment_id => $payment->id, renew_result => $renew_outcomes };
161 }
162
163 =head3 add_credit
164
165 This method allows adding credits to a patron's account
166
167 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
168     {
169         amount       => $amount,
170         description  => $description,
171         note         => $note,
172         user_id      => $user_id,
173         interface    => $interface,
174         library_id   => $library_id,
175         payment_type => $payment_type,
176         type         => $credit_type,
177         item_id      => $item_id
178     }
179 );
180
181 $credit_type can be any of:
182   - 'CREDIT'
183   - 'PAYMENT'
184   - 'FORGIVEN'
185   - 'LOST_FOUND'
186   - 'OVERPAYMENT'
187   - 'PAYMENT'
188   - 'WRITEOFF'
189   - 'PROCESSING_FOUND'
190
191 =cut
192
193 sub add_credit {
194
195     my ( $self, $params ) = @_;
196
197     # check for mandatory params
198     my @mandatory = ( 'interface', 'amount' );
199     for my $param (@mandatory) {
200         unless ( defined( $params->{$param} ) ) {
201             Koha::Exceptions::MissingParameter->throw(
202                 error => "The $param parameter is mandatory" );
203         }
204     }
205
206     # amount should always be passed as a positive value
207     my $amount = $params->{amount} * -1;
208     unless ( $amount < 0 ) {
209         Koha::Exceptions::Account::AmountNotPositive->throw(
210             error => 'Debit amount passed is not positive' );
211     }
212
213     my $description   = $params->{description} // q{};
214     my $note          = $params->{note} // q{};
215     my $user_id       = $params->{user_id};
216     my $interface     = $params->{interface};
217     my $library_id    = $params->{library_id};
218     my $cash_register = $params->{cash_register};
219     my $payment_type  = $params->{payment_type};
220     my $credit_type   = $params->{type} || 'PAYMENT';
221     my $item_id       = $params->{item_id};
222
223     Koha::Exceptions::Account::RegisterRequired->throw()
224       if ( C4::Context->preference("UseCashRegisters")
225         && defined($payment_type)
226         && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
227         && !defined($cash_register) );
228
229     my $line;
230     my $schema = Koha::Database->new->schema;
231     try {
232         $schema->txn_do(
233             sub {
234
235                 # Insert the account line
236                 $line = Koha::Account::Line->new(
237                     {
238                         borrowernumber    => $self->{patron_id},
239                         date              => \'NOW()',
240                         amount            => $amount,
241                         description       => $description,
242                         credit_type_code  => $credit_type,
243                         amountoutstanding => $amount,
244                         payment_type      => $payment_type,
245                         note              => $note,
246                         manager_id        => $user_id,
247                         interface         => $interface,
248                         branchcode        => $library_id,
249                         register_id       => $cash_register,
250                         itemnumber        => $item_id,
251                     }
252                 )->store();
253
254                 # Record the account offset
255                 my $account_offset = Koha::Account::Offset->new(
256                     {
257                         credit_id => $line->id,
258                         type      => 'CREATE',
259                         amount    => $amount * -1
260                     }
261                 )->store();
262
263                 C4::Stats::UpdateStats(
264                     {
265                         branch         => $library_id,
266                         type           => lc($credit_type),
267                         amount         => $amount,
268                         borrowernumber => $self->{patron_id},
269                     }
270                 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
271
272                 Koha::Plugins->call(
273                     'after_account_action',
274                     {
275                         action  => "add_credit",
276                         payload => {
277                             type => lc($credit_type),
278                             line => $line->get_from_storage, #TODO Seems unneeded
279                         }
280                     }
281                 );
282
283                 if ( C4::Context->preference("FinesLog") ) {
284                     logaction(
285                         "FINES", 'CREATE',
286                         $self->{patron_id},
287                         Dumper(
288                             {
289                                 action            => "create_$credit_type",
290                                 borrowernumber    => $self->{patron_id},
291                                 amount            => $amount,
292                                 description       => $description,
293                                 amountoutstanding => $amount,
294                                 credit_type_code  => $credit_type,
295                                 note              => $note,
296                                 itemnumber        => $item_id,
297                                 manager_id        => $user_id,
298                                 branchcode        => $library_id,
299                             }
300                         ),
301                         $interface
302                     );
303                 }
304             }
305         );
306     }
307     catch {
308         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
309             if ( $_->broken_fk eq 'credit_type_code' ) {
310                 Koha::Exceptions::Account::UnrecognisedType->throw(
311                     error => 'Type of credit not recognised' );
312             }
313         }
314         $_->rethrow;
315     };
316
317     return $line;
318 }
319
320 =head3 payin_amount
321
322     my $credit = $account->payin_amount(
323         {
324             amount          => $amount,
325             type            => $credit_type,
326             payment_type    => $payment_type,
327             cash_register   => $register_id,
328             interface       => $interface,
329             library_id      => $branchcode,
330             user_id         => $staff_id,
331             debits          => $debit_lines,
332             description     => $description,
333             note            => $note
334         }
335     );
336
337 This method allows an amount to be paid into a patrons account and immediately applied against debts.
338
339 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
340
341 $credit_type can be any of:
342   - 'PAYMENT'
343   - 'WRITEOFF'
344   - 'FORGIVEN'
345
346 =cut
347
348 sub payin_amount {
349     my ( $self, $params ) = @_;
350
351     # check for mandatory params
352     my @mandatory = ( 'interface', 'amount', 'type' );
353     for my $param (@mandatory) {
354         unless ( defined( $params->{$param} ) ) {
355             Koha::Exceptions::MissingParameter->throw(
356                 error => "The $param parameter is mandatory" );
357         }
358     }
359
360     # Check for mandatory register
361     Koha::Exceptions::Account::RegisterRequired->throw()
362       if ( C4::Context->preference("UseCashRegisters")
363         && defined( $params->{payment_type} )
364         && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
365         && !defined($params->{cash_register}) );
366
367     # amount should always be passed as a positive value
368     my $amount = $params->{amount};
369     unless ( $amount > 0 ) {
370         Koha::Exceptions::Account::AmountNotPositive->throw(
371             error => 'Payin amount passed is not positive' );
372     }
373
374     my $credit;
375     my $schema = Koha::Database->new->schema;
376     $schema->txn_do(
377         sub {
378
379             # Add payin credit
380             $credit = $self->add_credit($params);
381
382             # Offset debts passed first
383             if ( exists( $params->{debits} ) ) {
384                 $credit = $credit->apply(
385                     {
386                         debits => $params->{debits}
387                     }
388                 );
389             }
390
391             # Offset against remaining balance if AutoReconcile
392             if ( C4::Context->preference("AccountAutoReconcile")
393                 && $credit->amountoutstanding != 0 )
394             {
395                 $credit = $credit->apply(
396                     {
397                         debits => [ $self->outstanding_debits->as_list ]
398                     }
399                 );
400             }
401         }
402     );
403
404     return $credit;
405 }
406
407 =head3 add_debit
408
409 This method allows adding debits to a patron's account
410
411     my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
412         {
413             amount           => $amount,
414             description      => $description,
415             note             => $note,
416             user_id          => $user_id,
417             interface        => $interface,
418             library_id       => $library_id,
419             type             => $debit_type,
420             transaction_type => $transaction_type,
421             cash_register    => $register_id,
422             item_id          => $item_id,
423             issue_id         => $issue_id
424         }
425     );
426
427 $debit_type can be any of:
428   - ACCOUNT
429   - ACCOUNT_RENEW
430   - RESERVE_EXPIRED
431   - LOST
432   - sundry
433   - NEW_CARD
434   - OVERDUE
435   - PROCESSING
436   - RENT
437   - RENT_DAILY
438   - RENT_RENEW
439   - RENT_DAILY_RENEW
440   - RESERVE
441   - PAYOUT
442
443 =cut
444
445 sub add_debit {
446
447     my ( $self, $params ) = @_;
448
449     # check for mandatory params
450     my @mandatory = ( 'interface', 'type', 'amount' );
451     for my $param (@mandatory) {
452         unless ( defined( $params->{$param} ) ) {
453             Koha::Exceptions::MissingParameter->throw(
454                 error => "The $param parameter is mandatory" );
455         }
456     }
457
458     # check for cash register if using cash
459     Koha::Exceptions::Account::RegisterRequired->throw()
460       if ( C4::Context->preference("UseCashRegisters")
461         && defined( $params->{transaction_type} )
462         && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
463         && !defined( $params->{cash_register} ) );
464
465     # amount should always be a positive value
466     my $amount = $params->{amount};
467     unless ( $amount > 0 ) {
468         Koha::Exceptions::Account::AmountNotPositive->throw(
469             error => 'Debit amount passed is not positive' );
470     }
471
472     my $description      = $params->{description} // q{};
473     my $note             = $params->{note} // q{};
474     my $user_id          = $params->{user_id};
475     my $interface        = $params->{interface};
476     my $library_id       = $params->{library_id};
477     my $cash_register    = $params->{cash_register};
478     my $debit_type       = $params->{type};
479     my $transaction_type = $params->{transaction_type};
480     my $item_id          = $params->{item_id};
481     my $issue_id         = $params->{issue_id};
482
483     my $line;
484     my $schema = Koha::Database->new->schema;
485     try {
486         $schema->txn_do(
487             sub {
488
489                 # Insert the account line
490                 $line = Koha::Account::Line->new(
491                     {
492                         borrowernumber    => $self->{patron_id},
493                         date              => \'NOW()',
494                         amount            => $amount,
495                         description       => $description,
496                         debit_type_code   => $debit_type,
497                         amountoutstanding => $amount,
498                         payment_type      => $transaction_type,
499                         note              => $note,
500                         manager_id        => $user_id,
501                         interface         => $interface,
502                         itemnumber        => $item_id,
503                         issue_id          => $issue_id,
504                         branchcode        => $library_id,
505                         register_id       => $cash_register,
506                         (
507                             $debit_type eq 'OVERDUE'
508                             ? ( status => 'UNRETURNED' )
509                             : ()
510                         ),
511                     }
512                 )->store();
513
514                 # Record the account offset
515                 my $account_offset = Koha::Account::Offset->new(
516                     {
517                         debit_id => $line->id,
518                         type     => 'CREATE',
519                         amount   => $amount
520                     }
521                 )->store();
522
523                 if ( C4::Context->preference("FinesLog") ) {
524                     logaction(
525                         "FINES", 'CREATE',
526                         $self->{patron_id},
527                         Dumper(
528                             {
529                                 action            => "create_$debit_type",
530                                 borrowernumber    => $self->{patron_id},
531                                 amount            => $amount,
532                                 description       => $description,
533                                 amountoutstanding => $amount,
534                                 debit_type_code   => $debit_type,
535                                 note              => $note,
536                                 itemnumber        => $item_id,
537                                 manager_id        => $user_id,
538                             }
539                         ),
540                         $interface
541                     );
542                 }
543             }
544         );
545     }
546     catch {
547         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
548             if ( $_->broken_fk eq 'debit_type_code' ) {
549                 Koha::Exceptions::Account::UnrecognisedType->throw(
550                     error => 'Type of debit not recognised' );
551             }
552             else {
553                 $_->rethrow;
554             }
555         }
556     };
557
558     return $line;
559 }
560
561 =head3 payout_amount
562
563     my $debit = $account->payout_amount(
564         {
565             payout_type => $payout_type,
566             register_id => $register_id,
567             staff_id    => $staff_id,
568             interface   => 'intranet',
569             amount      => $amount,
570             credits     => $credit_lines
571         }
572     );
573
574 This method allows an amount to be paid out from a patrons account against outstanding credits.
575
576 $payout_type can be any of the defined payment_types:
577
578 =cut
579
580 sub payout_amount {
581     my ( $self, $params ) = @_;
582
583     # Check for mandatory parameters
584     my @mandatory =
585       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
586     for my $param (@mandatory) {
587         unless ( defined( $params->{$param} ) ) {
588             Koha::Exceptions::MissingParameter->throw(
589                 error => "The $param parameter is mandatory" );
590         }
591     }
592
593     # Check for mandatory register
594     Koha::Exceptions::Account::RegisterRequired->throw()
595       if ( C4::Context->preference("UseCashRegisters")
596         && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
597         && !defined($params->{cash_register}) );
598
599     # Amount should always be passed as a positive value
600     my $amount = $params->{amount};
601     unless ( $amount > 0 ) {
602         Koha::Exceptions::Account::AmountNotPositive->throw(
603             error => 'Payout amount passed is not positive' );
604     }
605
606     # Amount should always be less than or equal to outstanding credit
607     my $outstanding = 0;
608     my $outstanding_credits =
609       exists( $params->{credits} )
610       ? $params->{credits}
611       : $self->outstanding_credits->as_list;
612     for my $credit ( @{$outstanding_credits} ) {
613         $outstanding += $credit->amountoutstanding;
614     }
615     $outstanding = $outstanding * -1;
616     Koha::Exceptions::ParameterTooHigh->throw( error =>
617 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
618     ) unless ( $outstanding >= $amount );
619
620     my $payout;
621     my $schema = Koha::Database->new->schema;
622     $schema->txn_do(
623         sub {
624
625             # A 'payout' is a 'debit'
626             $payout = $self->add_debit(
627                 {
628                     amount            => $params->{amount},
629                     type              => 'PAYOUT',
630                     transaction_type  => $params->{payout_type},
631                     amountoutstanding => $params->{amount},
632                     user_id           => $params->{staff_id},
633                     interface         => $params->{interface},
634                     branchcode        => $params->{branch},
635                     cash_register     => $params->{cash_register}
636                 }
637             );
638
639             # Offset against credits
640             for my $credit ( @{$outstanding_credits} ) {
641                 $credit->apply( { debits => [$payout] } );
642                 $payout->discard_changes;
643                 last if $payout->amountoutstanding == 0;
644             }
645
646             # Set payout as paid
647             $payout->status('PAID')->store;
648         }
649     );
650
651     return $payout;
652 }
653
654 =head3 balance
655
656 my $balance = $self->balance
657
658 Return the balance (sum of amountoutstanding columns)
659
660 =cut
661
662 sub balance {
663     my ($self) = @_;
664     return $self->lines->total_outstanding;
665 }
666
667 =head3 outstanding_debits
668
669 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
670
671 It returns the debit lines with outstanding amounts for the patron.
672
673 It returns a Koha::Account::Lines iterator.
674
675 =cut
676
677 sub outstanding_debits {
678     my ($self) = @_;
679
680     return $self->lines->search(
681         {
682             amount            => { '>' => 0 },
683             amountoutstanding => { '>' => 0 }
684         }
685     );
686 }
687
688 =head3 outstanding_credits
689
690 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
691
692 It returns the credit lines with outstanding amounts for the patron.
693
694 It returns a Koha::Account::Lines iterator.
695
696 =cut
697
698 sub outstanding_credits {
699     my ($self) = @_;
700
701     return $self->lines->search(
702         {
703             amount            => { '<' => 0 },
704             amountoutstanding => { '<' => 0 }
705         }
706     );
707 }
708
709 =head3 non_issues_charges
710
711 my $non_issues_charges = $self->non_issues_charges
712
713 Calculates amount immediately owing by the patron - non-issue charges.
714
715 Charges exempt from non-issue are:
716 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
717 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
718 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
719
720 =cut
721
722 sub non_issues_charges {
723     my ($self) = @_;
724
725     #NOTE: With bug 23049 these preferences could be moved to being attached
726     #to individual debit types to give more flexability and specificity.
727     my @not_fines;
728     push @not_fines, 'RESERVE'
729       unless C4::Context->preference('HoldsInNoissuesCharge');
730     push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
731       unless C4::Context->preference('RentalsInNoissuesCharge');
732     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
733         my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
734         push @not_fines, @man_inv;
735     }
736
737     return $self->lines->search(
738         {
739             debit_type_code => { -not_in => \@not_fines }
740         },
741     )->total_outstanding;
742 }
743
744 =head3 lines
745
746 my $lines = $self->lines;
747
748 Return all credits and debits for the user, outstanding or otherwise
749
750 =cut
751
752 sub lines {
753     my ($self) = @_;
754
755     return Koha::Account::Lines->search(
756         {
757             borrowernumber => $self->{patron_id},
758         }
759     );
760 }
761
762 =head3 reconcile_balance
763
764 $account->reconcile_balance();
765
766 Find outstanding credits and use them to pay outstanding debits.
767 Currently, this implicitly uses the 'First In First Out' rule for
768 applying credits against debits.
769
770 =cut
771
772 sub reconcile_balance {
773     my ($self) = @_;
774
775     my $outstanding_debits  = $self->outstanding_debits;
776     my $outstanding_credits = $self->outstanding_credits;
777
778     while (     $outstanding_debits->total_outstanding > 0
779             and my $credit = $outstanding_credits->next )
780     {
781         # there's both outstanding debits and credits
782         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
783
784         $outstanding_debits = $self->outstanding_debits;
785
786     }
787
788     return $self;
789 }
790
791 1;
792
793 =head1 AUTHORS
794
795 =encoding utf8
796
797 Kyle M Hall <kyle.m.hall@gmail.com>
798 Tomás Cohen Arazi <tomascohen@gmail.com>
799 Martin Renvoize <martin.renvoize@ptfs-europe.com>
800
801 =cut