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