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