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