Bug 33789: Return issue info. when adding a credit
[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     my $issue_id      = $params->{issue_id};
214
215     Koha::Exceptions::Account::RegisterRequired->throw()
216       if ( C4::Context->preference("UseCashRegisters")
217         && defined($payment_type)
218         && ( $payment_type eq 'CASH' || $payment_type eq 'SIP00' )
219         && !defined($cash_register) );
220
221     my $line;
222     my $schema = Koha::Database->new->schema;
223     try {
224         $schema->txn_do(
225             sub {
226
227                 # Insert the account line
228                 $line = Koha::Account::Line->new(
229                     {
230                         borrowernumber    => $self->{patron_id},
231                         date              => \'NOW()',
232                         amount            => $amount,
233                         description       => $description,
234                         credit_type_code  => $credit_type,
235                         amountoutstanding => $amount,
236                         payment_type      => $payment_type,
237                         note              => $note,
238                         manager_id        => $user_id,
239                         interface         => $interface,
240                         branchcode        => $library_id,
241                         register_id       => $cash_register,
242                         itemnumber        => $item_id,
243                         issue_id          => $issue_id,
244                     }
245                 )->store();
246
247                 # Record the account offset
248                 my $account_offset = Koha::Account::Offset->new(
249                     {
250                         credit_id => $line->id,
251                         type      => 'CREATE',
252                         amount    => $amount * -1
253                     }
254                 )->store();
255
256                 C4::Stats::UpdateStats(
257                     {
258                         branch         => $library_id,
259                         type           => lc($credit_type),
260                         amount         => $amount,
261                         borrowernumber => $self->{patron_id},
262                         interface      => $interface,
263                     }
264                 ) if grep { $credit_type eq $_ } ( 'PAYMENT', 'WRITEOFF' );
265
266                 Koha::Plugins->call(
267                     'after_account_action',
268                     {
269                         action  => "add_credit",
270                         payload => {
271                             type => lc($credit_type),
272                             line => $line->get_from_storage, #TODO Seems unneeded
273                         }
274                     }
275                 );
276
277                 if ( C4::Context->preference("FinesLog") ) {
278                     logaction(
279                         "FINES", 'CREATE',
280                         $self->{patron_id},
281                         Dumper(
282                             {
283                                 action            => "create_$credit_type",
284                                 borrowernumber    => $self->{patron_id},
285                                 amount            => $amount,
286                                 description       => $description,
287                                 amountoutstanding => $amount,
288                                 credit_type_code  => $credit_type,
289                                 note              => $note,
290                                 itemnumber        => $item_id,
291                                 manager_id        => $user_id,
292                                 branchcode        => $library_id,
293                             }
294                         ),
295                         $interface
296                     );
297                 }
298             }
299         );
300     }
301     catch {
302         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
303             if ( $_->broken_fk eq 'credit_type_code' ) {
304                 Koha::Exceptions::Account::UnrecognisedType->throw(
305                     error => 'Type of credit not recognised' );
306             }
307             else {
308                 $_->rethrow;
309             }
310         }
311     };
312
313     return $line;
314 }
315
316 =head3 payin_amount
317
318     my $credit = $account->payin_amount(
319         {
320             amount          => $amount,
321             type            => $credit_type,
322             payment_type    => $payment_type,
323             cash_register   => $register_id,
324             interface       => $interface,
325             library_id      => $branchcode,
326             user_id         => $staff_id,
327             debits          => $debit_lines,
328             description     => $description,
329             note            => $note
330         }
331     );
332
333 This method allows an amount to be paid into a patrons account and immediately applied against debts.
334
335 You can optionally pass a debts parameter which consists of an arrayref of Koha::Account::Line debit lines.
336
337 $credit_type can be any of:
338   - 'PAYMENT'
339   - 'WRITEOFF'
340   - 'FORGIVEN'
341
342 =cut
343
344 sub payin_amount {
345     my ( $self, $params ) = @_;
346
347     # check for mandatory params
348     my @mandatory = ( 'interface', 'amount', 'type' );
349     for my $param (@mandatory) {
350         unless ( defined( $params->{$param} ) ) {
351             Koha::Exceptions::MissingParameter->throw(
352                 error => "The $param parameter is mandatory" );
353         }
354     }
355
356     # Check for mandatory register
357     Koha::Exceptions::Account::RegisterRequired->throw()
358       if ( C4::Context->preference("UseCashRegisters")
359         && defined( $params->{payment_type} )
360         && ( $params->{payment_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
361         && !defined($params->{cash_register}) );
362
363     # amount should always be passed as a positive value
364     my $amount = $params->{amount};
365     unless ( $amount > 0 ) {
366         Koha::Exceptions::Account::AmountNotPositive->throw(
367             error => 'Payin amount passed is not positive' );
368     }
369
370     my $credit;
371     my $schema = Koha::Database->new->schema;
372     $schema->txn_do(
373         sub {
374
375             # Add payin credit
376             $credit = $self->add_credit($params);
377
378             # Offset debts passed first
379             if ( exists( $params->{debits} ) ) {
380                 $credit = $credit->apply(
381                     {
382                         debits => $params->{debits}
383                     }
384                 );
385             }
386
387             # Offset against remaining balance if AutoReconcile
388             if ( C4::Context->preference("AccountAutoReconcile")
389                 && $credit->amountoutstanding != 0 )
390             {
391                 $credit = $credit->apply(
392                     {
393                         debits => [ $self->outstanding_debits->as_list ]
394                     }
395                 );
396             }
397         }
398     );
399
400     return $credit;
401 }
402
403 =head3 add_debit
404
405 This method allows adding debits to a patron's account
406
407     my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
408         {
409             amount           => $amount,
410             description      => $description,
411             note             => $note,
412             user_id          => $user_id,
413             interface        => $interface,
414             library_id       => $library_id,
415             type             => $debit_type,
416             transaction_type => $transaction_type,
417             cash_register    => $register_id,
418             item_id          => $item_id,
419             issue_id         => $issue_id
420         }
421     );
422
423 $debit_type can be any of:
424   - ACCOUNT
425   - ACCOUNT_RENEW
426   - RESERVE_EXPIRED
427   - LOST
428   - sundry
429   - NEW_CARD
430   - OVERDUE
431   - PROCESSING
432   - RENT
433   - RENT_DAILY
434   - RENT_RENEW
435   - RENT_DAILY_RENEW
436   - RESERVE
437   - PAYOUT
438
439 =cut
440
441 sub add_debit {
442
443     my ( $self, $params ) = @_;
444
445     # check for mandatory params
446     my @mandatory = ( 'interface', 'type', 'amount' );
447     for my $param (@mandatory) {
448         unless ( defined( $params->{$param} ) ) {
449             Koha::Exceptions::MissingParameter->throw(
450                 error => "The $param parameter is mandatory" );
451         }
452     }
453
454     # check for cash register if using cash
455     Koha::Exceptions::Account::RegisterRequired->throw()
456       if ( C4::Context->preference("UseCashRegisters")
457         && defined( $params->{transaction_type} )
458         && ( $params->{transaction_type} eq 'CASH' || $params->{payment_type} eq 'SIP00' )
459         && !defined( $params->{cash_register} ) );
460
461     # amount should always be a positive value
462     my $amount = $params->{amount};
463     unless ( $amount > 0 ) {
464         Koha::Exceptions::Account::AmountNotPositive->throw(
465             error => 'Debit amount passed is not positive' );
466     }
467
468     my $description      = $params->{description} // q{};
469     my $note             = $params->{note} // q{};
470     my $user_id          = $params->{user_id};
471     my $interface        = $params->{interface};
472     my $library_id       = $params->{library_id};
473     my $cash_register    = $params->{cash_register};
474     my $debit_type       = $params->{type};
475     my $transaction_type = $params->{transaction_type};
476     my $item_id          = $params->{item_id};
477     my $issue_id         = $params->{issue_id};
478
479     my $line;
480     my $schema = Koha::Database->new->schema;
481     try {
482         $schema->txn_do(
483             sub {
484
485                 # Insert the account line
486                 $line = Koha::Account::Line->new(
487                     {
488                         borrowernumber    => $self->{patron_id},
489                         date              => \'NOW()',
490                         amount            => $amount,
491                         description       => $description,
492                         debit_type_code   => $debit_type,
493                         amountoutstanding => $amount,
494                         payment_type      => $transaction_type,
495                         note              => $note,
496                         manager_id        => $user_id,
497                         interface         => $interface,
498                         itemnumber        => $item_id,
499                         issue_id          => $issue_id,
500                         branchcode        => $library_id,
501                         register_id       => $cash_register,
502                         (
503                             $debit_type eq 'OVERDUE'
504                             ? ( status => 'UNRETURNED' )
505                             : ()
506                         ),
507                     }
508                 )->store();
509
510                 # Record the account offset
511                 my $account_offset = Koha::Account::Offset->new(
512                     {
513                         debit_id => $line->id,
514                         type     => 'CREATE',
515                         amount   => $amount
516                     }
517                 )->store();
518
519                 if ( C4::Context->preference("FinesLog") ) {
520                     logaction(
521                         "FINES", 'CREATE',
522                         $self->{patron_id},
523                         Dumper(
524                             {
525                                 action            => "create_$debit_type",
526                                 borrowernumber    => $self->{patron_id},
527                                 amount            => $amount,
528                                 description       => $description,
529                                 amountoutstanding => $amount,
530                                 debit_type_code   => $debit_type,
531                                 note              => $note,
532                                 itemnumber        => $item_id,
533                                 manager_id        => $user_id,
534                             }
535                         ),
536                         $interface
537                     );
538                 }
539             }
540         );
541     }
542     catch {
543         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
544             if ( $_->broken_fk eq 'debit_type_code' ) {
545                 Koha::Exceptions::Account::UnrecognisedType->throw(
546                     error => 'Type of debit not recognised' );
547             }
548             else {
549                 $_->rethrow;
550             }
551         }
552     };
553
554     return $line;
555 }
556
557 =head3 payout_amount
558
559     my $debit = $account->payout_amount(
560         {
561             payout_type => $payout_type,
562             register_id => $register_id,
563             staff_id    => $staff_id,
564             interface   => 'intranet',
565             amount      => $amount,
566             credits     => $credit_lines
567         }
568     );
569
570 This method allows an amount to be paid out from a patrons account against outstanding credits.
571
572 $payout_type can be any of the defined payment_types:
573
574 =cut
575
576 sub payout_amount {
577     my ( $self, $params ) = @_;
578
579     # Check for mandatory parameters
580     my @mandatory =
581       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
582     for my $param (@mandatory) {
583         unless ( defined( $params->{$param} ) ) {
584             Koha::Exceptions::MissingParameter->throw(
585                 error => "The $param parameter is mandatory" );
586         }
587     }
588
589     # Check for mandatory register
590     Koha::Exceptions::Account::RegisterRequired->throw()
591       if ( C4::Context->preference("UseCashRegisters")
592         && ( $params->{payout_type} eq 'CASH' || $params->{payout_type} eq 'SIP00' )
593         && !defined($params->{cash_register}) );
594
595     # Amount should always be passed as a positive value
596     my $amount = $params->{amount};
597     unless ( $amount > 0 ) {
598         Koha::Exceptions::Account::AmountNotPositive->throw(
599             error => 'Payout amount passed is not positive' );
600     }
601
602     # Amount should always be less than or equal to outstanding credit
603     my $outstanding = 0;
604     my $outstanding_credits =
605       exists( $params->{credits} )
606       ? $params->{credits}
607       : $self->outstanding_credits->as_list;
608     for my $credit ( @{$outstanding_credits} ) {
609         $outstanding += $credit->amountoutstanding;
610     }
611     $outstanding = $outstanding * -1;
612     Koha::Exceptions::ParameterTooHigh->throw( error =>
613 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
614     ) unless ( $outstanding >= $amount );
615
616     my $payout;
617     my $schema = Koha::Database->new->schema;
618     $schema->txn_do(
619         sub {
620
621             # A 'payout' is a 'debit'
622             $payout = $self->add_debit(
623                 {
624                     amount            => $params->{amount},
625                     type              => 'PAYOUT',
626                     transaction_type  => $params->{payout_type},
627                     amountoutstanding => $params->{amount},
628                     user_id           => $params->{staff_id},
629                     interface         => $params->{interface},
630                     branchcode        => $params->{branch},
631                     cash_register     => $params->{cash_register}
632                 }
633             );
634
635             # Offset against credits
636             for my $credit ( @{$outstanding_credits} ) {
637                 $credit->apply( { debits => [$payout] } );
638                 $payout->discard_changes;
639                 last if $payout->amountoutstanding == 0;
640             }
641
642             # Set payout as paid
643             $payout->status('PAID')->store;
644         }
645     );
646
647     return $payout;
648 }
649
650 =head3 balance
651
652 my $balance = $self->balance
653
654 Return the balance (sum of amountoutstanding columns)
655
656 =cut
657
658 sub balance {
659     my ($self) = @_;
660     return $self->lines->total_outstanding;
661 }
662
663 =head3 outstanding_debits
664
665 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
666
667 It returns the debit lines with outstanding amounts for the patron.
668
669 It returns a Koha::Account::Lines iterator.
670
671 =cut
672
673 sub outstanding_debits {
674     my ($self) = @_;
675
676     return $self->lines->search(
677         {
678             amount            => { '>' => 0 },
679             amountoutstanding => { '>' => 0 }
680         }
681     );
682 }
683
684 =head3 outstanding_credits
685
686 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
687
688 It returns the credit lines with outstanding amounts for the patron.
689
690 It returns a Koha::Account::Lines iterator.
691
692 =cut
693
694 sub outstanding_credits {
695     my ($self) = @_;
696
697     return $self->lines->search(
698         {
699             amount            => { '<' => 0 },
700             amountoutstanding => { '<' => 0 }
701         }
702     );
703 }
704
705 =head3 non_issues_charges
706
707 my $non_issues_charges = $self->non_issues_charges
708
709 Calculates amount immediately owing by the patron - non-issue charges.
710
711 Charges can be set as exempt from non-issue by editing the debit type in the Debit Types area of System Preferences.
712
713 =cut
714
715 sub non_issues_charges {
716     my ($self) = @_;
717
718     my @blocking_debit_types = Koha::Account::DebitTypes->search({ restricts_checkouts => 1 }, { columns => 'code' })->get_column('code');
719
720     return $self->lines->search(
721         {
722             debit_type_code => { -in => \@blocking_debit_types }
723         },
724     )->total_outstanding;
725 }
726
727 =head3 lines
728
729 my $lines = $self->lines;
730
731 Return all credits and debits for the user, outstanding or otherwise
732
733 =cut
734
735 sub lines {
736     my ($self) = @_;
737
738     return Koha::Account::Lines->search(
739         {
740             borrowernumber => $self->{patron_id},
741         }
742     );
743 }
744
745
746 =head3 credits
747
748   my $credits = $self->credits;
749
750 Return all credits for the user
751
752 =cut
753
754 sub credits {
755     my ($self) = @_;
756
757     return Koha::Account::Credits->search(
758         {
759             borrowernumber => $self->{patron_id}
760         }
761     );
762 }
763
764 =head3 debits
765
766   my $debits = $self->debits;
767
768 Return all debits for the user
769
770 =cut
771
772 sub debits {
773     my ($self) = @_;
774
775     return Koha::Account::Debits->search(
776         {
777             borrowernumber   => $self->{patron_id},
778         }
779     );
780 }
781
782 =head3 reconcile_balance
783
784 $account->reconcile_balance();
785
786 Find outstanding credits and use them to pay outstanding debits.
787 Currently, this implicitly uses the 'First In First Out' rule for
788 applying credits against debits.
789
790 =cut
791
792 sub reconcile_balance {
793     my ($self) = @_;
794
795     my $outstanding_debits  = $self->outstanding_debits;
796     my $outstanding_credits = $self->outstanding_credits;
797
798     while (     $outstanding_debits->total_outstanding > 0
799             and my $credit = $outstanding_credits->next )
800     {
801         # there's both outstanding debits and credits
802         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
803
804         $outstanding_debits = $self->outstanding_debits;
805
806     }
807
808     return $self;
809 }
810
811 1;
812
813 =head1 AUTHORS
814
815 =encoding utf8
816
817 Kyle M Hall <kyle.m.hall@gmail.com>
818 Tomás Cohen Arazi <tomascohen@gmail.com>
819 Martin Renvoize <martin.renvoize@ptfs-europe.com>
820
821 =cut