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