Bug 23321: Add cash register support to paycollect
[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;
24 use List::MoreUtils qw( uniq );
25
26 use C4::Circulation qw( ReturnLostItem );
27 use C4::Letters;
28 use C4::Log qw( logaction );
29 use C4::Stats qw( UpdateStats );
30
31 use Koha::Patrons;
32 use Koha::Account::Lines;
33 use Koha::Account::Offsets;
34 use Koha::DateUtils qw( dt_from_string );
35 use Koha::Exceptions;
36 use Koha::Exceptions::Account;
37
38 =head1 NAME
39
40 Koha::Accounts - Module for managing payments and fees for patrons
41
42 =cut
43
44 sub new {
45     my ( $class, $params ) = @_;
46
47     Carp::croak("No patron id passed in!") unless $params->{patron_id};
48
49     return bless( $params, $class );
50 }
51
52 =head2 pay
53
54 This method allows payments to be made against fees/fines
55
56 Koha::Account->new( { patron_id => $borrowernumber } )->pay(
57     {
58         amount      => $amount,
59         note        => $note,
60         description => $description,
61         library_id  => $branchcode,
62         lines        => $lines, # Arrayref of Koha::Account::Line objects to pay
63         account_type => $type,  # accounttype code
64         offset_type => $offset_type,    # offset type code
65     }
66 );
67
68 =cut
69
70 sub pay {
71     my ( $self, $params ) = @_;
72
73     my $amount        = $params->{amount};
74     my $description   = $params->{description};
75     my $note          = $params->{note} || q{};
76     my $library_id    = $params->{library_id};
77     my $lines         = $params->{lines};
78     my $type          = $params->{type} || 'payment';
79     my $payment_type  = $params->{payment_type} || undef;
80     my $account_type  = $params->{account_type};
81     my $offset_type   = $params->{offset_type} || $type eq 'writeoff' ? 'Writeoff' : 'Payment';
82     my $cash_register = $params->{cash_register};
83
84     my $userenv = C4::Context->userenv;
85
86     my $patron = Koha::Patrons->find( $self->{patron_id} );
87
88     my $manager_id = $userenv ? $userenv->{number} : 0;
89     my $interface = $params ? ( $params->{interface} || C4::Context->interface ) : C4::Context->interface;
90     Koha::Exceptions::Account::RegisterRequired->throw()
91       if ( C4::Context->preference("UseCashRegisters")
92         && !defined($cash_register)
93         && ( $interface ne 'opac' ) );
94
95     my @fines_paid; # List of account lines paid on with this payment
96
97     my $balance_remaining = $amount; # Set it now so we can adjust the amount if necessary
98     $balance_remaining ||= 0;
99
100     my @account_offsets;
101
102     # We were passed a specific line to pay
103     foreach my $fine ( @$lines ) {
104         my $amount_to_pay =
105             $fine->amountoutstanding > $balance_remaining
106           ? $balance_remaining
107           : $fine->amountoutstanding;
108
109         my $old_amountoutstanding = $fine->amountoutstanding;
110         my $new_amountoutstanding = $old_amountoutstanding - $amount_to_pay;
111         $fine->amountoutstanding($new_amountoutstanding)->store();
112         $balance_remaining = $balance_remaining - $amount_to_pay;
113
114         # Same logic exists in Koha::Account::Line::apply
115         if (   $new_amountoutstanding == 0
116             && $fine->itemnumber
117             && $fine->accounttype
118             && ( $fine->accounttype eq 'LOST' ) )
119         {
120             C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
121         }
122
123         my $account_offset = Koha::Account::Offset->new(
124             {
125                 debit_id => $fine->id,
126                 type     => $offset_type,
127                 amount   => $amount_to_pay * -1,
128             }
129         );
130         push( @account_offsets, $account_offset );
131
132         if ( C4::Context->preference("FinesLog") ) {
133             logaction(
134                 "FINES", 'MODIFY',
135                 $self->{patron_id},
136                 Dumper(
137                     {
138                         action                => 'fee_payment',
139                         borrowernumber        => $fine->borrowernumber,
140                         old_amountoutstanding => $old_amountoutstanding,
141                         new_amountoutstanding => 0,
142                         amount_paid           => $old_amountoutstanding,
143                         accountlines_id       => $fine->id,
144                         manager_id            => $manager_id,
145                         note                  => $note,
146                     }
147                 ),
148                 $interface
149             );
150             push( @fines_paid, $fine->id );
151         }
152     }
153
154     # Were not passed a specific line to pay, or the payment was for more
155     # than the what was owed on the given line. In that case pay down other
156     # lines with remaining balance.
157     my @outstanding_fines;
158     @outstanding_fines = $self->lines->search(
159         {
160             amountoutstanding => { '>' => 0 },
161         }
162     ) if $balance_remaining > 0;
163
164     foreach my $fine (@outstanding_fines) {
165         my $amount_to_pay =
166             $fine->amountoutstanding > $balance_remaining
167           ? $balance_remaining
168           : $fine->amountoutstanding;
169
170         my $old_amountoutstanding = $fine->amountoutstanding;
171         $fine->amountoutstanding( $old_amountoutstanding - $amount_to_pay );
172         $fine->store();
173
174         if (   $fine->amountoutstanding == 0
175             && $fine->itemnumber
176             && $fine->accounttype
177             && ( $fine->accounttype eq 'LOST' ) )
178         {
179             C4::Circulation::ReturnLostItem( $self->{patron_id}, $fine->itemnumber );
180         }
181
182         my $account_offset = Koha::Account::Offset->new(
183             {
184                 debit_id => $fine->id,
185                 type     => $offset_type,
186                 amount   => $amount_to_pay * -1,
187             }
188         );
189         push( @account_offsets, $account_offset );
190
191         if ( C4::Context->preference("FinesLog") ) {
192             logaction(
193                 "FINES", 'MODIFY',
194                 $self->{patron_id},
195                 Dumper(
196                     {
197                         action                => "fee_$type",
198                         borrowernumber        => $fine->borrowernumber,
199                         old_amountoutstanding => $old_amountoutstanding,
200                         new_amountoutstanding => $fine->amountoutstanding,
201                         amount_paid           => $amount_to_pay,
202                         accountlines_id       => $fine->id,
203                         manager_id            => $manager_id,
204                         note                  => $note,
205                     }
206                 ),
207                 $interface
208             );
209             push( @fines_paid, $fine->id );
210         }
211
212         $balance_remaining = $balance_remaining - $amount_to_pay;
213         last unless $balance_remaining > 0;
214     }
215
216     $account_type ||=
217       $type eq 'writeoff'
218       ? 'W'
219       : 'Pay';
220
221     $description ||= $type eq 'writeoff' ? 'Writeoff' : q{};
222
223     my $payment = Koha::Account::Line->new(
224         {
225             borrowernumber    => $self->{patron_id},
226             date              => dt_from_string(),
227             amount            => 0 - $amount,
228             description       => $description,
229             accounttype       => $account_type,
230             payment_type      => $payment_type,
231             amountoutstanding => 0 - $balance_remaining,
232             manager_id        => $manager_id,
233             interface         => $interface,
234             branchcode        => $library_id,
235             register_id       => $cash_register,
236             note              => $note,
237         }
238     )->store();
239
240     foreach my $o ( @account_offsets ) {
241         $o->credit_id( $payment->id() );
242         $o->store();
243     }
244
245     UpdateStats(
246         {
247             branch         => $library_id,
248             type           => $type,
249             amount         => $amount,
250             borrowernumber => $self->{patron_id},
251         }
252     );
253
254     if ( C4::Context->preference("FinesLog") ) {
255         logaction(
256             "FINES", 'CREATE',
257             $self->{patron_id},
258             Dumper(
259                 {
260                     action            => "create_$type",
261                     borrowernumber    => $self->{patron_id},
262                     amount            => 0 - $amount,
263                     amountoutstanding => 0 - $balance_remaining,
264                     accounttype       => $account_type,
265                     accountlines_paid => \@fines_paid,
266                     manager_id        => $manager_id,
267                 }
268             ),
269             $interface
270         );
271     }
272
273     if ( C4::Context->preference('UseEmailReceipts') ) {
274         if (
275             my $letter = C4::Letters::GetPreparedLetter(
276                 module                 => 'circulation',
277                 letter_code            => uc("ACCOUNT_$type"),
278                 message_transport_type => 'email',
279                 lang    => $patron->lang,
280                 tables => {
281                     borrowers       => $self->{patron_id},
282                     branches        => $self->{library_id},
283                 },
284                 substitute => {
285                     credit => $payment,
286                     offsets => \@account_offsets,
287                 },
288               )
289           )
290         {
291             C4::Letters::EnqueueLetter(
292                 {
293                     letter                 => $letter,
294                     borrowernumber         => $self->{patron_id},
295                     message_transport_type => 'email',
296                 }
297             ) or warn "can't enqueue letter $letter";
298         }
299     }
300
301     return $payment->id;
302 }
303
304 =head3 add_credit
305
306 This method allows adding credits to a patron's account
307
308 my $credit_line = Koha::Account->new({ patron_id => $patron_id })->add_credit(
309     {
310         amount       => $amount,
311         description  => $description,
312         note         => $note,
313         user_id      => $user_id,
314         interface    => $interface,
315         library_id   => $library_id,
316         payment_type => $payment_type,
317         type         => $credit_type,
318         item_id      => $item_id
319     }
320 );
321
322 $credit_type can be any of:
323   - 'credit'
324   - 'payment'
325   - 'forgiven'
326   - 'lost_item_return'
327   - 'writeoff'
328
329 =cut
330
331 sub add_credit {
332
333     my ( $self, $params ) = @_;
334
335     # amount is passed as a positive value, but we store credit as negative values
336     my $amount        = $params->{amount} * -1;
337     my $description   = $params->{description} // q{};
338     my $note          = $params->{note} // q{};
339     my $user_id       = $params->{user_id};
340     my $interface     = $params->{interface};
341     my $library_id    = $params->{library_id};
342     my $cash_register = $params->{cash_register};
343     my $payment_type  = $params->{payment_type};
344     my $type          = $params->{type} || 'payment';
345     my $item_id       = $params->{item_id};
346
347     unless ( $interface ) {
348         Koha::Exceptions::MissingParameter->throw(
349             error => 'The interface parameter is mandatory'
350         );
351     }
352
353     Koha::Exceptions::Account::RegisterRequired->throw()
354       if ( C4::Context->preference("UseCashRegisters")
355         && !defined($cash_register)
356         && ( $payment_type eq 'CASH' ) );
357
358     my $schema = Koha::Database->new->schema;
359
360     my $account_type = $Koha::Account::account_type_credit->{$type};
361     my $line;
362
363     $schema->txn_do(
364         sub {
365
366             # Insert the account line
367             $line = Koha::Account::Line->new(
368                 {   borrowernumber    => $self->{patron_id},
369                     date              => \'NOW()',
370                     amount            => $amount,
371                     description       => $description,
372                     accounttype       => $account_type,
373                     amountoutstanding => $amount,
374                     payment_type      => $payment_type,
375                     note              => $note,
376                     manager_id        => $user_id,
377                     interface         => $interface,
378                     branchcode        => $library_id,
379                     register_id       => $cash_register,
380                     itemnumber        => $item_id,
381                 }
382             )->store();
383
384             # Record the account offset
385             my $account_offset = Koha::Account::Offset->new(
386                 {   credit_id => $line->id,
387                     type      => $Koha::Account::offset_type->{$type},
388                     amount    => $amount
389                 }
390             )->store();
391
392             UpdateStats(
393                 {   branch         => $library_id,
394                     type           => $type,
395                     amount         => $amount,
396                     borrowernumber => $self->{patron_id},
397                 }
398             ) if grep { $type eq $_ } ('payment', 'writeoff') ;
399
400             if ( C4::Context->preference("FinesLog") ) {
401                 logaction(
402                     "FINES", 'CREATE',
403                     $self->{patron_id},
404                     Dumper(
405                         {   action            => "create_$type",
406                             borrowernumber    => $self->{patron_id},
407                             amount            => $amount,
408                             description       => $description,
409                             amountoutstanding => $amount,
410                             accounttype       => $account_type,
411                             note              => $note,
412                             itemnumber        => $item_id,
413                             manager_id        => $user_id,
414                             branchcode        => $library_id,
415                         }
416                     ),
417                     $interface
418                 );
419             }
420         }
421     );
422
423     return $line;
424 }
425
426 =head3 add_debit
427
428 This method allows adding debits to a patron's account
429
430 my $debit_line = Koha::Account->new({ patron_id => $patron_id })->add_debit(
431     {
432         amount       => $amount,
433         description  => $description,
434         note         => $note,
435         user_id      => $user_id,
436         interface    => $interface,
437         library_id   => $library_id,
438         type         => $debit_type,
439         item_id      => $item_id,
440         issue_id     => $issue_id
441     }
442 );
443
444 $debit_type can be any of:
445   - overdue
446   - lost_item
447   - new_card
448   - account
449   - account_renew
450   - sundry
451   - processing
452   - rent
453   - rent_daily
454   - rent_renewal
455   - rent_daily_renewal
456   - reserve
457   - manual
458
459 =cut
460
461 sub add_debit {
462
463     my ( $self, $params ) = @_;
464
465     # amount should always be a positive value
466     my $amount       = $params->{amount};
467
468     unless ( $amount > 0 ) {
469         Koha::Exceptions::Account::AmountNotPositive->throw(
470             error => 'Debit amount passed is not positive'
471         );
472     }
473
474     my $description  = $params->{description} // q{};
475     my $note         = $params->{note} // q{};
476     my $user_id      = $params->{user_id};
477     my $interface    = $params->{interface};
478     my $library_id   = $params->{library_id};
479     my $type         = $params->{type};
480     my $item_id      = $params->{item_id};
481     my $issue_id     = $params->{issue_id};
482
483     unless ( $interface ) {
484         Koha::Exceptions::MissingParameter->throw(
485             error => 'The interface parameter is mandatory'
486         );
487     }
488
489     my $schema = Koha::Database->new->schema;
490
491     unless ( exists($Koha::Account::account_type_debit->{$type}) ) {
492         Koha::Exceptions::Account::UnrecognisedType->throw(
493             error => 'Type of debit not recognised'
494         );
495     }
496
497     my $account_type = $Koha::Account::account_type_debit->{$type};
498
499     my $line;
500
501     $schema->txn_do(
502         sub {
503
504             # Insert the account line
505             $line = Koha::Account::Line->new(
506                 {   borrowernumber    => $self->{patron_id},
507                     date              => \'NOW()',
508                     amount            => $amount,
509                     description       => $description,
510                     accounttype       => $account_type,
511                     amountoutstanding => $amount,
512                     payment_type      => undef,
513                     note              => $note,
514                     manager_id        => $user_id,
515                     interface         => $interface,
516                     itemnumber        => $item_id,
517                     issue_id          => $issue_id,
518                     branchcode        => $library_id,
519                     ( $type eq 'overdue' ? ( status => 'UNRETURNED' ) : ()),
520                 }
521             )->store();
522
523             # Record the account offset
524             my $account_offset = Koha::Account::Offset->new(
525                 {   debit_id => $line->id,
526                     type      => $Koha::Account::offset_type->{$type},
527                     amount    => $amount
528                 }
529             )->store();
530
531             if ( C4::Context->preference("FinesLog") ) {
532                 logaction(
533                     "FINES", 'CREATE',
534                     $self->{patron_id},
535                     Dumper(
536                         {   action            => "create_$type",
537                             borrowernumber    => $self->{patron_id},
538                             amount            => $amount,
539                             description       => $description,
540                             amountoutstanding => $amount,
541                             accounttype       => $account_type,
542                             note              => $note,
543                             itemnumber        => $item_id,
544                             manager_id        => $user_id,
545                         }
546                     ),
547                     $interface
548                 );
549             }
550         }
551     );
552
553     return $line;
554 }
555
556 =head3 balance
557
558 my $balance = $self->balance
559
560 Return the balance (sum of amountoutstanding columns)
561
562 =cut
563
564 sub balance {
565     my ($self) = @_;
566     return $self->lines->total_outstanding;
567 }
568
569 =head3 outstanding_debits
570
571 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
572
573 It returns the debit lines with outstanding amounts for the patron.
574
575 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
576 return a list of Koha::Account::Line objects.
577
578 =cut
579
580 sub outstanding_debits {
581     my ($self) = @_;
582
583     return $self->lines->search(
584         {
585             amount            => { '>' => 0 },
586             amountoutstanding => { '>' => 0 }
587         }
588     );
589 }
590
591 =head3 outstanding_credits
592
593 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
594
595 It returns the credit lines with outstanding amounts for the patron.
596
597 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
598 return a list of Koha::Account::Line objects.
599
600 =cut
601
602 sub outstanding_credits {
603     my ($self) = @_;
604
605     return $self->lines->search(
606         {
607             amount            => { '<' => 0 },
608             amountoutstanding => { '<' => 0 }
609         }
610     );
611 }
612
613 =head3 non_issues_charges
614
615 my $non_issues_charges = $self->non_issues_charges
616
617 Calculates amount immediately owing by the patron - non-issue charges.
618
619 Charges exempt from non-issue are:
620 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
621 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
622 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
623
624 =cut
625
626 sub non_issues_charges {
627     my ($self) = @_;
628
629     # FIXME REMOVE And add a warning in the about page + update DB if length(MANUAL_INV) > 5
630     my $ACCOUNT_TYPE_LENGTH = 5;    # this is plain ridiculous...
631
632     my @not_fines;
633     push @not_fines, 'Res'
634       unless C4::Context->preference('HoldsInNoissuesCharge');
635     push @not_fines, 'Rent'
636       unless C4::Context->preference('RentalsInNoissuesCharge');
637     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
638         my $dbh = C4::Context->dbh;
639         push @not_fines,
640           @{
641             $dbh->selectcol_arrayref(q|
642                 SELECT authorised_value FROM authorised_values WHERE category = 'MANUAL_INV'
643             |)
644           };
645     }
646     @not_fines = map { substr( $_, 0, $ACCOUNT_TYPE_LENGTH ) } uniq(@not_fines);
647
648     return $self->lines->search(
649         {
650             accounttype    => { -not_in => \@not_fines }
651         },
652     )->total_outstanding;
653 }
654
655 =head3 lines
656
657 my $lines = $self->lines;
658
659 Return all credits and debits for the user, outstanding or otherwise
660
661 =cut
662
663 sub lines {
664     my ($self) = @_;
665
666     return Koha::Account::Lines->search(
667         {
668             borrowernumber => $self->{patron_id},
669         }
670     );
671 }
672
673 =head3 reconcile_balance
674
675 $account->reconcile_balance();
676
677 Find outstanding credits and use them to pay outstanding debits.
678 Currently, this implicitly uses the 'First In First Out' rule for
679 applying credits against debits.
680
681 =cut
682
683 sub reconcile_balance {
684     my ($self) = @_;
685
686     my $outstanding_debits  = $self->outstanding_debits;
687     my $outstanding_credits = $self->outstanding_credits;
688
689     while (     $outstanding_debits->total_outstanding > 0
690             and my $credit = $outstanding_credits->next )
691     {
692         # there's both outstanding debits and credits
693         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
694
695         $outstanding_debits = $self->outstanding_debits;
696
697     }
698
699     return $self;
700 }
701
702 1;
703
704 =head2 Name mappings
705
706 =head3 $offset_type
707
708 =cut
709
710 our $offset_type = {
711     'credit'           => 'Manual Credit',
712     'forgiven'         => 'Writeoff',
713     'lost_item_return' => 'Lost Item',
714     'payment'          => 'Payment',
715     'writeoff'         => 'Writeoff',
716     'account'          => 'Account Fee',
717     'account_renew'    => 'Account Fee',
718     'reserve'          => 'Reserve Fee',
719     'processing'       => 'Processing Fee',
720     'lost_item'        => 'Lost Item',
721     'rent'             => 'Rental Fee',
722     'rent_daily'       => 'Rental Fee',
723     'rent_renew'       => 'Rental Fee',
724     'rent_daily_renew' => 'Rental Fee',
725     'overdue'          => 'OVERDUE',
726     'manual_debit'     => 'Manual Debit',
727     'hold_expired'     => 'Hold Expired'
728 };
729
730 =head3 $account_type_credit
731
732 =cut
733
734 our $account_type_credit = {
735     'credit'           => 'C',
736     'forgiven'         => 'FOR',
737     'lost_item_return' => 'LOST_RETURN',
738     'payment'          => 'Pay',
739     'writeoff'         => 'W'
740 };
741
742 =head3 $account_type_debit
743
744 =cut
745
746 our $account_type_debit = {
747     'account'          => 'ACCOUNT',
748     'account_renew'    => 'ACCOUNT_RENEW',
749     'overdue'          => 'OVERDUE',
750     'lost_item'        => 'LOST',
751     'new_card'         => 'N',
752     'sundry'           => 'M',
753     'processing'       => 'PF',
754     'rent'             => 'RENT',
755     'rent_daily'       => 'RENT_DAILY',
756     'rent_renew'       => 'RENT_RENEW',
757     'rent_daily_renew' => 'RENT_DAILY_RENEW',
758     'reserve'          => 'Res',
759     'manual_debit'     => 'M',
760     'hold_expired'     => 'HE'
761 };
762
763 =head1 AUTHORS
764
765 =encoding utf8
766
767 Kyle M Hall <kyle.m.hall@gmail.com>
768 Tomás Cohen Arazi <tomascohen@gmail.com>
769 Martin Renvoize <martin.renvoize@ptfs-europe.com>
770
771 =cut