Bug 27926: (QA follow-up) Switch to using data-order
[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->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->renewable) {
207             my $outcome = $fine->renew_item;
208             push @{$renew_outcomes}, $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         item_id      => $item_id,
502         issue_id     => $issue_id
503     }
504 );
505
506 $debit_type can be any of:
507   - ACCOUNT
508   - ACCOUNT_RENEW
509   - RESERVE_EXPIRED
510   - LOST
511   - sundry
512   - NEW_CARD
513   - OVERDUE
514   - PROCESSING
515   - RENT
516   - RENT_DAILY
517   - RENT_RENEW
518   - RENT_DAILY_RENEW
519   - RESERVE
520
521 =cut
522
523 sub add_debit {
524
525     my ( $self, $params ) = @_;
526
527     # check for mandatory params
528     my @mandatory = ( 'interface', 'type', 'amount' );
529     for my $param (@mandatory) {
530         unless ( defined( $params->{$param} ) ) {
531             Koha::Exceptions::MissingParameter->throw(
532                 error => "The $param parameter is mandatory" );
533         }
534     }
535
536     # amount should always be a positive value
537     my $amount = $params->{amount};
538     unless ( $amount > 0 ) {
539         Koha::Exceptions::Account::AmountNotPositive->throw(
540             error => 'Debit amount passed is not positive' );
541     }
542
543     my $description = $params->{description} // q{};
544     my $note        = $params->{note} // q{};
545     my $user_id     = $params->{user_id};
546     my $interface   = $params->{interface};
547     my $library_id  = $params->{library_id};
548     my $debit_type  = $params->{type};
549     my $item_id     = $params->{item_id};
550     my $issue_id    = $params->{issue_id};
551     my $offset_type = $Koha::Account::offset_type->{$debit_type} // 'Manual Debit';
552
553     my $line;
554     my $schema = Koha::Database->new->schema;
555     try {
556         $schema->txn_do(
557             sub {
558
559                 # Insert the account line
560                 $line = Koha::Account::Line->new(
561                     {
562                         borrowernumber    => $self->{patron_id},
563                         date              => \'NOW()',
564                         amount            => $amount,
565                         description       => $description,
566                         debit_type_code   => $debit_type,
567                         amountoutstanding => $amount,
568                         payment_type      => undef,
569                         note              => $note,
570                         manager_id        => $user_id,
571                         interface         => $interface,
572                         itemnumber        => $item_id,
573                         issue_id          => $issue_id,
574                         branchcode        => $library_id,
575                         (
576                             $debit_type eq 'OVERDUE'
577                             ? ( status => 'UNRETURNED' )
578                             : ()
579                         ),
580                     }
581                 )->store();
582
583                 # Record the account offset
584                 my $account_offset = Koha::Account::Offset->new(
585                     {
586                         debit_id => $line->id,
587                         type     => $offset_type,
588                         amount   => $amount
589                     }
590                 )->store();
591
592                 if ( C4::Context->preference("FinesLog") ) {
593                     logaction(
594                         "FINES", 'CREATE',
595                         $self->{patron_id},
596                         Dumper(
597                             {
598                                 action            => "create_$debit_type",
599                                 borrowernumber    => $self->{patron_id},
600                                 amount            => $amount,
601                                 description       => $description,
602                                 amountoutstanding => $amount,
603                                 debit_type_code   => $debit_type,
604                                 note              => $note,
605                                 itemnumber        => $item_id,
606                                 manager_id        => $user_id,
607                             }
608                         ),
609                         $interface
610                     );
611                 }
612             }
613         );
614     }
615     catch {
616         if ( ref($_) eq 'Koha::Exceptions::Object::FKConstraint' ) {
617             if ( $_->broken_fk eq 'debit_type_code' ) {
618                 Koha::Exceptions::Account::UnrecognisedType->throw(
619                     error => 'Type of debit not recognised' );
620             }
621             else {
622                 $_->rethrow;
623             }
624         }
625     };
626
627     return $line;
628 }
629
630 =head3 balance
631
632 my $balance = $self->balance
633
634 Return the balance (sum of amountoutstanding columns)
635
636 =cut
637
638 sub balance {
639     my ($self) = @_;
640     return $self->lines->total_outstanding;
641 }
642
643 =head3 outstanding_debits
644
645 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_debits;
646
647 It returns the debit lines with outstanding amounts for the patron.
648
649 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
650 return a list of Koha::Account::Line objects.
651
652 =cut
653
654 sub outstanding_debits {
655     my ($self) = @_;
656
657     return $self->lines->search(
658         {
659             amount            => { '>' => 0 },
660             amountoutstanding => { '>' => 0 }
661         }
662     );
663 }
664
665 =head3 outstanding_credits
666
667 my $lines = Koha::Account->new({ patron_id => $patron_id })->outstanding_credits;
668
669 It returns the credit lines with outstanding amounts for the patron.
670
671 In scalar context, it returns a Koha::Account::Lines iterator. In list context, it will
672 return a list of Koha::Account::Line objects.
673
674 =cut
675
676 sub outstanding_credits {
677     my ($self) = @_;
678
679     return $self->lines->search(
680         {
681             amount            => { '<' => 0 },
682             amountoutstanding => { '<' => 0 }
683         }
684     );
685 }
686
687 =head3 non_issues_charges
688
689 my $non_issues_charges = $self->non_issues_charges
690
691 Calculates amount immediately owing by the patron - non-issue charges.
692
693 Charges exempt from non-issue are:
694 * Res (holds) if HoldsInNoissuesCharge syspref is set to false
695 * Rent (rental) if RentalsInNoissuesCharge syspref is set to false
696 * Manual invoices if ManInvInNoissuesCharge syspref is set to false
697
698 =cut
699
700 sub non_issues_charges {
701     my ($self) = @_;
702
703     #NOTE: With bug 23049 these preferences could be moved to being attached
704     #to individual debit types to give more flexability and specificity.
705     my @not_fines;
706     push @not_fines, 'RESERVE'
707       unless C4::Context->preference('HoldsInNoissuesCharge');
708     push @not_fines, ( 'RENT', 'RENT_DAILY', 'RENT_RENEW', 'RENT_DAILY_RENEW' )
709       unless C4::Context->preference('RentalsInNoissuesCharge');
710     unless ( C4::Context->preference('ManInvInNoissuesCharge') ) {
711         my @man_inv = Koha::Account::DebitTypes->search({ is_system => 0 })->get_column('code');
712         push @not_fines, @man_inv;
713     }
714
715     return $self->lines->search(
716         {
717             debit_type_code => { -not_in => \@not_fines }
718         },
719     )->total_outstanding;
720 }
721
722 =head3 lines
723
724 my $lines = $self->lines;
725
726 Return all credits and debits for the user, outstanding or otherwise
727
728 =cut
729
730 sub lines {
731     my ($self) = @_;
732
733     return Koha::Account::Lines->search(
734         {
735             borrowernumber => $self->{patron_id},
736         }
737     );
738 }
739
740 =head3 reconcile_balance
741
742 $account->reconcile_balance();
743
744 Find outstanding credits and use them to pay outstanding debits.
745 Currently, this implicitly uses the 'First In First Out' rule for
746 applying credits against debits.
747
748 =cut
749
750 sub reconcile_balance {
751     my ($self) = @_;
752
753     my $outstanding_debits  = $self->outstanding_debits;
754     my $outstanding_credits = $self->outstanding_credits;
755
756     while (     $outstanding_debits->total_outstanding > 0
757             and my $credit = $outstanding_credits->next )
758     {
759         # there's both outstanding debits and credits
760         $credit->apply( { debits => [ $outstanding_debits->as_list ] } );    # applying credit, no special offset
761
762         $outstanding_debits = $self->outstanding_debits;
763
764     }
765
766     return $self;
767 }
768
769 1;
770
771 =head2 Name mappings
772
773 =head3 $offset_type
774
775 =cut
776
777 our $offset_type = {
778     'CREDIT'           => 'Manual Credit',
779     'FORGIVEN'         => 'Writeoff',
780     'LOST_FOUND'       => 'Lost Item Found',
781     'OVERPAYMENT'      => 'Overpayment',
782     'PAYMENT'          => 'Payment',
783     'WRITEOFF'         => 'Writeoff',
784     'ACCOUNT'          => 'Account Fee',
785     'ACCOUNT_RENEW'    => 'Account Fee',
786     'RESERVE'          => 'Reserve Fee',
787     'PROCESSING'       => 'Processing Fee',
788     'LOST'             => 'Lost Item',
789     'RENT'             => 'Rental Fee',
790     'RENT_DAILY'       => 'Rental Fee',
791     'RENT_RENEW'       => 'Rental Fee',
792     'RENT_DAILY_RENEW' => 'Rental Fee',
793     'OVERDUE'          => 'OVERDUE',
794     'RESERVE_EXPIRED'  => 'Hold Expired'
795 };
796
797 =head1 AUTHORS
798
799 =encoding utf8
800
801 Kyle M Hall <kyle.m.hall@gmail.com>
802 Tomás Cohen Arazi <tomascohen@gmail.com>
803 Martin Renvoize <martin.renvoize@ptfs-europe.com>
804
805 =cut