Bug 29407: Make the pickup locations dropdown JS reusable
[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->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 * -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' )
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' )
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' )
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 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
651 return a list of Koha::Account::Line objects.
652
653 =cut
654
655 sub outstanding_debits {
656     my ($self) = @_;
657
658     return $self->lines->search(
659         {
660             amount            => { '>' => 0 },
661             amountoutstanding => { '>' => 0 }
662         }
663     );
664 }
665
666 =head3 outstanding_credits
667
668 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
669
670 It returns the credit lines with outstanding amounts for the patron.
671
672 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
673 return a list of Koha::Account::Line objects.
674
675 =cut
676
677 sub outstanding_credits {
678     my ($self) = @_;
679
680     return $self->lines->search(
681         {
682             amount            => { '<' => 0 },
683             amountoutstanding => { '<' => 0 }
684         }
685     );
686 }
687
688 =head3 non_issues_charges
689
690 my $non_issues_charges = $self->non_issues_charges
691
692 Calculates amount immediately owing by the patron - non-issue charges.
693
694 Charges exempt from non-issue are:
695 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
696 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
697 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
698
699 =cut
700
701 sub non_issues_charges {
702     my ($self) = @_;
703
704     #NOTE: With bug 23049 these preferences could be moved to being attached
705     #to individual debit types to give more flexability and specificity.
706     my @not_fines;
707     push @not_fines, 'RESERVE'
708       unless C4::Context->preference('HoldsInNoissuesCharge');
709     push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
710       unless C4::Context->preference('RentalsInNoissuesCharge');
711     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
712         my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
713         push @not_fines, @man_inv;
714     }
715
716     return $self->lines->search(
717         {
718             debit_type_code => { -not_in => \@not_fines }
719         },
720     )->total_outstanding;
721 }
722
723 =head3 lines
724
725 my $lines = $self->lines;
726
727 Return all credits and debits for the user, outstanding or otherwise
728
729 =cut
730
731 sub lines {
732     my ($self) = @_;
733
734     return Koha::Account::Lines->search(
735         {
736             borrowernumber => $self->{patron_id},
737         }
738     );
739 }
740
741 =head3 reconcile_balance
742
743 $account->reconcile_balance();
744
745 Find outstanding credits and use them to pay outstanding debits.
746 Currently, this implicitly uses the 'First In First Out' rule for
747 applying credits against debits.
748
749 =cut
750
751 sub reconcile_balance {
752     my ($self) = @_;
753
754     my $outstanding_debits  = $self->outstanding_debits;
755     my $outstanding_credits = $self->outstanding_credits;
756
757     while (     $outstanding_debits->total_outstanding > 0
758             and my $credit = $outstanding_credits->next )
759     {
760         # there's both outstanding debits and credits
761         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
762
763         $outstanding_debits = $self->outstanding_debits;
764
765     }
766
767     return $self;
768 }
769
770 1;
771
772 =head1 AUTHORS
773
774 =encoding utf8
775
776 Kyle M Hall <kyle.m.hall@gmail.com>
777 Tomás Cohen Arazi <tomascohen@gmail.com>
778 Martin Renvoize <martin.renvoize@ptfs-europe.com>
779
780 =cut