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