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