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