delete invoices, RT#4048
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
8 use Date::Format;
9 use Text::Template 1.20;
10 use File::Temp 0.14;
11 use String::ShellQuote;
12 use HTML::Entities;
13 use Locale::Country;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
18 use FS::cust_main;
19 use FS::cust_statement;
20 use FS::cust_bill_pkg;
21 use FS::cust_bill_pkg_display;
22 use FS::cust_credit;
23 use FS::cust_pay;
24 use FS::cust_pkg;
25 use FS::cust_credit_bill;
26 use FS::pay_batch;
27 use FS::cust_pay_batch;
28 use FS::cust_bill_event;
29 use FS::cust_event;
30 use FS::part_pkg;
31 use FS::cust_bill_pay;
32 use FS::cust_bill_pay_batch;
33 use FS::part_bill_event;
34 use FS::payby;
35
36 @ISA = qw( FS::cust_main_Mixin FS::Record );
37
38 $DEBUG = 0;
39 $me = '[FS::cust_bill]';
40
41 #ask FS::UID to run this stuff for us later
42 FS::UID->install_callback( sub { 
43   $conf = new FS::Conf;
44   $money_char = $conf->config('money_char') || '$';  
45 } );
46
47 =head1 NAME
48
49 FS::cust_bill - Object methods for cust_bill records
50
51 =head1 SYNOPSIS
52
53   use FS::cust_bill;
54
55   $record = new FS::cust_bill \%hash;
56   $record = new FS::cust_bill { 'column' => 'value' };
57
58   $error = $record->insert;
59
60   $error = $new_record->replace($old_record);
61
62   $error = $record->delete;
63
64   $error = $record->check;
65
66   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
67
68   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
69
70   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
71
72   @cust_pay_objects = $cust_bill->cust_pay;
73
74   $tax_amount = $record->tax;
75
76   @lines = $cust_bill->print_text;
77   @lines = $cust_bill->print_text $time;
78
79 =head1 DESCRIPTION
80
81 An FS::cust_bill object represents an invoice; a declaration that a customer
82 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
83 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
84 following fields are currently supported:
85
86 Regular fields
87
88 =over 4
89
90 =item invnum - primary key (assigned automatically for new invoices)
91
92 =item custnum - customer (see L<FS::cust_main>)
93
94 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
95 L<Time::Local> and L<Date::Parse> for conversion functions.
96
97 =item charged - amount of this invoice
98
99 =back
100
101 Deprecated
102
103 =over 4
104
105 =item printed - deprecated
106
107 =back
108
109 Specific use cases
110
111 =over 4
112
113 =item closed - books closed flag, empty or `Y'
114
115 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
116
117 =item agent_invid - legacy invoice number
118
119 =back
120
121 =head1 METHODS
122
123 =over 4
124
125 =item new HASHREF
126
127 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
128 Invoices are normally created by calling the bill method of a customer object
129 (see L<FS::cust_main>).
130
131 =cut
132
133 sub table { 'cust_bill'; }
134
135 sub cust_linked { $_[0]->cust_main_custnum; } 
136 sub cust_unlinked_msg {
137   my $self = shift;
138   "WARNING: can't find cust_main.custnum ". $self->custnum.
139   ' (cust_bill.invnum '. $self->invnum. ')';
140 }
141
142 =item insert
143
144 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
145 returns the error, otherwise returns false.
146
147 =item delete
148
149 This method now works but you probably shouldn't use it.  Instead, apply a
150 credit against the invoice.
151
152 Using this method to delete invoices outright is really, really bad.  There
153 would be no record you ever posted this invoice, and there are no check to
154 make sure charged = 0 or that there are no associated cust_bill_pkg records.
155
156 Really, don't use it.
157
158 =cut
159
160 sub delete {
161   my $self = shift;
162   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
163
164   local $SIG{HUP} = 'IGNORE';
165   local $SIG{INT} = 'IGNORE';
166   local $SIG{QUIT} = 'IGNORE';
167   local $SIG{TERM} = 'IGNORE';
168   local $SIG{TSTP} = 'IGNORE';
169   local $SIG{PIPE} = 'IGNORE';
170
171   my $oldAutoCommit = $FS::UID::AutoCommit;
172   local $FS::UID::AutoCommit = 0;
173   my $dbh = dbh;
174
175   foreach my $table (qw(
176     cust_bill_event
177     cust_event
178     cust_credit_bill
179     cust_bill_pay
180     cust_bill_pay
181     cust_credit_bill
182     cust_pay_batch
183     cust_bill_pay_batch
184     cust_bill_pkg
185   )) {
186
187     foreach my $linked ( $self->$table() ) {
188       my $error = $linked->delete;
189       if ( $error ) {
190         $dbh->rollback if $oldAutoCommit;
191         return $error;
192       }
193     }
194
195   }
196
197   my $error = $self->SUPER::delete(@_);
198   if ( $error ) {
199     $dbh->rollback if $oldAutoCommit;
200     return $error;
201   }
202
203   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
204
205   '';
206
207 }
208
209 =item replace OLD_RECORD
210
211 Replaces the OLD_RECORD with this one in the database.  If there is an error,
212 returns the error, otherwise returns false.
213
214 Only printed may be changed.  printed is normally updated by calling the
215 collect method of a customer object (see L<FS::cust_main>).
216
217 =cut
218
219 #replace can be inherited from Record.pm
220
221 # replace_check is now the preferred way to #implement replace data checks
222 # (so $object->replace() works without an argument)
223
224 sub replace_check {
225   my( $new, $old ) = ( shift, shift );
226   return "Can't change custnum!" unless $old->custnum == $new->custnum;
227   #return "Can't change _date!" unless $old->_date eq $new->_date;
228   return "Can't change _date!" unless $old->_date == $new->_date;
229   return "Can't change charged!" unless $old->charged == $new->charged
230                                      || $old->charged == 0;
231
232   '';
233 }
234
235 =item check
236
237 Checks all fields to make sure this is a valid invoice.  If there is an error,
238 returns the error, otherwise returns false.  Called by the insert and replace
239 methods.
240
241 =cut
242
243 sub check {
244   my $self = shift;
245
246   my $error =
247     $self->ut_numbern('invnum')
248     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
249     || $self->ut_numbern('_date')
250     || $self->ut_money('charged')
251     || $self->ut_numbern('printed')
252     || $self->ut_enum('closed', [ '', 'Y' ])
253     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
254     || $self->ut_numbern('agent_invid') #varchar?
255   ;
256   return $error if $error;
257
258   $self->_date(time) unless $self->_date;
259
260   $self->printed(0) if $self->printed eq '';
261
262   $self->SUPER::check;
263 }
264
265 =item display_invnum
266
267 Returns the displayed invoice number for this invoice: agent_invid if
268 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
269
270 =cut
271
272 sub display_invnum {
273   my $self = shift;
274   if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
275     return $self->agent_invid;
276   } else {
277     return $self->invnum;
278   }
279 }
280
281 =item previous
282
283 Returns a list consisting of the total previous balance for this customer, 
284 followed by the previous outstanding invoices (as FS::cust_bill objects also).
285
286 =cut
287
288 sub previous {
289   my $self = shift;
290   my $total = 0;
291   my @cust_bill = sort { $a->_date <=> $b->_date }
292     grep { $_->owed != 0 && $_->_date < $self->_date }
293       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
294   ;
295   foreach ( @cust_bill ) { $total += $_->owed; }
296   $total, @cust_bill;
297 }
298
299 =item cust_bill_pkg
300
301 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
302
303 =cut
304
305 sub cust_bill_pkg {
306   my $self = shift;
307   qsearch(
308     { 'table'    => 'cust_bill_pkg',
309       'hashref'  => { 'invnum' => $self->invnum },
310       'order_by' => 'ORDER BY billpkgnum',
311     }
312   );
313 }
314
315 =item cust_bill_pkg_pkgnum PKGNUM
316
317 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
318 specified pkgnum.
319
320 =cut
321
322 sub cust_bill_pkg_pkgnum {
323   my( $self, $pkgnum ) = @_;
324   qsearch(
325     { 'table'    => 'cust_bill_pkg',
326       'hashref'  => { 'invnum' => $self->invnum,
327                       'pkgnum' => $pkgnum,
328                     },
329       'order_by' => 'ORDER BY billpkgnum',
330     }
331   );
332 }
333
334 =item cust_pkg
335
336 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
337 this invoice.
338
339 =cut
340
341 sub cust_pkg {
342   my $self = shift;
343   my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
344   my %saw = ();
345   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
346 }
347
348 =item open_cust_bill_pkg
349
350 Returns the open line items for this invoice.
351
352 Note that cust_bill_pkg with both setup and recur fees are returned as two
353 separate line items, each with only one fee.
354
355 =cut
356
357 # modeled after cust_main::open_cust_bill
358 sub open_cust_bill_pkg {
359   my $self = shift;
360
361   # grep { $_->owed > 0 } $self->cust_bill_pkg
362
363   my %other = ( 'recur' => 'setup',
364                 'setup' => 'recur', );
365   my @open = ();
366   foreach my $field ( qw( recur setup )) {
367     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
368                 grep { $_->owed($field) > 0 }
369                 $self->cust_bill_pkg;
370   }
371
372   @open;
373 }
374
375 =item cust_bill_event
376
377 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
378
379 =cut
380
381 sub cust_bill_event {
382   my $self = shift;
383   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
384 }
385
386 =item num_cust_bill_event
387
388 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
389
390 =cut
391
392 sub num_cust_bill_event {
393   my $self = shift;
394   my $sql =
395     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
396   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
397   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
398   $sth->fetchrow_arrayref->[0];
399 }
400
401 =item cust_event
402
403 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
404
405 =cut
406
407 #false laziness w/cust_pkg.pm
408 sub cust_event {
409   my $self = shift;
410   qsearch({
411     'table'     => 'cust_event',
412     'addl_from' => 'JOIN part_event USING ( eventpart )',
413     'hashref'   => { 'tablenum' => $self->invnum },
414     'extra_sql' => " AND eventtable = 'cust_bill' ",
415   });
416 }
417
418 =item num_cust_event
419
420 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
421
422 =cut
423
424 #false laziness w/cust_pkg.pm
425 sub num_cust_event {
426   my $self = shift;
427   my $sql =
428     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
429     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
430   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
431   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
432   $sth->fetchrow_arrayref->[0];
433 }
434
435 =item cust_main
436
437 Returns the customer (see L<FS::cust_main>) for this invoice.
438
439 =cut
440
441 sub cust_main {
442   my $self = shift;
443   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
444 }
445
446 =item cust_suspend_if_balance_over AMOUNT
447
448 Suspends the customer associated with this invoice if the total amount owed on
449 this invoice and all older invoices is greater than the specified amount.
450
451 Returns a list: an empty list on success or a list of errors.
452
453 =cut
454
455 sub cust_suspend_if_balance_over {
456   my( $self, $amount ) = ( shift, shift );
457   my $cust_main = $self->cust_main;
458   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
459     return ();
460   } else {
461     $cust_main->suspend(@_);
462   }
463 }
464
465 =item cust_credit
466
467 Depreciated.  See the cust_credited method.
468
469  #Returns a list consisting of the total previous credited (see
470  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
471  #outstanding credits (FS::cust_credit objects).
472
473 =cut
474
475 sub cust_credit {
476   use Carp;
477   croak "FS::cust_bill->cust_credit depreciated; see ".
478         "FS::cust_bill->cust_credit_bill";
479   #my $self = shift;
480   #my $total = 0;
481   #my @cust_credit = sort { $a->_date <=> $b->_date }
482   #  grep { $_->credited != 0 && $_->_date < $self->_date }
483   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
484   #;
485   #foreach (@cust_credit) { $total += $_->credited; }
486   #$total, @cust_credit;
487 }
488
489 =item cust_pay
490
491 Depreciated.  See the cust_bill_pay method.
492
493 #Returns all payments (see L<FS::cust_pay>) for this invoice.
494
495 =cut
496
497 sub cust_pay {
498   use Carp;
499   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
500   #my $self = shift;
501   #sort { $a->_date <=> $b->_date }
502   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
503   #;
504 }
505
506 sub cust_pay_batch {
507   my $self = shift;
508   qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
509 }
510
511 sub cust_bill_pay_batch {
512   my $self = shift;
513   qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
514 }
515
516 =item cust_bill_pay
517
518 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
519
520 =cut
521
522 sub cust_bill_pay {
523   my $self = shift;
524   sort { $a->_date <=> $b->_date }
525     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
526 }
527
528 =item cust_credited
529
530 =item cust_credit_bill
531
532 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
533
534 =cut
535
536 sub cust_credited {
537   my $self = shift;
538   sort { $a->_date <=> $b->_date }
539     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
540   ;
541 }
542
543 sub cust_credit_bill {
544   shift->cust_credited(@_);
545 }
546
547 =item cust_bill_pay_pkgnum PKGNUM
548
549 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
550 with matching pkgnum.
551
552 =cut
553
554 sub cust_bill_pay_pkgnum {
555   my( $self, $pkgnum ) = @_;
556   sort { $a->_date <=> $b->_date }
557     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
558                                 'pkgnum' => $pkgnum,
559                               }
560            );
561 }
562
563 =item cust_credited_pkgnum PKGNUM
564
565 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
566 with matching pkgnum.
567
568 =cut
569
570 sub cust_credited_pkgnum {
571   my( $self, $pkgnum ) = @_;
572   sort { $a->_date <=> $b->_date }
573     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
574                                    'pkgnum' => $pkgnum,
575                                  }
576            );
577 }
578
579 =item tax
580
581 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
582
583 =cut
584
585 sub tax {
586   my $self = shift;
587   my $total = 0;
588   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
589                                              'pkgnum' => 0 } );
590   foreach (@taxlines) { $total += $_->setup; }
591   $total;
592 }
593
594 =item owed
595
596 Returns the amount owed (still outstanding) on this invoice, which is charged
597 minus all payment applications (see L<FS::cust_bill_pay>) and credit
598 applications (see L<FS::cust_credit_bill>).
599
600 =cut
601
602 sub owed {
603   my $self = shift;
604   my $balance = $self->charged;
605   $balance -= $_->amount foreach ( $self->cust_bill_pay );
606   $balance -= $_->amount foreach ( $self->cust_credited );
607   $balance = sprintf( "%.2f", $balance);
608   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
609   $balance;
610 }
611
612 sub owed_pkgnum {
613   my( $self, $pkgnum ) = @_;
614
615   #my $balance = $self->charged;
616   my $balance = 0;
617   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
618
619   $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
620   $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
621
622   $balance = sprintf( "%.2f", $balance);
623   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
624   $balance;
625 }
626
627 =item apply_payments_and_credits [ OPTION => VALUE ... ]
628
629 Applies unapplied payments and credits to this invoice.
630
631 A hash of optional arguments may be passed.  Currently "manual" is supported.
632 If true, a payment receipt is sent instead of a statement when
633 'payment_receipt_email' configuration option is set.
634
635 If there is an error, returns the error, otherwise returns false.
636
637 =cut
638
639 sub apply_payments_and_credits {
640   my( $self, %options ) = @_;
641
642   local $SIG{HUP} = 'IGNORE';
643   local $SIG{INT} = 'IGNORE';
644   local $SIG{QUIT} = 'IGNORE';
645   local $SIG{TERM} = 'IGNORE';
646   local $SIG{TSTP} = 'IGNORE';
647   local $SIG{PIPE} = 'IGNORE';
648
649   my $oldAutoCommit = $FS::UID::AutoCommit;
650   local $FS::UID::AutoCommit = 0;
651   my $dbh = dbh;
652
653   $self->select_for_update; #mutex
654
655   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
656   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
657
658   if ( $conf->exists('pkg-balances') ) {
659     # limit @payments & @credits to those w/ a pkgnum grepped from $self
660     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
661     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
662     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
663   }
664
665   while ( $self->owed > 0 and ( @payments || @credits ) ) {
666
667     my $app = '';
668     if ( @payments && @credits ) {
669
670       #decide which goes first by weight of top (unapplied) line item
671
672       my @open_lineitems = $self->open_cust_bill_pkg;
673
674       my $max_pay_weight =
675         max( map  { $_->part_pkg->pay_weight || 0 }
676              grep { $_ }
677              map  { $_->cust_pkg }
678                   @open_lineitems
679            );
680       my $max_credit_weight =
681         max( map  { $_->part_pkg->credit_weight || 0 }
682              grep { $_ } 
683              map  { $_->cust_pkg }
684                   @open_lineitems
685            );
686
687       #if both are the same... payments first?  it has to be something
688       if ( $max_pay_weight >= $max_credit_weight ) {
689         $app = 'pay';
690       } else {
691         $app = 'credit';
692       }
693     
694     } elsif ( @payments ) {
695       $app = 'pay';
696     } elsif ( @credits ) {
697       $app = 'credit';
698     } else {
699       die "guru meditation #12 and 35";
700     }
701
702     my $unapp_amount;
703     if ( $app eq 'pay' ) {
704
705       my $payment = shift @payments;
706       $unapp_amount = $payment->unapplied;
707       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
708       $app->pkgnum( $payment->pkgnum )
709         if $conf->exists('pkg-balances') && $payment->pkgnum;
710
711     } elsif ( $app eq 'credit' ) {
712
713       my $credit = shift @credits;
714       $unapp_amount = $credit->credited;
715       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
716       $app->pkgnum( $credit->pkgnum )
717         if $conf->exists('pkg-balances') && $credit->pkgnum;
718
719     } else {
720       die "guru meditation #12 and 35";
721     }
722
723     my $owed;
724     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
725       warn "owed_pkgnum ". $app->pkgnum;
726       $owed = $self->owed_pkgnum($app->pkgnum);
727     } else {
728       $owed = $self->owed;
729     }
730     next unless $owed > 0;
731
732     warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
733     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
734
735     $app->invnum( $self->invnum );
736
737     my $error = $app->insert(%options);
738     if ( $error ) {
739       $dbh->rollback if $oldAutoCommit;
740       return "Error inserting ". $app->table. " record: $error";
741     }
742     die $error if $error;
743
744   }
745
746   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
747   ''; #no error
748
749 }
750
751 =item generate_email OPTION => VALUE ...
752
753 Options:
754
755 =over 4
756
757 =item from
758
759 sender address, required
760
761 =item tempate
762
763 alternate template name, optional
764
765 =item print_text
766
767 text attachment arrayref, optional
768
769 =item subject
770
771 email subject, optional
772
773 =back
774
775 Returns an argument list to be passed to L<FS::Misc::send_email>.
776
777 =cut
778
779 use MIME::Entity;
780
781 sub generate_email {
782
783   my $self = shift;
784   my %args = @_;
785
786   my $me = '[FS::cust_bill::generate_email]';
787
788   my %return = (
789     'from'      => $args{'from'},
790     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
791   );
792
793   my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
794
795   if (ref($args{'to'}) eq 'ARRAY') {
796     $return{'to'} = $args{'to'};
797   } else {
798     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
799                            $self->cust_main->invoicing_list
800                     ];
801   }
802
803   if ( $conf->exists('invoice_html') ) {
804
805     warn "$me creating HTML/text multipart message"
806       if $DEBUG;
807
808     $return{'nobody'} = 1;
809
810     my $alternative = build MIME::Entity
811       'Type'        => 'multipart/alternative',
812       'Encoding'    => '7bit',
813       'Disposition' => 'inline'
814     ;
815
816     my $data;
817     if ( $conf->exists('invoice_email_pdf')
818          and scalar($conf->config('invoice_email_pdf_note')) ) {
819
820       warn "$me using 'invoice_email_pdf_note' in multipart message"
821         if $DEBUG;
822       $data = [ map { $_ . "\n" }
823                     $conf->config('invoice_email_pdf_note')
824               ];
825
826     } else {
827
828       warn "$me not using 'invoice_email_pdf_note' in multipart message"
829         if $DEBUG;
830       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
831         $data = $args{'print_text'};
832       } else {
833         $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
834       }
835
836     }
837
838     $alternative->attach(
839       'Type'        => 'text/plain',
840       #'Encoding'    => 'quoted-printable',
841       'Encoding'    => '7bit',
842       'Data'        => $data,
843       'Disposition' => 'inline',
844     );
845
846     $args{'from'} =~ /\@([\w\.\-]+)/;
847     my $from = $1 || 'example.com';
848     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
849
850     my $logo;
851     my $agentnum = $self->cust_main->agentnum;
852     if ( defined($args{'template'}) && length($args{'template'})
853          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
854        )
855     {
856       $logo = 'logo_'. $args{'template'}. '.png';
857     } else {
858       $logo = "logo.png";
859     }
860     my $image_data = $conf->config_binary( $logo, $agentnum);
861
862     my $image = build MIME::Entity
863       'Type'       => 'image/png',
864       'Encoding'   => 'base64',
865       'Data'       => $image_data,
866       'Filename'   => 'logo.png',
867       'Content-ID' => "<$content_id>",
868     ;
869
870     $alternative->attach(
871       'Type'        => 'text/html',
872       'Encoding'    => 'quoted-printable',
873       'Data'        => [ '<html>',
874                          '  <head>',
875                          '    <title>',
876                          '      '. encode_entities($return{'subject'}), 
877                          '    </title>',
878                          '  </head>',
879                          '  <body bgcolor="#e8e8e8">',
880                          $self->print_html({ time          => '',
881                                              template      => $args{'template'},
882                                              cid           => $content_id,
883                                              %cdrs,
884                                           }),
885                          '  </body>',
886                          '</html>',
887                        ],
888       'Disposition' => 'inline',
889       #'Filename'    => 'invoice.pdf',
890     );
891
892     my @otherparts = ();
893     if ( $self->cust_main->email_csv_cdr ) {
894
895       push @otherparts, build MIME::Entity
896         'Type'        => 'text/csv',
897         'Encoding'    => '7bit',
898         'Data'        => [ map { "$_\n" }
899                              $self->call_details('prepend_billed_number' => 1)
900                          ],
901         'Disposition' => 'attachment',
902         'Filename'    => 'usage-'. $self->invnum. '.csv',
903       ;
904
905     }
906
907     if ( $conf->exists('invoice_email_pdf') ) {
908
909       #attaching pdf too:
910       # multipart/mixed
911       #   multipart/related
912       #     multipart/alternative
913       #       text/plain
914       #       text/html
915       #     image/png
916       #   application/pdf
917
918       my $related = build MIME::Entity 'Type'     => 'multipart/related',
919                                        'Encoding' => '7bit';
920
921       #false laziness w/Misc::send_email
922       $related->head->replace('Content-type',
923         $related->mime_type.
924         '; boundary="'. $related->head->multipart_boundary. '"'.
925         '; type=multipart/alternative'
926       );
927
928       $related->add_part($alternative);
929
930       $related->add_part($image);
931
932       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
933
934       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
935
936     } else {
937
938       #no other attachment:
939       # multipart/related
940       #   multipart/alternative
941       #     text/plain
942       #     text/html
943       #   image/png
944
945       $return{'content-type'} = 'multipart/related';
946       $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
947       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
948       #$return{'disposition'} = 'inline';
949
950     }
951   
952   } else {
953
954     if ( $conf->exists('invoice_email_pdf') ) {
955       warn "$me creating PDF attachment"
956         if $DEBUG;
957
958       #mime parts arguments a la MIME::Entity->build().
959       $return{'mimeparts'} = [
960         { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
961       ];
962     }
963   
964     if ( $conf->exists('invoice_email_pdf')
965          and scalar($conf->config('invoice_email_pdf_note')) ) {
966
967       warn "$me using 'invoice_email_pdf_note'"
968         if $DEBUG;
969       $return{'body'} = [ map { $_ . "\n" }
970                               $conf->config('invoice_email_pdf_note')
971                         ];
972
973     } else {
974
975       warn "$me not using 'invoice_email_pdf_note'"
976         if $DEBUG;
977       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
978         $return{'body'} = $args{'print_text'};
979       } else {
980         $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
981       }
982
983     }
984
985   }
986
987   %return;
988
989 }
990
991 =item mimebuild_pdf
992
993 Returns a list suitable for passing to MIME::Entity->build(), representing
994 this invoice as PDF attachment.
995
996 =cut
997
998 sub mimebuild_pdf {
999   my $self = shift;
1000   (
1001     'Type'        => 'application/pdf',
1002     'Encoding'    => 'base64',
1003     'Data'        => [ $self->print_pdf(@_) ],
1004     'Disposition' => 'attachment',
1005     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
1006   );
1007 }
1008
1009 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1010
1011 Sends this invoice to the destinations configured for this customer: sends
1012 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1013
1014 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1015
1016 AGENTNUM, if specified, means that this invoice will only be sent for customers
1017 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
1018 single agent) or an arrayref of agentnums.
1019
1020 INVOICE_FROM, if specified, overrides the default email invoice From: address.
1021
1022 AMOUNT, if specified, only sends the invoice if the total amount owed on this
1023 invoice and all older invoices is greater than the specified amount.
1024
1025 =cut
1026
1027 sub queueable_send {
1028   my %opt = @_;
1029
1030   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1031     or die "invalid invoice number: " . $opt{invnum};
1032
1033   my @args = ( $opt{template}, $opt{agentnum} );
1034   push @args, $opt{invoice_from}
1035     if exists($opt{invoice_from}) && $opt{invoice_from};
1036
1037   my $error = $self->send( @args );
1038   die $error if $error;
1039
1040 }
1041
1042 sub send {
1043   my $self = shift;
1044   my $template = scalar(@_) ? shift : '';
1045   if ( scalar(@_) && $_[0]  ) {
1046     my $agentnums = ref($_[0]) ? shift : [ shift ];
1047     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
1048   }
1049
1050   my $invoice_from =
1051     scalar(@_)
1052       ? shift
1053       : ( $self->_agent_invoice_from ||    #XXX should go away
1054           $conf->config('invoice_from', $self->cust_main->agentnum )
1055         );
1056
1057   my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
1058
1059   return ''
1060     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1061
1062   my @invoicing_list = $self->cust_main->invoicing_list;
1063
1064   #$self->email_invoice($template, $invoice_from)
1065   $self->email($template, $invoice_from)
1066     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1067
1068   #$self->print_invoice($template)
1069   $self->print($template)
1070     if grep { $_ eq 'POST' } @invoicing_list; #postal
1071
1072   $self->fax_invoice($template)
1073     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1074
1075   '';
1076
1077 }
1078
1079 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
1080
1081 Emails this invoice.
1082
1083 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1084
1085 INVOICE_FROM, if specified, overrides the default email invoice From: address.
1086
1087 =cut
1088
1089 sub queueable_email {
1090   my %opt = @_;
1091
1092   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1093     or die "invalid invoice number: " . $opt{invnum};
1094
1095   my @args = ( $opt{template} );
1096   push @args, $opt{invoice_from}
1097     if exists($opt{invoice_from}) && $opt{invoice_from};
1098
1099   my $error = $self->email( @args );
1100   die $error if $error;
1101
1102 }
1103
1104 #sub email_invoice {
1105 sub email {
1106   my $self = shift;
1107   my $template = scalar(@_) ? shift : '';
1108   my $invoice_from =
1109     scalar(@_)
1110       ? shift
1111       : ( $self->_agent_invoice_from ||    #XXX should go away
1112           $conf->config('invoice_from', $self->cust_main->agentnum )
1113         );
1114
1115
1116   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1117                             $self->cust_main->invoicing_list;
1118
1119   #better to notify this person than silence
1120   @invoicing_list = ($invoice_from) unless @invoicing_list;
1121
1122   my $subject = $self->email_subject($template);
1123
1124   my $error = send_email(
1125     $self->generate_email(
1126       'from'       => $invoice_from,
1127       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1128       'subject'    => $subject,
1129       'template'   => $template,
1130     )
1131   );
1132   die "can't email invoice: $error\n" if $error;
1133   #die "$error\n" if $error;
1134
1135 }
1136
1137 sub email_subject {
1138   my $self = shift;
1139
1140   #my $template = scalar(@_) ? shift : '';
1141   #per-template?
1142
1143   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1144                 || 'Invoice';
1145
1146   my $cust_main = $self->cust_main;
1147   my $name = $cust_main->name;
1148   my $name_short = $cust_main->name_short;
1149   my $invoice_number = $self->invnum;
1150   my $invoice_date = $self->_date_pretty;
1151
1152   eval qq("$subject");
1153 }
1154
1155 =item lpr_data [ TEMPLATENAME ]
1156
1157 Returns the postscript or plaintext for this invoice as an arrayref.
1158
1159 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1160
1161 =cut
1162
1163 sub lpr_data {
1164   my( $self, $template) = @_;
1165   $conf->exists('invoice_latex')
1166     ? [ $self->print_ps('', $template) ]
1167     : [ $self->print_text('', $template) ];
1168 }
1169
1170 =item print [ TEMPLATENAME ]
1171
1172 Prints this invoice.
1173
1174 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1175
1176 =cut
1177
1178 #sub print_invoice {
1179 sub print {
1180   my $self = shift;
1181   my $template = scalar(@_) ? shift : '';
1182
1183   do_print $self->lpr_data($template);
1184 }
1185
1186 =item fax_invoice [ TEMPLATENAME ] 
1187
1188 Faxes this invoice.
1189
1190 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1191
1192 =cut
1193
1194 sub fax_invoice {
1195   my $self = shift;
1196   my $template = scalar(@_) ? shift : '';
1197
1198   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1199     unless $conf->exists('invoice_latex');
1200
1201   my $dialstring = $self->cust_main->getfield('fax');
1202   #Check $dialstring?
1203
1204   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
1205                         'dialstring' => $dialstring,
1206                       );
1207   die $error if $error;
1208
1209 }
1210
1211 =item ftp_invoice [ TEMPLATENAME ] 
1212
1213 Sends this invoice data via FTP.
1214
1215 TEMPLATENAME is unused?
1216
1217 =cut
1218
1219 sub ftp_invoice {
1220   my $self = shift;
1221   my $template = scalar(@_) ? shift : '';
1222
1223   $self->send_csv(
1224     'protocol'   => 'ftp',
1225     'server'     => $conf->config('cust_bill-ftpserver'),
1226     'username'   => $conf->config('cust_bill-ftpusername'),
1227     'password'   => $conf->config('cust_bill-ftppassword'),
1228     'dir'        => $conf->config('cust_bill-ftpdir'),
1229     'format'     => $conf->config('cust_bill-ftpformat'),
1230   );
1231 }
1232
1233 =item spool_invoice [ TEMPLATENAME ] 
1234
1235 Spools this invoice data (see L<FS::spool_csv>)
1236
1237 TEMPLATENAME is unused?
1238
1239 =cut
1240
1241 sub spool_invoice {
1242   my $self = shift;
1243   my $template = scalar(@_) ? shift : '';
1244
1245   $self->spool_csv(
1246     'format'       => $conf->config('cust_bill-spoolformat'),
1247     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1248   );
1249 }
1250
1251 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1252
1253 Like B<send>, but only sends the invoice if it is the newest open invoice for
1254 this customer.
1255
1256 =cut
1257
1258 sub send_if_newest {
1259   my $self = shift;
1260
1261   return ''
1262     if scalar(
1263                grep { $_->owed > 0 } 
1264                     qsearch('cust_bill', {
1265                       'custnum' => $self->custnum,
1266                       #'_date'   => { op=>'>', value=>$self->_date },
1267                       'invnum'  => { op=>'>', value=>$self->invnum },
1268                     } )
1269              );
1270     
1271   $self->send(@_);
1272 }
1273
1274 =item send_csv OPTION => VALUE, ...
1275
1276 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1277
1278 Options are:
1279
1280 protocol - currently only "ftp"
1281 server
1282 username
1283 password
1284 dir
1285
1286 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1287 and YYMMDDHHMMSS is a timestamp.
1288
1289 See L</print_csv> for a description of the output format.
1290
1291 =cut
1292
1293 sub send_csv {
1294   my($self, %opt) = @_;
1295
1296   #create file(s)
1297
1298   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1299   mkdir $spooldir, 0700 unless -d $spooldir;
1300
1301   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1302   my $file = "$spooldir/$tracctnum.csv";
1303   
1304   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1305
1306   open(CSV, ">$file") or die "can't open $file: $!";
1307   print CSV $header;
1308
1309   print CSV $detail;
1310
1311   close CSV;
1312
1313   my $net;
1314   if ( $opt{protocol} eq 'ftp' ) {
1315     eval "use Net::FTP;";
1316     die $@ if $@;
1317     $net = Net::FTP->new($opt{server}) or die @$;
1318   } else {
1319     die "unknown protocol: $opt{protocol}";
1320   }
1321
1322   $net->login( $opt{username}, $opt{password} )
1323     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1324
1325   $net->binary or die "can't set binary mode";
1326
1327   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1328
1329   $net->put($file) or die "can't put $file: $!";
1330
1331   $net->quit;
1332
1333   unlink $file;
1334
1335 }
1336
1337 =item spool_csv
1338
1339 Spools CSV invoice data.
1340
1341 Options are:
1342
1343 =over 4
1344
1345 =item format - 'default' or 'billco'
1346
1347 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1348
1349 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1350
1351 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1352
1353 =back
1354
1355 =cut
1356
1357 sub spool_csv {
1358   my($self, %opt) = @_;
1359
1360   my $cust_main = $self->cust_main;
1361
1362   if ( $opt{'dest'} ) {
1363     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1364                              $cust_main->invoicing_list;
1365     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1366                      || ! keys %invoicing_list;
1367   }
1368
1369   if ( $opt{'balanceover'} ) {
1370     return 'N/A'
1371       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1372   }
1373
1374   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1375   mkdir $spooldir, 0700 unless -d $spooldir;
1376
1377   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1378
1379   my $file =
1380     "$spooldir/".
1381     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1382     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1383     '.csv';
1384   
1385   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1386
1387   open(CSV, ">>$file") or die "can't open $file: $!";
1388   flock(CSV, LOCK_EX);
1389   seek(CSV, 0, 2);
1390
1391   print CSV $header;
1392
1393   if ( lc($opt{'format'}) eq 'billco' ) {
1394
1395     flock(CSV, LOCK_UN);
1396     close CSV;
1397
1398     $file =
1399       "$spooldir/".
1400       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1401       '-detail.csv';
1402
1403     open(CSV,">>$file") or die "can't open $file: $!";
1404     flock(CSV, LOCK_EX);
1405     seek(CSV, 0, 2);
1406   }
1407
1408   print CSV $detail;
1409
1410   flock(CSV, LOCK_UN);
1411   close CSV;
1412
1413   return '';
1414
1415 }
1416
1417 =item print_csv OPTION => VALUE, ...
1418
1419 Returns CSV data for this invoice.
1420
1421 Options are:
1422
1423 format - 'default' or 'billco'
1424
1425 Returns a list consisting of two scalars.  The first is a single line of CSV
1426 header information for this invoice.  The second is one or more lines of CSV
1427 detail information for this invoice.
1428
1429 If I<format> is not specified or "default", the fields of the CSV file are as
1430 follows:
1431
1432 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1433
1434 =over 4
1435
1436 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1437
1438 B<record_type> is C<cust_bill> for the initial header line only.  The
1439 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1440 fields are filled in.
1441
1442 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1443 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1444 are filled in.
1445
1446 =item invnum - invoice number
1447
1448 =item custnum - customer number
1449
1450 =item _date - invoice date
1451
1452 =item charged - total invoice amount
1453
1454 =item first - customer first name
1455
1456 =item last - customer first name
1457
1458 =item company - company name
1459
1460 =item address1 - address line 1
1461
1462 =item address2 - address line 1
1463
1464 =item city
1465
1466 =item state
1467
1468 =item zip
1469
1470 =item country
1471
1472 =item pkg - line item description
1473
1474 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1475
1476 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1477
1478 =item sdate - start date for recurring fee
1479
1480 =item edate - end date for recurring fee
1481
1482 =back
1483
1484 If I<format> is "billco", the fields of the header CSV file are as follows:
1485
1486   +-------------------------------------------------------------------+
1487   |                        FORMAT HEADER FILE                         |
1488   |-------------------------------------------------------------------|
1489   | Field | Description                   | Name       | Type | Width |
1490   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1491   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1492   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1493   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1494   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1495   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1496   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1497   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1498   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1499   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1500   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1501   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1502   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1503   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1504   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1505   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1506   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1507   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1508   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1509   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1510   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1511   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1512   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1513   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1514   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1515   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1516   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1517   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1518   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1519   +-------+-------------------------------+------------+------+-------+
1520
1521 If I<format> is "billco", the fields of the detail CSV file are as follows:
1522
1523                                   FORMAT FOR DETAIL FILE
1524         |                            |           |      |
1525   Field | Description                | Name      | Type | Width
1526   1     | N/A-Leave Empty            | RC        | CHAR |     2
1527   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1528   3     | Account Number             | TRACCTNUM | CHAR |    15
1529   4     | Invoice Number             | TRINVOICE | CHAR |    15
1530   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1531   6     | Transaction Detail         | DETAILS   | CHAR |   100
1532   7     | Amount                     | AMT       | NUM* |     9
1533   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1534   9     | Grouping Code              | GROUP     | CHAR |     2
1535   10    | User Defined               | ACCT CODE | CHAR |    15
1536
1537 =cut
1538
1539 sub print_csv {
1540   my($self, %opt) = @_;
1541   
1542   eval "use Text::CSV_XS";
1543   die $@ if $@;
1544
1545   my $cust_main = $self->cust_main;
1546
1547   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1548
1549   if ( lc($opt{'format'}) eq 'billco' ) {
1550
1551     my $taxtotal = 0;
1552     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1553
1554     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1555
1556     my( $previous_balance, @unused ) = $self->previous; #previous balance
1557
1558     my $pmt_cr_applied = 0;
1559     $pmt_cr_applied += $_->{'amount'}
1560       foreach ( $self->_items_payments, $self->_items_credits ) ;
1561
1562     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1563
1564     $csv->combine(
1565       '',                         #  1 | N/A-Leave Empty               CHAR   2
1566       '',                         #  2 | N/A-Leave Empty               CHAR  15
1567       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1568       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1569       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1570       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1571       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1572       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1573       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1574       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1575       '',                         # 10 | Ancillary Billing Information CHAR  30
1576       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1577       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1578
1579       # XXX ?
1580       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1581
1582       # XXX ?
1583       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1584
1585       $previous_balance,          # 15 | Previous Balance              NUM*   9
1586       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1587       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1588       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1589       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1590       '',                         # 20 | 30 Day Aging                  NUM*   9
1591       '',                         # 21 | 60 Day Aging                  NUM*   9
1592       '',                         # 22 | 90 Day Aging                  NUM*   9
1593       'N',                        # 23 | Y/N                           CHAR   1
1594       '',                         # 24 | Remittance automation         CHAR 100
1595       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1596       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1597       '0',                        # 27 | Federal Tax***                NUM*   9
1598       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1599       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1600     );
1601
1602   } else {
1603   
1604     $csv->combine(
1605       'cust_bill',
1606       $self->invnum,
1607       $self->custnum,
1608       time2str("%x", $self->_date),
1609       sprintf("%.2f", $self->charged),
1610       ( map { $cust_main->getfield($_) }
1611           qw( first last company address1 address2 city state zip country ) ),
1612       map { '' } (1..5),
1613     ) or die "can't create csv";
1614   }
1615
1616   my $header = $csv->string. "\n";
1617
1618   my $detail = '';
1619   if ( lc($opt{'format'}) eq 'billco' ) {
1620
1621     my $lineseq = 0;
1622     foreach my $item ( $self->_items_pkg ) {
1623
1624       $csv->combine(
1625         '',                     #  1 | N/A-Leave Empty            CHAR   2
1626         '',                     #  2 | N/A-Leave Empty            CHAR  15
1627         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1628         $self->invnum,          #  4 | Invoice Number             CHAR  15
1629         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1630         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1631         $item->{'amount'},      #  7 | Amount                     NUM*   9
1632         '',                     #  8 | Line Format Control**      CHAR   2
1633         '',                     #  9 | Grouping Code              CHAR   2
1634         '',                     # 10 | User Defined               CHAR  15
1635       );
1636
1637       $detail .= $csv->string. "\n";
1638
1639     }
1640
1641   } else {
1642
1643     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1644
1645       my($pkg, $setup, $recur, $sdate, $edate);
1646       if ( $cust_bill_pkg->pkgnum ) {
1647       
1648         ($pkg, $setup, $recur, $sdate, $edate) = (
1649           $cust_bill_pkg->part_pkg->pkg,
1650           ( $cust_bill_pkg->setup != 0
1651             ? sprintf("%.2f", $cust_bill_pkg->setup )
1652             : '' ),
1653           ( $cust_bill_pkg->recur != 0
1654             ? sprintf("%.2f", $cust_bill_pkg->recur )
1655             : '' ),
1656           ( $cust_bill_pkg->sdate 
1657             ? time2str("%x", $cust_bill_pkg->sdate)
1658             : '' ),
1659           ($cust_bill_pkg->edate 
1660             ?time2str("%x", $cust_bill_pkg->edate)
1661             : '' ),
1662         );
1663   
1664       } else { #pkgnum tax
1665         next unless $cust_bill_pkg->setup != 0;
1666         $pkg = $cust_bill_pkg->desc;
1667         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1668         ( $sdate, $edate ) = ( '', '' );
1669       }
1670   
1671       $csv->combine(
1672         'cust_bill_pkg',
1673         $self->invnum,
1674         ( map { '' } (1..11) ),
1675         ($pkg, $setup, $recur, $sdate, $edate)
1676       ) or die "can't create csv";
1677
1678       $detail .= $csv->string. "\n";
1679
1680     }
1681
1682   }
1683
1684   ( $header, $detail );
1685
1686 }
1687
1688 =item comp
1689
1690 Pays this invoice with a compliemntary payment.  If there is an error,
1691 returns the error, otherwise returns false.
1692
1693 =cut
1694
1695 sub comp {
1696   my $self = shift;
1697   my $cust_pay = new FS::cust_pay ( {
1698     'invnum'   => $self->invnum,
1699     'paid'     => $self->owed,
1700     '_date'    => '',
1701     'payby'    => 'COMP',
1702     'payinfo'  => $self->cust_main->payinfo,
1703     'paybatch' => '',
1704   } );
1705   $cust_pay->insert;
1706 }
1707
1708 =item realtime_card
1709
1710 Attempts to pay this invoice with a credit card payment via a
1711 Business::OnlinePayment realtime gateway.  See
1712 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1713 for supported processors.
1714
1715 =cut
1716
1717 sub realtime_card {
1718   my $self = shift;
1719   $self->realtime_bop( 'CC', @_ );
1720 }
1721
1722 =item realtime_ach
1723
1724 Attempts to pay this invoice with an electronic check (ACH) payment via a
1725 Business::OnlinePayment realtime gateway.  See
1726 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1727 for supported processors.
1728
1729 =cut
1730
1731 sub realtime_ach {
1732   my $self = shift;
1733   $self->realtime_bop( 'ECHECK', @_ );
1734 }
1735
1736 =item realtime_lec
1737
1738 Attempts to pay this invoice with phone bill (LEC) payment via a
1739 Business::OnlinePayment realtime gateway.  See
1740 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1741 for supported processors.
1742
1743 =cut
1744
1745 sub realtime_lec {
1746   my $self = shift;
1747   $self->realtime_bop( 'LEC', @_ );
1748 }
1749
1750 sub realtime_bop {
1751   my( $self, $method ) = @_;
1752
1753   my $cust_main = $self->cust_main;
1754   my $balance = $cust_main->balance;
1755   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1756   $amount = sprintf("%.2f", $amount);
1757   return "not run (balance $balance)" unless $amount > 0;
1758
1759   my $description = 'Internet Services';
1760   if ( $conf->exists('business-onlinepayment-description') ) {
1761     my $dtempl = $conf->config('business-onlinepayment-description');
1762
1763     my $agent_obj = $cust_main->agent
1764       or die "can't retreive agent for $cust_main (agentnum ".
1765              $cust_main->agentnum. ")";
1766     my $agent = $agent_obj->agent;
1767     my $pkgs = join(', ',
1768       map { $_->part_pkg->pkg }
1769         grep { $_->pkgnum } $self->cust_bill_pkg
1770     );
1771     $description = eval qq("$dtempl");
1772   }
1773
1774   $cust_main->realtime_bop($method, $amount,
1775     'description' => $description,
1776     'invnum'      => $self->invnum,
1777   );
1778
1779 }
1780
1781 =item batch_card OPTION => VALUE...
1782
1783 Adds a payment for this invoice to the pending credit card batch (see
1784 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1785 runs the payment using a realtime gateway.
1786
1787 =cut
1788
1789 sub batch_card {
1790   my ($self, %options) = @_;
1791   my $cust_main = $self->cust_main;
1792
1793   $options{invnum} = $self->invnum;
1794   
1795   $cust_main->batch_card(%options);
1796 }
1797
1798 sub _agent_template {
1799   my $self = shift;
1800   $self->cust_main->agent_template;
1801 }
1802
1803 sub _agent_invoice_from {
1804   my $self = shift;
1805   $self->cust_main->agent_invoice_from;
1806 }
1807
1808 =item print_text [ TIME [ , TEMPLATE ] ]
1809
1810 Returns an text invoice, as a list of lines.
1811
1812 TIME an optional value used to control the printing of overdue messages.  The
1813 default is now.  It isn't the date of the invoice; that's the `_date' field.
1814 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1815 L<Time::Local> and L<Date::Parse> for conversion functions.
1816
1817 =cut
1818
1819 sub print_text {
1820   my( $self, $today, $template, %opt ) = @_;
1821
1822   my %params = ( 'format' => 'template' );
1823   $params{'time'} = $today if $today;
1824   $params{'template'} = $template if $template;
1825   $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1826
1827   $self->print_generic( %params );
1828 }
1829
1830 =item print_latex [ TIME [ , TEMPLATE ] ]
1831
1832 Internal method - returns a filename of a filled-in LaTeX template for this
1833 invoice (Note: add ".tex" to get the actual filename), and a filename of
1834 an associated logo (with the .eps extension included).
1835
1836 See print_ps and print_pdf for methods that return PostScript and PDF output.
1837
1838 TIME an optional value used to control the printing of overdue messages.  The
1839 default is now.  It isn't the date of the invoice; that's the `_date' field.
1840 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1841 L<Time::Local> and L<Date::Parse> for conversion functions.
1842
1843 =cut
1844
1845 sub print_latex {
1846   my( $self, $today, $template, %opt ) = @_;
1847
1848   my %params = ( 'format' => 'latex' );
1849   $params{'time'} = $today if $today;
1850   $params{'template'} = $template if $template;
1851   $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1852
1853   $template ||= $self->_agent_template;
1854
1855   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1856   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1857                            DIR      => $dir,
1858                            SUFFIX   => '.eps',
1859                            UNLINK   => 0,
1860                          ) or die "can't open temp file: $!\n";
1861
1862   my $agentnum = $self->cust_main->agentnum;
1863
1864   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1865     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1866       or die "can't write temp file: $!\n";
1867   } else {
1868     print $lh $conf->config_binary('logo.eps', $agentnum)
1869       or die "can't write temp file: $!\n";
1870   }
1871   close $lh;
1872   $params{'logo_file'} = $lh->filename;
1873
1874   my @filled_in = $self->print_generic( %params );
1875   
1876   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1877                            DIR      => $dir,
1878                            SUFFIX   => '.tex',
1879                            UNLINK   => 0,
1880                          ) or die "can't open temp file: $!\n";
1881   print $fh join('', @filled_in );
1882   close $fh;
1883
1884   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1885   return ($1, $params{'logo_file'});
1886
1887 }
1888
1889 =item print_generic OPTIONS_HASH
1890
1891 Internal method - returns a filled-in template for this invoice as a scalar.
1892
1893 See print_ps and print_pdf for methods that return PostScript and PDF output.
1894
1895 Non optional options include 
1896   format - latex, html, template
1897
1898 Optional options include
1899
1900 template - a value used as a suffix for a configuration template
1901
1902 time - a value used to control the printing of overdue messages.  The
1903 default is now.  It isn't the date of the invoice; that's the `_date' field.
1904 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1905 L<Time::Local> and L<Date::Parse> for conversion functions.
1906
1907 cid - 
1908
1909 unsquelch_cdr - overrides any per customer cdr squelching when true
1910
1911 =cut
1912
1913 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
1914 # (alignment?) problems to change them all to '%.2f' ?
1915 sub print_generic {
1916
1917   my( $self, %params ) = @_;
1918   my $today = $params{today} ? $params{today} : time;
1919   warn "$me print_generic called on $self with suffix $params{template}\n"
1920     if $DEBUG;
1921
1922   my $format = $params{format};
1923   die "Unknown format: $format"
1924     unless $format =~ /^(latex|html|template)$/;
1925
1926   my $cust_main = $self->cust_main;
1927   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1928     unless $cust_main->payname
1929         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1930
1931   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
1932                      'html'     => [ '<%=', '%>' ],
1933                      'template' => [ '{', '}' ],
1934                    );
1935
1936   #create the template
1937   my $template = $params{template} ? $params{template} : $self->_agent_template;
1938   my $templatefile = "invoice_$format";
1939   $templatefile .= "_$template"
1940     if length($template);
1941   my @invoice_template = map "$_\n", $conf->config($templatefile)
1942     or die "cannot load config data $templatefile";
1943
1944   my $old_latex = '';
1945   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1946     #change this to a die when the old code is removed
1947     warn "old-style invoice template $templatefile; ".
1948          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1949     $old_latex = 'true';
1950     @invoice_template = _translate_old_latex_format(@invoice_template);
1951   } 
1952
1953   my $text_template = new Text::Template(
1954     TYPE => 'ARRAY',
1955     SOURCE => \@invoice_template,
1956     DELIMITERS => $delimiters{$format},
1957   );
1958
1959   $text_template->compile()
1960     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1961
1962
1963   # additional substitution could possibly cause breakage in existing templates
1964   my %convert_maps = ( 
1965     'latex' => {
1966                  'notes'         => sub { map "$_", @_ },
1967                  'footer'        => sub { map "$_", @_ },
1968                  'smallfooter'   => sub { map "$_", @_ },
1969                  'returnaddress' => sub { map "$_", @_ },
1970                  'coupon'        => sub { map "$_", @_ },
1971                },
1972     'html'  => {
1973                  'notes' =>
1974                    sub {
1975                      map { 
1976                        s/%%(.*)$/<!-- $1 -->/g;
1977                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1978                        s/\\begin\{enumerate\}/<ol>/g;
1979                        s/\\item /  <li>/g;
1980                        s/\\end\{enumerate\}/<\/ol>/g;
1981                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1982                        s/\\\\\*/<br>/g;
1983                        s/\\dollar ?/\$/g;
1984                        s/\\#/#/g;
1985                        s/~/&nbsp;/g;
1986                        $_;
1987                      }  @_
1988                    },
1989                  'footer' =>
1990                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1991                  'smallfooter' =>
1992                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1993                  'returnaddress' =>
1994                    sub {
1995                      map { 
1996                        s/~/&nbsp;/g;
1997                        s/\\\\\*?\s*$/<BR>/;
1998                        s/\\hyphenation\{[\w\s\-]+}//;
1999                        s/\\([&])/$1/g;
2000                        $_;
2001                      }  @_
2002                    },
2003                  'coupon'        => sub { "" },
2004                },
2005     'template' => {
2006                  'notes' =>
2007                    sub {
2008                      map { 
2009                        s/%%.*$//g;
2010                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2011                        s/\\begin\{enumerate\}//g;
2012                        s/\\item /  * /g;
2013                        s/\\end\{enumerate\}//g;
2014                        s/\\textbf\{(.*)\}/$1/g;
2015                        s/\\\\\*/ /;
2016                        s/\\dollar ?/\$/g;
2017                        $_;
2018                      }  @_
2019                    },
2020                  'footer' =>
2021                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2022                  'smallfooter' =>
2023                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2024                  'returnaddress' =>
2025                    sub {
2026                      map { 
2027                        s/~/ /g;
2028                        s/\\\\\*?\s*$/\n/;             # dubious
2029                        s/\\hyphenation\{[\w\s\-]+}//;
2030                        $_;
2031                      }  @_
2032                    },
2033                  'coupon'        => sub { "" },
2034                },
2035   );
2036
2037
2038   # hashes for differing output formats
2039   my %nbsps = ( 'latex'    => '~',
2040                 'html'     => '',    # '&nbps;' would be nice
2041                 'template' => '',    # not used
2042               );
2043   my $nbsp = $nbsps{$format};
2044
2045   my %escape_functions = ( 'latex'    => \&_latex_escape,
2046                            'html'     => \&encode_entities,
2047                            'template' => sub { shift },
2048                          );
2049   my $escape_function = $escape_functions{$format};
2050
2051   my %date_formats = ( 'latex'    => '%b %o, %Y',
2052                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
2053                        'template' => '%s',
2054                      );
2055   my $date_format = $date_formats{$format};
2056
2057   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2058                                                },
2059                              'html'     => sub { return '<b>'. shift(). '</b>'
2060                                                },
2061                              'template' => sub { shift },
2062                            );
2063   my $embolden_function = $embolden_functions{$format};
2064
2065
2066   # generate template variables
2067   my $returnaddress;
2068   if (
2069          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2070                                         $template
2071                                       )
2072                 )
2073        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2074                                         $template
2075                                       )
2076                 )
2077   ) {
2078
2079     $returnaddress = join("\n",
2080       $conf->config_orbase("invoice_${format}returnaddress", $template)
2081     );
2082
2083   } elsif ( grep /\S/,
2084             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2085
2086     my $convert_map = $convert_maps{$format}{'returnaddress'};
2087     $returnaddress =
2088       join( "\n",
2089             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2090                                                  $template
2091                                                )
2092                          )
2093           );
2094   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2095
2096     my $convert_map = $convert_maps{$format}{'returnaddress'};
2097     $returnaddress = join( "\n", &$convert_map(
2098                                    map { s/( {2,})/'~' x length($1)/eg;
2099                                          s/$/\\\\\*/;
2100                                          $_
2101                                        }
2102                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2103                                        $conf->config('company_address', $self->cust_main->agentnum),
2104                                      )
2105                                  )
2106                      );
2107
2108   } else {
2109
2110     my $warning = "Couldn't find a return address; ".
2111                   "do you need to set the company_address configuration value?";
2112     warn "$warning\n";
2113     $returnaddress = $nbsp;
2114     #$returnaddress = $warning;
2115
2116   }
2117
2118   my %invoice_data = (
2119     'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2120     'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2121     'custnum'         => $cust_main->display_custnum,
2122     'invnum'          => $self->invnum,
2123     'date'            => time2str($date_format, $self->_date),
2124     'today'           => time2str('%b %o, %Y', $today),
2125     'agent'           => &$escape_function($cust_main->agent->agent),
2126     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2127     'payname'         => &$escape_function($cust_main->payname),
2128     'company'         => &$escape_function($cust_main->company),
2129     'address1'        => &$escape_function($cust_main->address1),
2130     'address2'        => &$escape_function($cust_main->address2),
2131     'city'            => &$escape_function($cust_main->city),
2132     'state'           => &$escape_function($cust_main->state),
2133     'zip'             => &$escape_function($cust_main->zip),
2134     'fax'             => &$escape_function($cust_main->fax),
2135     'returnaddress'   => $returnaddress,
2136     #'quantity'        => 1,
2137     'terms'           => $self->terms,
2138     'template'        => $template, #params{'template'},
2139     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
2140     # better hang on to conf_dir for a while
2141     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2142     'page'            => 1,
2143     'total_pages'     => 1,
2144     'current_charges' => sprintf("%.2f", $self->charged),
2145     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
2146     'ship_enable'     => $conf->exists('invoice-ship_address'),
2147     'unitprices'      => $conf->exists('invoice-unitprice'),
2148   );
2149
2150   my $countrydefault = $conf->config('countrydefault') || 'US';
2151   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2152   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2153     my $method = $prefix.$_;
2154     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2155   }
2156   $invoice_data{'ship_country'} = ''
2157     if ( $invoice_data{'ship_country'} eq $countrydefault );
2158   
2159   $invoice_data{'cid'} = $params{'cid'}
2160     if $params{'cid'};
2161
2162   if ( $cust_main->country eq $countrydefault ) {
2163     $invoice_data{'country'} = '';
2164   } else {
2165     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2166   }
2167
2168   my @address = ();
2169   $invoice_data{'address'} = \@address;
2170   push @address,
2171     $cust_main->payname.
2172       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2173         ? " (P.O. #". $cust_main->payinfo. ")"
2174         : ''
2175       )
2176   ;
2177   push @address, $cust_main->company
2178     if $cust_main->company;
2179   push @address, $cust_main->address1;
2180   push @address, $cust_main->address2
2181     if $cust_main->address2;
2182   push @address,
2183     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2184   push @address, $invoice_data{'country'}
2185     if $invoice_data{'country'};
2186   push @address, ''
2187     while (scalar(@address) < 5);
2188
2189   $invoice_data{'logo_file'} = $params{'logo_file'}
2190     if $params{'logo_file'};
2191
2192   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2193 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2194   #my $balance_due = $self->owed + $pr_total - $cr_total;
2195   my $balance_due = $self->owed + $pr_total;
2196   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2197   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2198
2199   my $agentnum = $self->cust_main->agentnum;
2200
2201   #do variable substitution in notes, footer, smallfooter
2202   foreach my $include (qw( notes footer smallfooter coupon )) {
2203
2204     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2205     my @inc_src;
2206
2207     if ( $conf->exists($inc_file, $agentnum)
2208          && length( $conf->config($inc_file, $agentnum) ) ) {
2209
2210       @inc_src = $conf->config($inc_file, $agentnum);
2211
2212     } else {
2213
2214       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2215
2216       my $convert_map = $convert_maps{$format}{$include};
2217
2218       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2219                        s/--\@\]/$delimiters{$format}[1]/g;
2220                        $_;
2221                      } 
2222                  &$convert_map( $conf->config($inc_file, $agentnum) );
2223
2224     }
2225
2226     my $inc_tt = new Text::Template (
2227       TYPE       => 'ARRAY',
2228       SOURCE     => [ map "$_\n", @inc_src ],
2229       DELIMITERS => $delimiters{$format},
2230     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2231
2232     unless ( $inc_tt->compile() ) {
2233       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2234       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2235       die $error;
2236     }
2237
2238     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2239
2240     $invoice_data{$include} =~ s/\n+$//
2241       if ($format eq 'latex');
2242   }
2243
2244   $invoice_data{'po_line'} =
2245     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2246       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2247       : $nbsp;
2248
2249   my %money_chars = ( 'latex'    => '',
2250                       'html'     => $conf->config('money_char') || '$',
2251                       'template' => '',
2252                     );
2253   my $money_char = $money_chars{$format};
2254
2255   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2256                             'html'     => $conf->config('money_char') || '$',
2257                             'template' => '',
2258                           );
2259   my $other_money_char = $other_money_chars{$format};
2260
2261   my @detail_items = ();
2262   my @total_items = ();
2263   my @buf = ();
2264   my @sections = ();
2265
2266   $invoice_data{'detail_items'} = \@detail_items;
2267   $invoice_data{'total_items'} = \@total_items;
2268   $invoice_data{'buf'} = \@buf;
2269   $invoice_data{'sections'} = \@sections;
2270
2271   my $previous_section = { 'description' => 'Previous Charges',
2272                            'subtotal'    => $other_money_char.
2273                                             sprintf('%.2f', $pr_total),
2274                          };
2275
2276   my $taxtotal = 0;
2277   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2278                       'subtotal'    => $taxtotal }; # adjusted below
2279
2280   my $adjusttotal = 0;
2281   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2282                          'subtotal'    => 0 }; # adjusted below
2283
2284   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2285   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2286   my $late_sections = [];
2287   if ( $multisection ) {
2288     push @sections, $self->_items_sections( $late_sections );
2289   }else{
2290     push @sections, { 'description' => '', 'subtotal' => '' };
2291   }
2292
2293   unless (    $conf->exists('disable_previous_balance')
2294            || $conf->exists('previous_balance-summary_only')
2295          )
2296   {
2297
2298     foreach my $line_item ( $self->_items_previous ) {
2299
2300       my $detail = {
2301         ext_description => [],
2302       };
2303       $detail->{'ref'} = $line_item->{'pkgnum'};
2304       $detail->{'quantity'} = 1;
2305       $detail->{'section'} = $previous_section;
2306       $detail->{'description'} = &$escape_function($line_item->{'description'});
2307       if ( exists $line_item->{'ext_description'} ) {
2308         @{$detail->{'ext_description'}} = map {
2309           &$escape_function($_);
2310         } @{$line_item->{'ext_description'}};
2311       }
2312       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2313                             $line_item->{'amount'};
2314       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2315
2316       push @detail_items, $detail;
2317       push @buf, [ $detail->{'description'},
2318                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2319                  ];
2320     }
2321
2322   }
2323
2324   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2325     push @buf, ['','-----------'];
2326     push @buf, [ 'Total Previous Balance',
2327                  $money_char. sprintf("%10.2f", $pr_total) ];
2328     push @buf, ['',''];
2329   }
2330
2331   foreach my $section (@sections, @$late_sections) {
2332
2333     $section->{'subtotal'} = $other_money_char.
2334                              sprintf('%.2f', $section->{'subtotal'})
2335       if $multisection;
2336
2337     if ( $section->{'description'} ) {
2338       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2339                    [ '', '' ],
2340                  );
2341     }
2342
2343     my %options = ();
2344     $options{'section'} = $section if $multisection;
2345     $options{'format'} = $format;
2346     $options{'escape_function'} = $escape_function;
2347     $options{'format_function'} = sub { () } unless $unsquelched;
2348     $options{'unsquelched'} = $unsquelched;
2349
2350     foreach my $line_item ( $self->_items_pkg(%options) ) {
2351       my $detail = {
2352         ext_description => [],
2353       };
2354       $detail->{'ref'} = $line_item->{'pkgnum'};
2355       $detail->{'quantity'} = $line_item->{'quantity'};
2356       $detail->{'section'} = $section;
2357       $detail->{'description'} = &$escape_function($line_item->{'description'});
2358       if ( exists $line_item->{'ext_description'} ) {
2359         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2360       }
2361       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2362                               $line_item->{'amount'};
2363       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2364                                  $line_item->{'unit_amount'};
2365       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2366   
2367       push @detail_items, $detail;
2368       push @buf, ( [ $detail->{'description'},
2369                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2370                    ],
2371                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2372                  );
2373     }
2374
2375     if ( $section->{'description'} ) {
2376       push @buf, ( ['','-----------'],
2377                    [ $section->{'description'}. ' sub-total',
2378                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2379                    ],
2380                    [ '', '' ],
2381                    [ '', '' ],
2382                  );
2383     }
2384   
2385   }
2386   
2387   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2388     unshift @sections, $previous_section if $pr_total;
2389   }
2390
2391   foreach my $tax ( $self->_items_tax ) {
2392
2393     $taxtotal += $tax->{'amount'};
2394
2395     my $description = &$escape_function( $tax->{'description'} );
2396     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2397
2398     if ( $multisection ) {
2399
2400       my $money = $old_latex ? '' : $money_char;
2401       push @detail_items, {
2402         ext_description => [],
2403         ref          => '',
2404         quantity     => '',
2405         description  => $description,
2406         amount       => $money. $amount,
2407         product_code => '',
2408         section      => $tax_section,
2409       };
2410
2411     } else {
2412
2413       push @total_items, {
2414         'total_item'   => $description,
2415         'total_amount' => $other_money_char. $amount,
2416       };
2417
2418     }
2419
2420     push @buf,[ $description,
2421                 $money_char. $amount,
2422               ];
2423
2424   }
2425   
2426   if ( $taxtotal ) {
2427     my $total = {};
2428     $total->{'total_item'} = 'Sub-total';
2429     $total->{'total_amount'} =
2430       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2431
2432     if ( $multisection ) {
2433       $tax_section->{'subtotal'} = $other_money_char.
2434                                    sprintf('%.2f', $taxtotal);
2435       $tax_section->{'pretotal'} = 'New charges sub-total '.
2436                                    $total->{'total_amount'};
2437       push @sections, $tax_section if $taxtotal;
2438     }else{
2439       unshift @total_items, $total;
2440     }
2441   }
2442   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2443
2444   push @buf,['','-----------'];
2445   push @buf,[( $conf->exists('disable_previous_balance') 
2446                ? 'Total Charges'
2447                : 'Total New Charges'
2448              ),
2449              $money_char. sprintf("%10.2f",$self->charged) ];
2450   push @buf,['',''];
2451
2452   {
2453     my $total = {};
2454     $total->{'total_item'} = &$embolden_function('Total');
2455     $total->{'total_amount'} =
2456       &$embolden_function(
2457         $other_money_char.
2458         sprintf( '%.2f',
2459                  $self->charged + ( $conf->exists('disable_previous_balance')
2460                                     ? 0
2461                                     : $pr_total
2462                                   )
2463                )
2464       );
2465     if ( $multisection ) {
2466       $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2467                                       sprintf('%.2f', $self->charged );
2468     }else{
2469       push @total_items, $total;
2470     }
2471     push @buf,['','-----------'];
2472     push @buf,['Total Charges',
2473                $money_char.
2474                sprintf( '%10.2f', $self->charged +
2475                                     ( $conf->exists('disable_previous_balance')
2476                                         ? 0
2477                                         : $pr_total
2478                                     )
2479                       )
2480               ];
2481     push @buf,['',''];
2482   }
2483   
2484   unless ( $conf->exists('disable_previous_balance') ) {
2485     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2486   
2487     # credits
2488     my $credittotal = 0;
2489     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2490
2491       my $total;
2492       $total->{'total_item'} = &$escape_function($credit->{'description'});
2493       $credittotal += $credit->{'amount'};
2494       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2495       $adjusttotal += $credit->{'amount'};
2496       if ( $multisection ) {
2497         my $money = $old_latex ? '' : $money_char;
2498         push @detail_items, {
2499           ext_description => [],
2500           ref          => '',
2501           quantity     => '',
2502           description  => &$escape_function($credit->{'description'}),
2503           amount       => $money. $credit->{'amount'},
2504           product_code => '',
2505           section      => $adjust_section,
2506         };
2507       } else {
2508         push @total_items, $total;
2509       }
2510
2511     }
2512     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2513
2514     #credits (again)
2515     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2516       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2517     }
2518
2519     # payments
2520     my $paymenttotal = 0;
2521     foreach my $payment ( $self->_items_payments ) {
2522       my $total = {};
2523       $total->{'total_item'} = &$escape_function($payment->{'description'});
2524       $paymenttotal += $payment->{'amount'};
2525       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2526       $adjusttotal += $payment->{'amount'};
2527       if ( $multisection ) {
2528         my $money = $old_latex ? '' : $money_char;
2529         push @detail_items, {
2530           ext_description => [],
2531           ref          => '',
2532           quantity     => '',
2533           description  => &$escape_function($payment->{'description'}),
2534           amount       => $money. $payment->{'amount'},
2535           product_code => '',
2536           section      => $adjust_section,
2537         };
2538       }else{
2539         push @total_items, $total;
2540       }
2541       push @buf, [ $payment->{'description'},
2542                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2543                  ];
2544     }
2545     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2546   
2547     if ( $multisection ) {
2548       $adjust_section->{'subtotal'} = $other_money_char.
2549                                       sprintf('%.2f', $adjusttotal);
2550       push @sections, $adjust_section;
2551     }
2552
2553     { 
2554       my $total;
2555       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2556       $total->{'total_amount'} =
2557         &$embolden_function(
2558           $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2559         );
2560       if ( $multisection ) {
2561         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2562                                          $total->{'total_amount'};
2563       }else{
2564         push @total_items, $total;
2565       }
2566       push @buf,['','-----------'];
2567       push @buf,[$self->balance_due_msg, $money_char. 
2568         sprintf("%10.2f", $balance_due ) ];
2569     }
2570   }
2571
2572   if ( $multisection ) {
2573     push @sections, @$late_sections
2574       if $unsquelched;
2575   }
2576
2577   $invoice_lines = 0;
2578   my $wasfunc = 0;
2579   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2580     /invoice_lines\((\d*)\)/;
2581     $invoice_lines += $1 || scalar(@buf);
2582     $wasfunc=1;
2583   }
2584   die "no invoice_lines() functions in template?"
2585     if ( $format eq 'template' && !$wasfunc );
2586
2587   if ($format eq 'template') {
2588
2589     if ( $invoice_lines ) {
2590       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2591       $invoice_data{'total_pages'}++
2592         if scalar(@buf) % $invoice_lines;
2593     }
2594
2595     #setup subroutine for the template
2596     sub FS::cust_bill::_template::invoice_lines {
2597       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2598       map { 
2599         scalar(@FS::cust_bill::_template::buf)
2600           ? shift @FS::cust_bill::_template::buf
2601           : [ '', '' ];
2602       }
2603       ( 1 .. $lines );
2604     }
2605
2606     my $lines;
2607     my @collect;
2608     while (@buf) {
2609       push @collect, split("\n",
2610         $text_template->fill_in( HASH => \%invoice_data,
2611                                  PACKAGE => 'FS::cust_bill::_template'
2612                                )
2613       );
2614       $FS::cust_bill::_template::page++;
2615     }
2616     map "$_\n", @collect;
2617   }else{
2618     warn "filling in template for invoice ". $self->invnum. "\n"
2619       if $DEBUG;
2620     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2621       if $DEBUG > 1;
2622
2623     $text_template->fill_in(HASH => \%invoice_data);
2624   }
2625 }
2626
2627 =item print_ps [ TIME [ , TEMPLATE ] ]
2628
2629 Returns an postscript invoice, as a scalar.
2630
2631 TIME an optional value used to control the printing of overdue messages.  The
2632 default is now.  It isn't the date of the invoice; that's the `_date' field.
2633 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2634 L<Time::Local> and L<Date::Parse> for conversion functions.
2635
2636 =cut
2637
2638 sub print_ps {
2639   my $self = shift;
2640
2641   my ($file, $lfile) = $self->print_latex(@_);
2642   my $ps = generate_ps($file);
2643   unlink($lfile);
2644
2645   $ps;
2646 }
2647
2648 =item print_pdf [ TIME [ , TEMPLATE ] ]
2649
2650 Returns an PDF invoice, as a scalar.
2651
2652 TIME an optional value used to control the printing of overdue messages.  The
2653 default is now.  It isn't the date of the invoice; that's the `_date' field.
2654 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2655 L<Time::Local> and L<Date::Parse> for conversion functions.
2656
2657 =cut
2658
2659 sub print_pdf {
2660   my $self = shift;
2661
2662   my ($file, $lfile) = $self->print_latex(@_);
2663   my $pdf = generate_pdf($file);
2664   unlink($lfile);
2665
2666   $pdf;
2667 }
2668
2669 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2670
2671 Returns an HTML invoice, as a scalar.
2672
2673 TIME an optional value used to control the printing of overdue messages.  The
2674 default is now.  It isn't the date of the invoice; that's the `_date' field.
2675 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2676 L<Time::Local> and L<Date::Parse> for conversion functions.
2677
2678 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2679 when emailing the invoice as part of a multipart/related MIME email.
2680
2681 =cut
2682
2683 sub print_html {
2684   my $self = shift;
2685   my %params;
2686   if ( ref $_[0]  ) {
2687     %params = %{ shift() }; 
2688   }else{
2689     $params{'time'} = shift;
2690     $params{'template'} = shift;
2691     $params{'cid'} = shift;
2692   }
2693
2694   $params{'format'} = 'html';
2695
2696   $self->print_generic( %params );
2697 }
2698
2699 # quick subroutine for print_latex
2700 #
2701 # There are ten characters that LaTeX treats as special characters, which
2702 # means that they do not simply typeset themselves: 
2703 #      # $ % & ~ _ ^ \ { }
2704 #
2705 # TeX ignores blanks following an escaped character; if you want a blank (as
2706 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2707
2708 sub _latex_escape {
2709   my $value = shift;
2710   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2711   $value =~ s/([<>])/\$$1\$/g;
2712   $value;
2713 }
2714
2715 #utility methods for print_*
2716
2717 sub _translate_old_latex_format {
2718   warn "_translate_old_latex_format called\n"
2719     if $DEBUG; 
2720
2721   my @template = ();
2722   while ( @_ ) {
2723     my $line = shift;
2724   
2725     if ( $line =~ /^%%Detail\s*$/ ) {
2726   
2727       push @template, q![@--!,
2728                       q!  foreach my $_tr_line (@detail_items) {!,
2729                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2730                       q!      $_tr_line->{'description'} .= !, 
2731                       q!        "\\tabularnewline\n~~".!,
2732                       q!        join( "\\tabularnewline\n~~",!,
2733                       q!          @{$_tr_line->{'ext_description'}}!,
2734                       q!        );!,
2735                       q!    }!;
2736
2737       while ( ( my $line_item_line = shift )
2738               !~ /^%%EndDetail\s*$/                            ) {
2739         $line_item_line =~ s/'/\\'/g;    # nice LTS
2740         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2741         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2742         push @template, "    \$OUT .= '$line_item_line';";
2743       }
2744   
2745       push @template, '}',
2746                       '--@]';
2747
2748     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2749
2750       push @template, '[@--',
2751                       '  foreach my $_tr_line (@total_items) {';
2752
2753       while ( ( my $total_item_line = shift )
2754               !~ /^%%EndTotalDetails\s*$/                      ) {
2755         $total_item_line =~ s/'/\\'/g;    # nice LTS
2756         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2757         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2758         push @template, "    \$OUT .= '$total_item_line';";
2759       }
2760
2761       push @template, '}',
2762                       '--@]';
2763
2764     } else {
2765       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2766       push @template, $line;  
2767     }
2768   
2769   }
2770
2771   if ($DEBUG) {
2772     warn "$_\n" foreach @template;
2773   }
2774
2775   (@template);
2776 }
2777
2778 sub terms {
2779   my $self = shift;
2780
2781   #check for an invoice- specific override (eventually)
2782   
2783   #check for a customer- specific override
2784   return $self->cust_main->invoice_terms
2785     if $self->cust_main->invoice_terms;
2786
2787   #use configured default
2788   $conf->config('invoice_default_terms') || '';
2789 }
2790
2791 sub due_date {
2792   my $self = shift;
2793   my $duedate = '';
2794   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2795     $duedate = $self->_date() + ( $1 * 86400 );
2796   }
2797   $duedate;
2798 }
2799
2800 sub due_date2str {
2801   my $self = shift;
2802   $self->due_date ? time2str(shift, $self->due_date) : '';
2803 }
2804
2805 sub balance_due_msg {
2806   my $self = shift;
2807   my $msg = 'Balance Due';
2808   return $msg unless $self->terms;
2809   if ( $self->due_date ) {
2810     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2811   } elsif ( $self->terms ) {
2812     $msg .= ' - '. $self->terms;
2813   }
2814   $msg;
2815 }
2816
2817 sub balance_due_date {
2818   my $self = shift;
2819   my $duedate = '';
2820   if (    $conf->exists('invoice_default_terms') 
2821        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2822     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2823   }
2824   $duedate;
2825 }
2826
2827 =item invnum_date_pretty
2828
2829 Returns a string with the invoice number and date, for example:
2830 "Invoice #54 (3/20/2008)"
2831
2832 =cut
2833
2834 sub invnum_date_pretty {
2835   my $self = shift;
2836   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2837 }
2838
2839 =item _date_pretty
2840
2841 Returns a string with the date, for example: "3/20/2008"
2842
2843 =cut
2844
2845 sub _date_pretty {
2846   my $self = shift;
2847   time2str('%x', $self->_date);
2848 }
2849
2850 sub _items_sections {
2851   my $self = shift;
2852   my $late = shift;
2853
2854   my %s = ();
2855   my %l = ();
2856
2857   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2858   {
2859
2860     if ( $cust_bill_pkg->pkgnum > 0 ) {
2861       my $usage = $cust_bill_pkg->usage;
2862
2863       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2864         my $desc = $display->section;
2865         my $type = $display->type;
2866
2867         if ( $display->post_total ) {
2868           if (! $type || $type eq 'S') {
2869             $l{$desc} += $cust_bill_pkg->setup
2870               if ( $cust_bill_pkg->setup != 0 );
2871           }
2872
2873           if (! $type) {
2874             $l{$desc} += $cust_bill_pkg->recur
2875               if ( $cust_bill_pkg->recur != 0 );
2876           }
2877
2878           if ($type && $type eq 'R') {
2879             $l{$desc} += $cust_bill_pkg->recur - $usage
2880               if ( $cust_bill_pkg->recur != 0 );
2881           }
2882           
2883           if ($type && $type eq 'U') {
2884             $l{$desc} += $usage;
2885           }
2886
2887         } else {
2888           if (! $type || $type eq 'S') {
2889             $s{$desc} += $cust_bill_pkg->setup
2890               if ( $cust_bill_pkg->setup != 0 );
2891           }
2892
2893           if (! $type) {
2894             $s{$desc} += $cust_bill_pkg->recur
2895               if ( $cust_bill_pkg->recur != 0 );
2896           }
2897
2898           if ($type && $type eq 'R') {
2899             $s{$desc} += $cust_bill_pkg->recur - $usage
2900               if ( $cust_bill_pkg->recur != 0 );
2901           }
2902           
2903           if ($type && $type eq 'U') {
2904             $s{$desc} += $usage;
2905           }
2906
2907         }
2908
2909       }
2910
2911     }
2912
2913   }
2914
2915   push @$late, map { { 'description' => $_,
2916                        'subtotal'    => $l{$_},
2917                        'post_total'  => 1,
2918                    } } sort keys %l;
2919
2920   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2921
2922 }
2923
2924 sub _items {
2925   my $self = shift;
2926
2927   #my @display = scalar(@_)
2928   #              ? @_
2929   #              : qw( _items_previous _items_pkg );
2930   #              #: qw( _items_pkg );
2931   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2932   my @display = qw( _items_previous _items_pkg );
2933
2934   my @b = ();
2935   foreach my $display ( @display ) {
2936     push @b, $self->$display(@_);
2937   }
2938   @b;
2939 }
2940
2941 sub _items_previous {
2942   my $self = shift;
2943   my $cust_main = $self->cust_main;
2944   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2945   my @b = ();
2946   foreach ( @pr_cust_bill ) {
2947     push @b, {
2948       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2949                        ' ('. time2str('%x',$_->_date). ')',
2950       #'pkgpart'     => 'N/A',
2951       'pkgnum'      => 'N/A',
2952       'amount'      => sprintf("%.2f", $_->owed),
2953     };
2954   }
2955   @b;
2956
2957   #{
2958   #    'description'     => 'Previous Balance',
2959   #    #'pkgpart'         => 'N/A',
2960   #    'pkgnum'          => 'N/A',
2961   #    'amount'          => sprintf("%10.2f", $pr_total ),
2962   #    'ext_description' => [ map {
2963   #                                 "Invoice ". $_->invnum.
2964   #                                 " (". time2str("%x",$_->_date). ") ".
2965   #                                 sprintf("%10.2f", $_->owed)
2966   #                         } @pr_cust_bill ],
2967
2968   #};
2969 }
2970
2971 sub _items_pkg {
2972   my $self = shift;
2973   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2974   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2975 }
2976
2977 sub _taxsort {
2978   return 0 unless $a cmp $b;
2979   return -1 if $b eq 'Tax';
2980   return 1 if $a eq 'Tax';
2981   return -1 if $b eq 'Other surcharges';
2982   return 1 if $a eq 'Other surcharges';
2983   $a cmp $b;
2984 }
2985
2986 sub _items_tax {
2987   my $self = shift;
2988   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2989   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2990 }
2991
2992 sub _items_cust_bill_pkg {
2993   my $self = shift;
2994   my $cust_bill_pkg = shift;
2995   my %opt = @_;
2996
2997   my $format = $opt{format} || '';
2998   my $escape_function = $opt{escape_function} || sub { shift };
2999   my $format_function = $opt{format_function} || '';
3000   my $unsquelched = $opt{unsquelched} || '';
3001   my $section = $opt{section}->{description} if $opt{section};
3002
3003   my @b = ();
3004   my ($s, $r, $u) = ( undef, undef, undef );
3005   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3006   {
3007
3008     foreach ( $s, $r, $u ) {
3009       if ( $_ && !$cust_bill_pkg->hidden ) {
3010         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3011         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3012         push @b, { %$_ };
3013         $_ = undef;
3014       }
3015     }
3016
3017     foreach my $display ( grep { defined($section)
3018                                  ? $_->section eq $section
3019                                  : 1
3020                                }
3021                           $cust_bill_pkg->cust_bill_pkg_display
3022                         )
3023     {
3024
3025       my $type = $display->type;
3026
3027       my $desc = $cust_bill_pkg->desc;
3028       $desc = substr($desc, 0, 50). '...'
3029         if $format eq 'latex' && length($desc) > 50;
3030
3031       my %details_opt = ( 'format'          => $format,
3032                           'escape_function' => $escape_function,
3033                           'format_function' => $format_function,
3034                         );
3035
3036       if ( $cust_bill_pkg->pkgnum > 0 ) {
3037
3038         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3039
3040         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3041
3042           my $description = $desc;
3043           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3044
3045           my @d = ();
3046           push @d, map &{$escape_function}($_),
3047                        $cust_pkg->h_labels_short($self->_date)
3048             unless $cust_pkg->part_pkg->hide_svc_detail
3049                 || $cust_bill_pkg->hidden;
3050           push @d, $cust_bill_pkg->details(%details_opt)
3051             if $cust_bill_pkg->recur == 0;
3052
3053           if ( $cust_bill_pkg->hidden ) {
3054             $s->{amount}      += $cust_bill_pkg->setup;
3055             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3056             push @{ $s->{ext_description} }, @d;
3057           } else {
3058             $s = {
3059               description     => $description,
3060               #pkgpart         => $part_pkg->pkgpart,
3061               pkgnum          => $cust_bill_pkg->pkgnum,
3062               amount          => $cust_bill_pkg->setup,
3063               unit_amount     => $cust_bill_pkg->unitsetup,
3064               quantity        => $cust_bill_pkg->quantity,
3065               ext_description => \@d,
3066             };
3067           };
3068
3069         }
3070
3071         if ( $cust_bill_pkg->recur != 0 &&
3072              ( !$type || $type eq 'R' || $type eq 'U' )
3073            )
3074         {
3075
3076           my $is_summary = $display->summary;
3077           my $description = $is_summary ? "Usage charges" : $desc;
3078
3079           unless ( $conf->exists('disable_line_item_date_ranges') ) {
3080             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3081                             " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3082           }
3083
3084           my @d = ();
3085
3086           #at least until cust_bill_pkg has "past" ranges in addition to
3087           #the "future" sdate/edate ones... see #3032
3088           my @dates = ( $self->_date );
3089           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3090           push @dates, $prev->sdate if $prev;
3091
3092           push @d, map &{$escape_function}($_),
3093                        $cust_pkg->h_labels_short(@dates)
3094                                                  #$cust_bill_pkg->edate,
3095                                                  #$cust_bill_pkg->sdate)
3096             unless $cust_pkg->part_pkg->hide_svc_detail
3097                 || $cust_bill_pkg->itemdesc
3098                 || $cust_bill_pkg->hidden
3099                 || $is_summary;
3100
3101           push @d, $cust_bill_pkg->details(%details_opt)
3102             unless ($is_summary || $type && $type eq 'R');
3103   
3104           my $amount = 0;
3105           if (!$type) {
3106             $amount = $cust_bill_pkg->recur;
3107           }elsif($type eq 'R') {
3108             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3109           }elsif($type eq 'U') {
3110             $amount = $cust_bill_pkg->usage;
3111           }
3112   
3113           if ( !$type || $type eq 'R' ) {
3114
3115             if ( $cust_bill_pkg->hidden ) {
3116               $r->{amount}      += $amount;
3117               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3118               push @{ $r->{ext_description} }, @d;
3119             } else {
3120               $r = {
3121                 description     => $description,
3122                 #pkgpart         => $part_pkg->pkgpart,
3123                 pkgnum          => $cust_bill_pkg->pkgnum,
3124                 amount          => $amount,
3125                 unit_amount     => $cust_bill_pkg->unitrecur,
3126                 quantity        => $cust_bill_pkg->quantity,
3127                 ext_description => \@d,
3128               };
3129             }
3130
3131           } elsif ( $amount ) {  # && $type eq 'U'
3132
3133             if ( $cust_bill_pkg->hidden ) {
3134               $u->{amount}      += $amount;
3135               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3136               push @{ $u->{ext_description} }, @d;
3137             } else {
3138               $u = {
3139                 description     => $description,
3140                 #pkgpart         => $part_pkg->pkgpart,
3141                 pkgnum          => $cust_bill_pkg->pkgnum,
3142                 amount          => $amount,
3143                 unit_amount     => $cust_bill_pkg->unitrecur,
3144                 quantity        => $cust_bill_pkg->quantity,
3145                 ext_description => \@d,
3146               };
3147             }
3148
3149           }
3150
3151         } # recurring or usage with recurring charge
3152
3153       } else { #pkgnum tax or one-shot line item (??)
3154
3155         if ( $cust_bill_pkg->setup != 0 ) {
3156           push @b, {
3157             'description' => $desc,
3158             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3159           };
3160         }
3161         if ( $cust_bill_pkg->recur != 0 ) {
3162           push @b, {
3163             'description' => "$desc (".
3164                              time2str("%x", $cust_bill_pkg->sdate). ' - '.
3165                              time2str("%x", $cust_bill_pkg->edate). ')',
3166             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3167           };
3168         }
3169
3170       }
3171
3172     }
3173
3174   }
3175
3176   foreach ( $s, $r, $u ) {
3177     if ( $_ ) {
3178       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3179       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3180       push @b, { %$_ };
3181     }
3182   }
3183
3184   @b;
3185
3186 }
3187
3188 sub _items_credits {
3189   my( $self, %opt ) = @_;
3190   my $trim_len = $opt{'trim_len'} || 60;
3191
3192   my @b;
3193   #credits
3194   foreach ( $self->cust_credited ) {
3195
3196     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3197
3198     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3199     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3200     $reason = " ($reason) " if $reason;
3201
3202     push @b, {
3203       #'description' => 'Credit ref\#'. $_->crednum.
3204       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
3205       #                 $reason,
3206       'description' => 'Credit applied '.
3207                        time2str("%x",$_->cust_credit->_date). $reason,
3208       'amount'      => sprintf("%.2f",$_->amount),
3209     };
3210   }
3211
3212   @b;
3213
3214 }
3215
3216 sub _items_payments {
3217   my $self = shift;
3218
3219   my @b;
3220   #get & print payments
3221   foreach ( $self->cust_bill_pay ) {
3222
3223     #something more elaborate if $_->amount ne ->cust_pay->paid ?
3224
3225     push @b, {
3226       'description' => "Payment received ".
3227                        time2str("%x",$_->cust_pay->_date ),
3228       'amount'      => sprintf("%.2f", $_->amount )
3229     };
3230   }
3231
3232   @b;
3233
3234 }
3235
3236 =item call_details [ OPTION => VALUE ... ]
3237
3238 Returns an array of CSV strings representing the call details for this invoice
3239 The only option available is the boolean prepend_billed_number
3240
3241 =cut
3242
3243 sub call_details {
3244   my ($self, %opt) = @_;
3245
3246   my $format_function = sub { shift };
3247
3248   if ($opt{prepend_billed_number}) {
3249     $format_function = sub {
3250       my $detail = shift;
3251       my $row = shift;
3252
3253       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3254       
3255     };
3256   }
3257
3258   my @details = map { $_->details( 'format_function' => $format_function,
3259                                    'escape_function' => sub{ return() },
3260                                  )
3261                     }
3262                   grep { $_->pkgnum }
3263                   $self->cust_bill_pkg;
3264   my $header = $details[0];
3265   ( $header, grep { $_ ne $header } @details );
3266 }
3267
3268
3269 =back
3270
3271 =head1 SUBROUTINES
3272
3273 =over 4
3274
3275 =item process_reprint
3276
3277 =cut
3278
3279 sub process_reprint {
3280   process_re_X('print', @_);
3281 }
3282
3283 =item process_reemail
3284
3285 =cut
3286
3287 sub process_reemail {
3288   process_re_X('email', @_);
3289 }
3290
3291 =item process_refax
3292
3293 =cut
3294
3295 sub process_refax {
3296   process_re_X('fax', @_);
3297 }
3298
3299 =item process_reftp
3300
3301 =cut
3302
3303 sub process_reftp {
3304   process_re_X('ftp', @_);
3305 }
3306
3307 =item respool
3308
3309 =cut
3310
3311 sub process_respool {
3312   process_re_X('spool', @_);
3313 }
3314
3315 use Storable qw(thaw);
3316 use Data::Dumper;
3317 use MIME::Base64;
3318 sub process_re_X {
3319   my( $method, $job ) = ( shift, shift );
3320   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3321
3322   my $param = thaw(decode_base64(shift));
3323   warn Dumper($param) if $DEBUG;
3324
3325   re_X(
3326     $method,
3327     $job,
3328     %$param,
3329   );
3330
3331 }
3332
3333 sub re_X {
3334   my($method, $job, %param ) = @_;
3335   if ( $DEBUG ) {
3336     warn "re_X $method for job $job with param:\n".
3337          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3338   }
3339
3340   #some false laziness w/search/cust_bill.html
3341   my $distinct = '';
3342   my $orderby = 'ORDER BY cust_bill._date';
3343
3344   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3345
3346   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3347      
3348   my @cust_bill = qsearch( {
3349     #'select'    => "cust_bill.*",
3350     'table'     => 'cust_bill',
3351     'addl_from' => $addl_from,
3352     'hashref'   => {},
3353     'extra_sql' => $extra_sql,
3354     'order_by'  => $orderby,
3355     'debug' => 1,
3356   } );
3357
3358   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3359
3360   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3361     if $DEBUG;
3362
3363   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3364   foreach my $cust_bill ( @cust_bill ) {
3365     $cust_bill->$method();
3366
3367     if ( $job ) { #progressbar foo
3368       $num++;
3369       if ( time - $min_sec > $last ) {
3370         my $error = $job->update_statustext(
3371           int( 100 * $num / scalar(@cust_bill) )
3372         );
3373         die $error if $error;
3374         $last = time;
3375       }
3376     }
3377
3378   }
3379
3380 }
3381
3382 =back
3383
3384 =head1 CLASS METHODS
3385
3386 =over 4
3387
3388 =item owed_sql
3389
3390 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3391
3392 =cut
3393
3394 sub owed_sql {
3395   my $class = shift;
3396   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3397 }
3398
3399 =item net_sql
3400
3401 Returns an SQL fragment to retreive the net amount (charged minus credited).
3402
3403 =cut
3404
3405 sub net_sql {
3406   my $class = shift;
3407   'charged - '. $class->credited_sql;
3408 }
3409
3410 =item paid_sql
3411
3412 Returns an SQL fragment to retreive the amount paid against this invoice.
3413
3414 =cut
3415
3416 sub paid_sql {
3417   #my $class = shift;
3418   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3419        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
3420 }
3421
3422 =item credited_sql
3423
3424 Returns an SQL fragment to retreive the amount credited against this invoice.
3425
3426 =cut
3427
3428 sub credited_sql {
3429   #my $class = shift;
3430   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3431        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
3432 }
3433
3434 =item search_sql HASHREF
3435
3436 Class method which returns an SQL WHERE fragment to search for parameters
3437 specified in HASHREF.  Valid parameters are
3438
3439 =over 4
3440
3441 =item begin
3442
3443 Epoch date (UNIX timestamp) setting a lower bound for _date values
3444
3445 =item end
3446
3447 Epoch date (UNIX timestamp) setting an upper bound for _date values
3448
3449 =item invnum_min
3450
3451 =item invnum_max
3452
3453 =item agentnum
3454
3455 =item owed
3456
3457 =item net
3458
3459 =item days
3460
3461 =item newest_percust
3462
3463 =back
3464
3465 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3466
3467 =cut
3468
3469 sub search_sql {
3470   my($class, $param) = @_;
3471   if ( $DEBUG ) {
3472     warn "$me search_sql called with params: \n".
3473          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3474   }
3475
3476   my @search = ();
3477
3478   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3479     push @search, "cust_bill._date >= $1";
3480   }
3481   if ( $param->{'end'} =~ /^(\d+)$/ ) {
3482     push @search, "cust_bill._date < $1";
3483   }
3484   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3485     push @search, "cust_bill.invnum >= $1";
3486   }
3487   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3488     push @search, "cust_bill.invnum <= $1";
3489   }
3490   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3491     push @search, "cust_main.agentnum = $1";
3492   }
3493
3494   push @search, '0 != '. FS::cust_bill->owed_sql
3495     if $param->{'open'};
3496
3497   push @search, '0 != '. FS::cust_bill->net_sql
3498     if $param->{'net'};
3499
3500   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3501     if $param->{'days'};
3502
3503   if ( $param->{'newest_percust'} ) {
3504
3505     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3506     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3507
3508     my @newest_where = map { my $x = $_;
3509                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3510                              $x;
3511                            }
3512                            grep ! /^cust_main./, @search;
3513     my $newest_where = scalar(@newest_where)
3514                          ? ' AND '. join(' AND ', @newest_where)
3515                          : '';
3516
3517
3518     push @search, "cust_bill._date = (
3519       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3520         WHERE newest_cust_bill.custnum = cust_bill.custnum
3521           $newest_where
3522     )";
3523
3524   }
3525
3526   my $curuser = $FS::CurrentUser::CurrentUser;
3527   if ( $curuser->username eq 'fs_queue'
3528        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3529     my $username = $1;
3530     my $newuser = qsearchs('access_user', {
3531       'username' => $username,
3532       'disabled' => '',
3533     } );
3534     if ( $newuser ) {
3535       $curuser = $newuser;
3536     } else {
3537       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3538     }
3539   }
3540
3541   push @search, $curuser->agentnums_sql;
3542
3543   join(' AND ', @search );
3544
3545 }
3546
3547 =back
3548
3549 =head1 BUGS
3550
3551 The delete method.
3552
3553 =head1 SEE ALSO
3554
3555 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3556 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3557 documentation.
3558
3559 =cut
3560
3561 1;
3562