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