Bug 22435: Use 'CREATE' offset type in Koha::Account
[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->debit_offsets;
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->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' )
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
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' )
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                         offset_type => $Koha::Account::offset_type->{$params->{type}}
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                         offset_type => $Koha::Account::offset_type->{$params->{type}}
377                     }
378                 );
379             }
380         }
381     );
382
383     return $credit;
384 }
385
386 =head3 add_debit
387
388 This method allows adding debits to a patron's account
389
390     my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
391         {
392             amount           => $amount,
393             description      => $description,
394             note             => $note,
395             user_id          => $user_id,
396             interface        => $interface,
397             library_id       => $library_id,
398             type             => $debit_type,
399             transaction_type => $transaction_type,
400             cash_register    => $register_id,
401             item_id          => $item_id,
402             issue_id         => $issue_id
403         }
404     );
405
406 $debit_type can be any of:
407   - ACCOUNT
408   - ACCOUNT_RENEW
409   - RESERVE_EXPIRED
410   - LOST
411   - sundry
412   - NEW_CARD
413   - OVERDUE
414   - PROCESSING
415   - RENT
416   - RENT_DAILY
417   - RENT_RENEW
418   - RENT_DAILY_RENEW
419   - RESERVE
420   - PAYOUT
421
422 =cut
423
424 sub add_debit {
425
426     my ( $self, $params ) = @_;
427
428     # check for mandatory params
429     my @mandatory = ( 'interface', 'type', 'amount' );
430     for my $param (@mandatory) {
431         unless ( defined( $params->{$param} ) ) {
432             Koha::Exceptions::MissingParameter->throw(
433                 error => "The $param parameter is mandatory" );
434         }
435     }
436
437     # check for cash register if using cash
438     Koha::Exceptions::Account::RegisterRequired->throw()
439       if ( C4::Context->preference("UseCashRegisters")
440         && defined( $params->{transaction_type} )
441         && ( $params->{transaction_type} eq 'CASH' )
442         && !defined( $params->{cash_register} ) );
443
444     # amount should always be a positive value
445     my $amount = $params->{amount};
446     unless ( $amount > 0 ) {
447         Koha::Exceptions::Account::AmountNotPositive->throw(
448             error => 'Debit amount passed is not positive' );
449     }
450
451     my $description      = $params->{description} // q{};
452     my $note             = $params->{note} // q{};
453     my $user_id          = $params->{user_id};
454     my $interface        = $params->{interface};
455     my $library_id       = $params->{library_id};
456     my $cash_register    = $params->{cash_register};
457     my $debit_type       = $params->{type};
458     my $transaction_type = $params->{transaction_type};
459     my $item_id          = $params->{item_id};
460     my $issue_id         = $params->{issue_id};
461
462     my $line;
463     my $schema = Koha::Database->new->schema;
464     try {
465         $schema->txn_do(
466             sub {
467
468                 # Insert the account line
469                 $line = Koha::Account::Line->new(
470                     {
471                         borrowernumber    => $self->{patron_id},
472                         date              => \'NOW()',
473                         amount            => $amount,
474                         description       => $description,
475                         debit_type_code   => $debit_type,
476                         amountoutstanding => $amount,
477                         payment_type      => $transaction_type,
478                         note              => $note,
479                         manager_id        => $user_id,
480                         interface         => $interface,
481                         itemnumber        => $item_id,
482                         issue_id          => $issue_id,
483                         branchcode        => $library_id,
484                         register_id       => $cash_register,
485                         (
486                             $debit_type eq 'OVERDUE'
487                             ? ( status => 'UNRETURNED' )
488                             : ()
489                         ),
490                     }
491                 )->store();
492
493                 # Record the account offset
494                 my $account_offset = Koha::Account::Offset->new(
495                     {
496                         debit_id => $line->id,
497                         type     => 'CREATE',
498                         amount   => $amount
499                     }
500                 )->store();
501
502                 if ( C4::Context->preference("FinesLog") ) {
503                     logaction(
504                         "FINES", 'CREATE',
505                         $self->{patron_id},
506                         Dumper(
507                             {
508                                 action            => "create_$debit_type",
509                                 borrowernumber    => $self->{patron_id},
510                                 amount            => $amount,
511                                 description       => $description,
512                                 amountoutstanding => $amount,
513                                 debit_type_code   => $debit_type,
514                                 note              => $note,
515                                 itemnumber        => $item_id,
516                                 manager_id        => $user_id,
517                             }
518                         ),
519                         $interface
520                     );
521                 }
522             }
523         );
524     }
525     catch {
526         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
527             if ( $_->broken_fk eq 'debit_type_code' ) {
528                 Koha::Exceptions::Account::UnrecognisedType->throw(
529                     error => 'Type of debit not recognised' );
530             }
531             else {
532                 $_->rethrow;
533             }
534         }
535     };
536
537     return $line;
538 }
539
540 =head3 payout_amount
541
542     my $debit = $account->payout_amount(
543         {
544             payout_type => $payout_type,
545             register_id => $register_id,
546             staff_id    => $staff_id,
547             interface   => 'intranet',
548             amount      => $amount,
549             credits     => $credit_lines
550         }
551     );
552
553 This method allows an amount to be paid out from a patrons account against outstanding credits.
554
555 $payout_type can be any of the defined payment_types:
556
557 =cut
558
559 sub payout_amount {
560     my ( $self, $params ) = @_;
561
562     # Check for mandatory parameters
563     my @mandatory =
564       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
565     for my $param (@mandatory) {
566         unless ( defined( $params->{$param} ) ) {
567             Koha::Exceptions::MissingParameter->throw(
568                 error => "The $param parameter is mandatory" );
569         }
570     }
571
572     # Check for mandatory register
573     Koha::Exceptions::Account::RegisterRequired->throw()
574       if ( C4::Context->preference("UseCashRegisters")
575         && ( $params->{payout_type} eq 'CASH' )
576         && !defined($params->{cash_register}) );
577
578     # Amount should always be passed as a positive value
579     my $amount = $params->{amount};
580     unless ( $amount > 0 ) {
581         Koha::Exceptions::Account::AmountNotPositive->throw(
582             error => 'Payout amount passed is not positive' );
583     }
584
585     # Amount should always be less than or equal to outstanding credit
586     my $outstanding = 0;
587     my $outstanding_credits =
588       exists( $params->{credits} )
589       ? $params->{credits}
590       : $self->outstanding_credits->as_list;
591     for my $credit ( @{$outstanding_credits} ) {
592         $outstanding += $credit->amountoutstanding;
593     }
594     $outstanding = $outstanding * -1;
595     Koha::Exceptions::ParameterTooHigh->throw( error =>
596 "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)"
597     ) unless ( $outstanding >= $amount );
598
599     my $payout;
600     my $schema = Koha::Database->new->schema;
601     $schema->txn_do(
602         sub {
603
604             # A 'payout' is a 'debit'
605             $payout = $self->add_debit(
606                 {
607                     amount            => $params->{amount},
608                     type              => 'PAYOUT',
609                     transaction_type  => $params->{payout_type},
610                     amountoutstanding => $params->{amount},
611                     manager_id        => $params->{staff_id},
612                     interface         => $params->{interface},
613                     branchcode        => $params->{branch},
614                     cash_register     => $params->{cash_register}
615                 }
616             );
617
618             # Offset against credits
619             for my $credit ( @{$outstanding_credits} ) {
620                 $credit->apply(
621                     { debits => [$payout], offset_type => 'PAYOUT' } );
622                 $payout->discard_changes;
623                 last if $payout->amountoutstanding == 0;
624             }
625
626             # Set payout as paid
627             $payout->status('PAID')->store;
628         }
629     );
630
631     return $payout;
632 }
633
634 =head3 balance
635
636 my $balance = $self->balance
637
638 Return the balance (sum of amountoutstanding columns)
639
640 =cut
641
642 sub balance {
643     my ($self) = @_;
644     return $self->lines->total_outstanding;
645 }
646
647 =head3 outstanding_debits
648
649 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
650
651 It returns the debit lines with outstanding amounts for the patron.
652
653 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
654 return a list of Koha::Account::Line objects.
655
656 =cut
657
658 sub outstanding_debits {
659     my ($self) = @_;
660
661     return $self->lines->search(
662         {
663             amount            => { '>' => 0 },
664             amountoutstanding => { '>' => 0 }
665         }
666     );
667 }
668
669 =head3 outstanding_credits
670
671 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
672
673 It returns the credit lines with outstanding amounts for the patron.
674
675 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
676 return a list of Koha::Account::Line objects.
677
678 =cut
679
680 sub outstanding_credits {
681     my ($self) = @_;
682
683     return $self->lines->search(
684         {
685             amount            => { '<' => 0 },
686             amountoutstanding => { '<' => 0 }
687         }
688     );
689 }
690
691 =head3 non_issues_charges
692
693 my $non_issues_charges = $self->non_issues_charges
694
695 Calculates amount immediately owing by the patron - non-issue charges.
696
697 Charges exempt from non-issue are:
698 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
699 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
700 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
701
702 =cut
703
704 sub non_issues_charges {
705     my ($self) = @_;
706
707     #NOTE: With bug 23049 these preferences could be moved to being attached
708     #to individual debit types to give more flexability and specificity.
709     my @not_fines;
710     push @not_fines, 'RESERVE'
711       unless C4::Context->preference('HoldsInNoissuesCharge');
712     push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
713       unless C4::Context->preference('RentalsInNoissuesCharge');
714     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
715         my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
716         push @not_fines, @man_inv;
717     }
718
719     return $self->lines->search(
720         {
721             debit_type_code => { -not_in => \@not_fines }
722         },
723     )->total_outstanding;
724 }
725
726 =head3 lines
727
728 my $lines = $self->lines;
729
730 Return all credits and debits for the user, outstanding or otherwise
731
732 =cut
733
734 sub lines {
735     my ($self) = @_;
736
737     return Koha::Account::Lines->search(
738         {
739             borrowernumber => $self->{patron_id},
740         }
741     );
742 }
743
744 =head3 reconcile_balance
745
746 $account->reconcile_balance();
747
748 Find outstanding credits and use them to pay outstanding debits.
749 Currently, this implicitly uses the 'First In First Out' rule for
750 applying credits against debits.
751
752 =cut
753
754 sub reconcile_balance {
755     my ($self) = @_;
756
757     my $outstanding_debits  = $self->outstanding_debits;
758     my $outstanding_credits = $self->outstanding_credits;
759
760     while (     $outstanding_debits->total_outstanding > 0
761             and my $credit = $outstanding_credits->next )
762     {
763         # there's both outstanding debits and credits
764         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
765
766         $outstanding_debits = $self->outstanding_debits;
767
768     }
769
770     return $self;
771 }
772
773 1;
774
775 =head1 AUTHORS
776
777 =encoding utf8
778
779 Kyle M Hall <kyle.m.hall@gmail.com>
780 Tomás Cohen Arazi <tomascohen@gmail.com>
781 Martin Renvoize <martin.renvoize@ptfs-europe.com>
782
783 =cut