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