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