html invoices!
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Date::Format;
7 use Text::Template 1.20;
8 use File::Temp 0.14;
9 use String::ShellQuote;
10 use HTML::Entities;
11 use Locale::Country;
12 use FS::UID qw( datasrc );
13 use FS::Record qw( qsearch qsearchs );
14 use FS::Misc qw( send_email send_fax );
15 use FS::cust_main;
16 use FS::cust_bill_pkg;
17 use FS::cust_credit;
18 use FS::cust_pay;
19 use FS::cust_pkg;
20 use FS::cust_credit_bill;
21 use FS::cust_pay_batch;
22 use FS::cust_bill_event;
23
24 @ISA = qw( FS::Record );
25
26 $DEBUG = 1;
27
28 #ask FS::UID to run this stuff for us later
29 FS::UID->install_callback( sub { 
30   $conf = new FS::Conf;
31   $money_char = $conf->config('money_char') || '$';  
32 } );
33
34 =head1 NAME
35
36 FS::cust_bill - Object methods for cust_bill records
37
38 =head1 SYNOPSIS
39
40   use FS::cust_bill;
41
42   $record = new FS::cust_bill \%hash;
43   $record = new FS::cust_bill { 'column' => 'value' };
44
45   $error = $record->insert;
46
47   $error = $new_record->replace($old_record);
48
49   $error = $record->delete;
50
51   $error = $record->check;
52
53   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
54
55   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
56
57   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
58
59   @cust_pay_objects = $cust_bill->cust_pay;
60
61   $tax_amount = $record->tax;
62
63   @lines = $cust_bill->print_text;
64   @lines = $cust_bill->print_text $time;
65
66 =head1 DESCRIPTION
67
68 An FS::cust_bill object represents an invoice; a declaration that a customer
69 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
70 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
71 following fields are currently supported:
72
73 =over 4
74
75 =item invnum - primary key (assigned automatically for new invoices)
76
77 =item custnum - customer (see L<FS::cust_main>)
78
79 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
80 L<Time::Local> and L<Date::Parse> for conversion functions.
81
82 =item charged - amount of this invoice
83
84 =item printed - deprecated
85
86 =item closed - books closed flag, empty or `Y'
87
88 =back
89
90 =head1 METHODS
91
92 =over 4
93
94 =item new HASHREF
95
96 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
97 Invoices are normally created by calling the bill method of a customer object
98 (see L<FS::cust_main>).
99
100 =cut
101
102 sub table { 'cust_bill'; }
103
104 =item insert
105
106 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
107 returns the error, otherwise returns false.
108
109 =item delete
110
111 Currently unimplemented.  I don't remove invoices because there would then be
112 no record you ever posted this invoice (which is bad, no?)
113
114 =cut
115
116 sub delete {
117   my $self = shift;
118   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
119   $self->SUPER::delete(@_);
120 }
121
122 =item replace OLD_RECORD
123
124 Replaces the OLD_RECORD with this one in the database.  If there is an error,
125 returns the error, otherwise returns false.
126
127 Only printed may be changed.  printed is normally updated by calling the
128 collect method of a customer object (see L<FS::cust_main>).
129
130 =cut
131
132 sub replace {
133   my( $new, $old ) = ( shift, shift );
134   return "Can't change custnum!" unless $old->custnum == $new->custnum;
135   #return "Can't change _date!" unless $old->_date eq $new->_date;
136   return "Can't change _date!" unless $old->_date == $new->_date;
137   return "Can't change charged!" unless $old->charged == $new->charged;
138
139   $new->SUPER::replace($old);
140 }
141
142 =item check
143
144 Checks all fields to make sure this is a valid invoice.  If there is an error,
145 returns the error, otherwise returns false.  Called by the insert and replace
146 methods.
147
148 =cut
149
150 sub check {
151   my $self = shift;
152
153   my $error =
154     $self->ut_numbern('invnum')
155     || $self->ut_number('custnum')
156     || $self->ut_numbern('_date')
157     || $self->ut_money('charged')
158     || $self->ut_numbern('printed')
159     || $self->ut_enum('closed', [ '', 'Y' ])
160   ;
161   return $error if $error;
162
163   return "Unknown customer"
164     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
165
166   $self->_date(time) unless $self->_date;
167
168   $self->printed(0) if $self->printed eq '';
169
170   $self->SUPER::check;
171 }
172
173 =item previous
174
175 Returns a list consisting of the total previous balance for this customer, 
176 followed by the previous outstanding invoices (as FS::cust_bill objects also).
177
178 =cut
179
180 sub previous {
181   my $self = shift;
182   my $total = 0;
183   my @cust_bill = sort { $a->_date <=> $b->_date }
184     grep { $_->owed != 0 && $_->_date < $self->_date }
185       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
186   ;
187   foreach ( @cust_bill ) { $total += $_->owed; }
188   $total, @cust_bill;
189 }
190
191 =item cust_bill_pkg
192
193 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
194
195 =cut
196
197 sub cust_bill_pkg {
198   my $self = shift;
199   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
200 }
201
202 =item cust_bill_event
203
204 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
205 invoice.
206
207 =cut
208
209 sub cust_bill_event {
210   my $self = shift;
211   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
212 }
213
214
215 =item cust_main
216
217 Returns the customer (see L<FS::cust_main>) for this invoice.
218
219 =cut
220
221 sub cust_main {
222   my $self = shift;
223   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
224 }
225
226 =item cust_credit
227
228 Depreciated.  See the cust_credited method.
229
230  #Returns a list consisting of the total previous credited (see
231  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
232  #outstanding credits (FS::cust_credit objects).
233
234 =cut
235
236 sub cust_credit {
237   use Carp;
238   croak "FS::cust_bill->cust_credit depreciated; see ".
239         "FS::cust_bill->cust_credit_bill";
240   #my $self = shift;
241   #my $total = 0;
242   #my @cust_credit = sort { $a->_date <=> $b->_date }
243   #  grep { $_->credited != 0 && $_->_date < $self->_date }
244   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
245   #;
246   #foreach (@cust_credit) { $total += $_->credited; }
247   #$total, @cust_credit;
248 }
249
250 =item cust_pay
251
252 Depreciated.  See the cust_bill_pay method.
253
254 #Returns all payments (see L<FS::cust_pay>) for this invoice.
255
256 =cut
257
258 sub cust_pay {
259   use Carp;
260   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
261   #my $self = shift;
262   #sort { $a->_date <=> $b->_date }
263   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
264   #;
265 }
266
267 =item cust_bill_pay
268
269 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
270
271 =cut
272
273 sub cust_bill_pay {
274   my $self = shift;
275   sort { $a->_date <=> $b->_date }
276     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
277 }
278
279 =item cust_credited
280
281 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
282
283 =cut
284
285 sub cust_credited {
286   my $self = shift;
287   sort { $a->_date <=> $b->_date }
288     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
289   ;
290 }
291
292 =item tax
293
294 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
295
296 =cut
297
298 sub tax {
299   my $self = shift;
300   my $total = 0;
301   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
302                                              'pkgnum' => 0 } );
303   foreach (@taxlines) { $total += $_->setup; }
304   $total;
305 }
306
307 =item owed
308
309 Returns the amount owed (still outstanding) on this invoice, which is charged
310 minus all payment applications (see L<FS::cust_bill_pay>) and credit
311 applications (see L<FS::cust_credit_bill>).
312
313 =cut
314
315 sub owed {
316   my $self = shift;
317   my $balance = $self->charged;
318   $balance -= $_->amount foreach ( $self->cust_bill_pay );
319   $balance -= $_->amount foreach ( $self->cust_credited );
320   $balance = sprintf( "%.2f", $balance);
321   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
322   $balance;
323 }
324
325
326 =item generate_email PARAMHASH
327
328 PARAMHASH can contain the following:
329
330 =over 4
331
332 =item from       => sender address, required
333
334 =item tempate    => alternate template name, optional
335
336 =item print_text => text attachment arrayref, optional
337
338 =item subject    => email subject, optional
339
340 =back
341
342 Returns an argument list to be passed to L<FS::Misc::send_email>.
343
344 =cut
345
346 use MIME::Entity;
347
348 sub generate_email {
349
350   my $self = shift;
351   my %args = @_;
352
353   my $me = '[FS::cust_bill::generate_email]';
354
355   my %return = (
356     'from'      => $args{'from'},
357     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
358   );
359
360   if (ref($args{'to'} eq 'ARRAY')) {
361     $return{'to'} = $args{'to'};
362   } else {
363     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
364                            $self->cust_main->invoicing_list
365                     ];
366   }
367
368   if ( $conf->exists('invoice_html') ) {
369
370     warn "$me creating HTML/text multipart message"
371       if $DEBUG;
372
373     $return{'nobody'} = 1;
374
375     my $alternative = build MIME::Entity
376       'Type'        => 'multipart/alternative',
377       'Encoding'    => '7bit',
378       'Disposition' => 'inline'
379     ;
380
381     my $data;
382     if ( $conf->exists('invoice_email_pdf')
383          and scalar($conf->config('invoice_email_pdf_note')) ) {
384
385       warn "$me using 'invoice_email_pdf_note' in multipart message"
386         if $DEBUG;
387       $data = [ map { $_ . "\n" }
388                     $conf->config('invoice_email_pdf_note')
389               ];
390
391     } else {
392
393       warn "$me not using 'invoice_email_pdf_note' in multipart message"
394         if $DEBUG;
395       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
396         $data = $args{'print_text'};
397       } else {
398         $data = [ $self->print_text('', $args{'template'}) ];
399       }
400
401     }
402
403     $alternative->attach(
404       'Type'        => 'text/plain',
405       #'Encoding'    => 'quoted-printable',
406       'Encoding'    => '7bit',
407       'Data'        => $data,
408       'Disposition' => 'inline',
409     );
410
411     $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
412     my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
413
414     my $image = build MIME::Entity
415       'Type'       => 'image/png',
416       'Encoding'   => 'base64',
417       'Path'       => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png",
418       'Filename'   => 'logo.png',
419       'Content-ID' => "<$content_id>",
420     ;
421
422     $alternative->attach(
423       'Type'        => 'text/html',
424       'Encoding'    => 'quoted-printable',
425       'Data'        => [ '<html>',
426                          '  <head>',
427                          '    <title>',
428                          '      '. encode_entities($return{'subject'}), 
429                          '    </title>',
430                          '  </head>',
431                          '  <body bgcolor="#e8e8e8">',
432                          $self->print_html('', $args{'template'}, $content_id),
433                          '  </body>',
434                          '</html>',
435                        ],
436       'Disposition' => 'inline',
437       #'Filename'    => 'invoice.pdf',
438     );
439
440     if ( $conf->exists('invoice_email_pdf') ) {
441
442       #attaching pdf too:
443       # multipart/mixed
444       #   multipart/related
445       #     multipart/alternative
446       #       text/plain
447       #       text/html
448       #     image/png
449       #   application/pdf
450
451       my $related = build MIME::Entity 'Type'     => 'multipart/related',
452                                        'Encoding' => '7bit';
453
454       #false laziness w/Misc::send_email
455       $related->head->replace('Content-type',
456         $related->mime_type.
457         '; boundary="'. $related->head->multipart_boundary. '"'.
458         '; type=multipart/alternative'
459       );
460
461       $related->add_part($alternative);
462
463       $related->add_part($image);
464
465       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
466
467       $return{'mimeparts'} = [ $related, $pdf ];
468
469     } else {
470
471       #no other attachment:
472       # multipart/related
473       #   multipart/alternative
474       #     text/plain
475       #     text/html
476       #   image/png
477
478       $return{'content-type'} = 'multipart/related';
479       $return{'mimeparts'} = [ $alternative, $image ];
480       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
481       #$return{'disposition'} = 'inline';
482
483     }
484   
485   } else {
486
487     if ( $conf->exists('invoice_email_pdf') ) {
488       warn "$me creating PDF attachment"
489         if $DEBUG;
490
491       #mime parts arguments a la MIME::Entity->build().
492       $return{'mimeparts'} = [
493         { $self->mimebuild_pdf('', $args{'template'}) }
494       ];
495     }
496   
497     if ( $conf->exists('invoice_email_pdf')
498          and scalar($conf->config('invoice_email_pdf_note')) ) {
499
500       warn "$me using 'invoice_email_pdf_note'"
501         if $DEBUG;
502       $return{'body'} = [ map { $_ . "\n" }
503                               $conf->config('invoice_email_pdf_note')
504                         ];
505
506     } else {
507
508       warn "$me not using 'invoice_email_pdf_note'"
509         if $DEBUG;
510       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
511         $return{'body'} = $args{'print_text'};
512       } else {
513         $return{'body'} = [ $self->print_text('', $args{'template'}) ];
514       }
515
516     }
517
518   }
519
520   %return;
521
522 }
523
524 =item mimebuild_pdf
525
526 Returns a list suitable for passing to MIME::Entity->build(), representing
527 this invoice as PDF attachment.
528
529 =cut
530
531 sub mimebuild_pdf {
532   my $self = shift;
533   (
534     'Type'        => 'application/pdf',
535     'Encoding'    => 'base64',
536     'Data'        => [ $self->print_pdf(@_) ],
537     'Disposition' => 'attachment',
538     'Filename'    => 'invoice.pdf',
539   );
540 }
541
542 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
543
544 Sends this invoice to the destinations configured for this customer: send
545 emails or print.  See L<FS::cust_main_invoice>.
546
547 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
548
549 AGENTNUM, if specified, means that this invoice will only be sent for customers
550 of the specified agent.
551
552 INVOICE_FROM, if specified, overrides the default email invoice From: address.
553
554 =cut
555
556 sub send {
557   my $self = shift;
558   my $template = scalar(@_) ? shift : '';
559   return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
560   my $invoice_from =
561     scalar(@_)
562       ? shift
563       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
564
565   #my @print_text = $self->print_text('', $template);
566   my @invoicing_list = $self->cust_main->invoicing_list;
567
568   if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list  ) {
569     #email
570
571     #better to notify this person than silence
572     @invoicing_list = ($invoice_from) unless @invoicing_list;
573
574     my $error = send_email(
575       $self->generate_email(
576         'from'       => $invoice_from,
577         'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
578         #'print_text' => [ @print_text ],
579         'template'   => $template,
580       )
581     );
582     die "can't email invoice: $error\n" if $error;
583     #die "$error\n" if $error;
584
585   }
586
587   if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
588     my $lpr_data;
589     if ($conf->config('invoice_latex')) {
590       $lpr_data = [ $self->print_ps('', $template) ];
591     } else {
592       $lpr_data = [ $self->print_text('', $template) ];
593     }
594
595     if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
596       my $lpr = $conf->config('lpr');
597       open(LPR, "|$lpr")
598         or die "Can't open pipe to $lpr: $!\n";
599       print LPR @{$lpr_data};
600       close LPR
601         or die $! ? "Error closing $lpr: $!\n"
602                   : "Exit status $? from $lpr\n";
603     }
604
605     if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
606       die 'FAX invoice destination not supported with plain text invoices.'
607         unless $conf->exists('invoice_latex');
608       my $dialstring = $self->cust_main->getfield('fax');
609       #Check $dialstring?
610       my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
611       die $error if $error;
612     }
613
614   }
615
616   '';
617
618 }
619
620 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
621
622 Like B<send>, but only sends the invoice if it is the newest open invoice for
623 this customer.
624
625 =cut
626
627 sub send_if_newest {
628   my $self = shift;
629
630   return ''
631     if scalar(
632                grep { $_->owed > 0 } 
633                     qsearch('cust_bill', {
634                       'custnum' => $self->custnum,
635                       #'_date'   => { op=>'>', value=>$self->_date },
636                       'invnum'  => { op=>'>', value=>$self->invnum },
637                     } )
638              );
639     
640   $self->send(@_);
641 }
642
643 =item send_csv OPTIONS
644
645 Sends invoice as a CSV data-file to a remote host with the specified protocol.
646
647 Options are:
648
649 protocol - currently only "ftp"
650 server
651 username
652 password
653 dir
654
655 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
656 and YYMMDDHHMMSS is a timestamp.
657
658 The fields of the CSV file is as follows:
659
660 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
661
662 =over 4
663
664 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
665
666 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
667 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
668 fields are filled in.
669
670 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
671 first two fields (B<record_type> and B<invnum>) and the last five fields
672 (B<pkg> through B<edate>) are filled in.
673
674 =item invnum - invoice number
675
676 =item custnum - customer number
677
678 =item _date - invoice date
679
680 =item charged - total invoice amount
681
682 =item first - customer first name
683
684 =item last - customer first name
685
686 =item company - company name
687
688 =item address1 - address line 1
689
690 =item address2 - address line 1
691
692 =item city
693
694 =item state
695
696 =item zip
697
698 =item country
699
700 =item pkg - line item description
701
702 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
703
704 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
705
706 =item sdate - start date for recurring fee
707
708 =item edate - end date for recurring fee
709
710 =back
711
712 =cut
713
714 sub send_csv {
715   my($self, %opt) = @_;
716
717   #part one: create file
718
719   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
720   mkdir $spooldir, 0700 unless -d $spooldir;
721
722   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
723
724   open(CSV, ">$file") or die "can't open $file: $!";
725
726   eval "use Text::CSV_XS";
727   die $@ if $@;
728
729   my $csv = Text::CSV_XS->new({'always_quote'=>1});
730
731   my $cust_main = $self->cust_main;
732
733   $csv->combine(
734     'cust_bill',
735     $self->invnum,
736     $self->custnum,
737     time2str("%x", $self->_date),
738     sprintf("%.2f", $self->charged),
739     ( map { $cust_main->getfield($_) }
740         qw( first last company address1 address2 city state zip country ) ),
741     map { '' } (1..5),
742   ) or die "can't create csv";
743   print CSV $csv->string. "\n";
744
745   #new charges (false laziness w/print_text)
746   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
747
748     my($pkg, $setup, $recur, $sdate, $edate);
749     if ( $cust_bill_pkg->pkgnum ) {
750     
751       ($pkg, $setup, $recur, $sdate, $edate) = (
752         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
753         ( $cust_bill_pkg->setup != 0
754           ? sprintf("%.2f", $cust_bill_pkg->setup )
755           : '' ),
756         ( $cust_bill_pkg->recur != 0
757           ? sprintf("%.2f", $cust_bill_pkg->recur )
758           : '' ),
759         time2str("%x", $cust_bill_pkg->sdate),
760         time2str("%x", $cust_bill_pkg->edate),
761       );
762
763     } else { #pkgnum tax
764       next unless $cust_bill_pkg->setup != 0;
765       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
766                        ? ( $cust_bill_pkg->itemdesc || 'Tax' )
767                        : 'Tax';
768       ($pkg, $setup, $recur, $sdate, $edate) =
769         ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
770     }
771
772     $csv->combine(
773       'cust_bill_pkg',
774       $self->invnum,
775       ( map { '' } (1..11) ),
776       ($pkg, $setup, $recur, $sdate, $edate)
777     ) or die "can't create csv";
778     print CSV $csv->string. "\n";
779
780   }
781
782   close CSV or die "can't close CSV: $!";
783
784   #part two: upload it
785
786   my $net;
787   if ( $opt{protocol} eq 'ftp' ) {
788     eval "use Net::FTP;";
789     die $@ if $@;
790     $net = Net::FTP->new($opt{server}) or die @$;
791   } else {
792     die "unknown protocol: $opt{protocol}";
793   }
794
795   $net->login( $opt{username}, $opt{password} )
796     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
797
798   $net->binary or die "can't set binary mode";
799
800   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
801
802   $net->put($file) or die "can't put $file: $!";
803
804   $net->quit;
805
806   unlink $file;
807
808 }
809
810 =item comp
811
812 Pays this invoice with a compliemntary payment.  If there is an error,
813 returns the error, otherwise returns false.
814
815 =cut
816
817 sub comp {
818   my $self = shift;
819   my $cust_pay = new FS::cust_pay ( {
820     'invnum'   => $self->invnum,
821     'paid'     => $self->owed,
822     '_date'    => '',
823     'payby'    => 'COMP',
824     'payinfo'  => $self->cust_main->payinfo,
825     'paybatch' => '',
826   } );
827   $cust_pay->insert;
828 }
829
830 =item realtime_card
831
832 Attempts to pay this invoice with a credit card payment via a
833 Business::OnlinePayment realtime gateway.  See
834 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
835 for supported processors.
836
837 =cut
838
839 sub realtime_card {
840   my $self = shift;
841   $self->realtime_bop( 'CC', @_ );
842 }
843
844 =item realtime_ach
845
846 Attempts to pay this invoice with an electronic check (ACH) payment via a
847 Business::OnlinePayment realtime gateway.  See
848 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
849 for supported processors.
850
851 =cut
852
853 sub realtime_ach {
854   my $self = shift;
855   $self->realtime_bop( 'ECHECK', @_ );
856 }
857
858 =item realtime_lec
859
860 Attempts to pay this invoice with phone bill (LEC) payment via a
861 Business::OnlinePayment realtime gateway.  See
862 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
863 for supported processors.
864
865 =cut
866
867 sub realtime_lec {
868   my $self = shift;
869   $self->realtime_bop( 'LEC', @_ );
870 }
871
872 sub realtime_bop {
873   my( $self, $method ) = @_;
874
875   my $cust_main = $self->cust_main;
876   my $balance = $cust_main->balance;
877   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
878   $amount = sprintf("%.2f", $amount);
879   return "not run (balance $balance)" unless $amount > 0;
880
881   my $description = 'Internet Services';
882   if ( $conf->exists('business-onlinepayment-description') ) {
883     my $dtempl = $conf->config('business-onlinepayment-description');
884
885     my $agent_obj = $cust_main->agent
886       or die "can't retreive agent for $cust_main (agentnum ".
887              $cust_main->agentnum. ")";
888     my $agent = $agent_obj->agent;
889     my $pkgs = join(', ',
890       map { $_->cust_pkg->part_pkg->pkg }
891         grep { $_->pkgnum } $self->cust_bill_pkg
892     );
893     $description = eval qq("$dtempl");
894   }
895
896   $cust_main->realtime_bop($method, $amount,
897     'description' => $description,
898     'invnum'      => $self->invnum,
899   );
900
901 }
902
903 =item batch_card
904
905 Adds a payment for this invoice to the pending credit card batch (see
906 L<FS::cust_pay_batch>).
907
908 =cut
909
910 sub batch_card {
911   my $self = shift;
912   my $cust_main = $self->cust_main;
913
914   my $cust_pay_batch = new FS::cust_pay_batch ( {
915     'invnum'   => $self->getfield('invnum'),
916     'custnum'  => $cust_main->getfield('custnum'),
917     'last'     => $cust_main->getfield('last'),
918     'first'    => $cust_main->getfield('first'),
919     'address1' => $cust_main->getfield('address1'),
920     'address2' => $cust_main->getfield('address2'),
921     'city'     => $cust_main->getfield('city'),
922     'state'    => $cust_main->getfield('state'),
923     'zip'      => $cust_main->getfield('zip'),
924     'country'  => $cust_main->getfield('country'),
925     'cardnum'  => $cust_main->payinfo,
926     'exp'      => $cust_main->getfield('paydate'),
927     'payname'  => $cust_main->getfield('payname'),
928     'amount'   => $self->owed,
929   } );
930   my $error = $cust_pay_batch->insert;
931   die $error if $error;
932
933   '';
934 }
935
936 sub _agent_template {
937   my $self = shift;
938   $self->_agent_plandata('agent_templatename');
939 }
940
941 sub _agent_invoice_from {
942   my $self = shift;
943   $self->_agent_plandata('agent_invoice_from');
944 }
945
946 sub _agent_plandata {
947   my( $self, $option ) = @_;
948
949   my $part_bill_event = qsearchs( 'part_bill_event',
950     {
951       'payby'     => $self->cust_main->payby,
952       'plan'      => 'send_agent',
953       'plandata'  => { 'op'    => '~',
954                        'value' => "(^|\n)agentnum ".
955                                   $self->cust_main->agentnum.
956                                   "(\n|\$)",
957                      },
958     },
959     '',
960     'ORDER BY seconds LIMIT 1'
961   );
962
963   return '' unless $part_bill_event;
964
965   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
966     return $1;
967   } else {
968     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
969          " plandata for $option";
970     return '';
971   }
972
973 }
974
975 =item print_text [ TIME [ , TEMPLATE ] ]
976
977 Returns an text invoice, as a list of lines.
978
979 TIME an optional value used to control the printing of overdue messages.  The
980 default is now.  It isn't the date of the invoice; that's the `_date' field.
981 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
982 L<Time::Local> and L<Date::Parse> for conversion functions.
983
984 =cut
985
986 #still some false laziness w/print_text
987 sub print_text {
988
989   my( $self, $today, $template ) = @_;
990   $today ||= time;
991
992 #  my $invnum = $self->invnum;
993   my $cust_main = $self->cust_main;
994   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
995     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
996
997   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
998 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
999   #my $balance_due = $self->owed + $pr_total - $cr_total;
1000   my $balance_due = $self->owed + $pr_total;
1001
1002   #my @collect = ();
1003   #my($description,$amount);
1004   @buf = ();
1005
1006   #previous balance
1007   foreach ( @pr_cust_bill ) {
1008     push @buf, [
1009       "Previous Balance, Invoice #". $_->invnum. 
1010                  " (". time2str("%x",$_->_date). ")",
1011       $money_char. sprintf("%10.2f",$_->owed)
1012     ];
1013   }
1014   if (@pr_cust_bill) {
1015     push @buf,['','-----------'];
1016     push @buf,[ 'Total Previous Balance',
1017                 $money_char. sprintf("%10.2f",$pr_total ) ];
1018     push @buf,['',''];
1019   }
1020
1021   #new charges
1022   foreach my $cust_bill_pkg (
1023     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1024     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1025   ) {
1026
1027     if ( $cust_bill_pkg->pkgnum ) {
1028
1029       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1030       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1031       my $pkg = $part_pkg->pkg;
1032
1033       if ( $cust_bill_pkg->setup != 0 ) {
1034         my $description = $pkg;
1035         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1036         push @buf, [ $description,
1037                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1038         push @buf,
1039           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1040               $cust_pkg->h_labels($self->_date);
1041       }
1042
1043       if ( $cust_bill_pkg->recur != 0 ) {
1044         push @buf, [
1045           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1046                                 time2str("%x", $cust_bill_pkg->edate) . ")",
1047           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1048         ];
1049         push @buf,
1050           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1051               $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1052       }
1053
1054       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1055
1056     } else { #pkgnum tax or one-shot line item
1057       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1058                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1059                      : 'Tax';
1060       if ( $cust_bill_pkg->setup != 0 ) {
1061         push @buf, [ $itemdesc,
1062                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1063       }
1064       if ( $cust_bill_pkg->recur != 0 ) {
1065         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1066                                   . time2str("%x", $cust_bill_pkg->edate). ")",
1067                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1068                    ];
1069       }
1070     }
1071   }
1072
1073   push @buf,['','-----------'];
1074   push @buf,['Total New Charges',
1075              $money_char. sprintf("%10.2f",$self->charged) ];
1076   push @buf,['',''];
1077
1078   push @buf,['','-----------'];
1079   push @buf,['Total Charges',
1080              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1081   push @buf,['',''];
1082
1083   #credits
1084   foreach ( $self->cust_credited ) {
1085
1086     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1087
1088     my $reason = substr($_->cust_credit->reason,0,32);
1089     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1090     $reason = " ($reason) " if $reason;
1091     push @buf,[
1092       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1093         $reason,
1094       $money_char. sprintf("%10.2f",$_->amount)
1095     ];
1096   }
1097   #foreach ( @cr_cust_credit ) {
1098   #  push @buf,[
1099   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1100   #    $money_char. sprintf("%10.2f",$_->credited)
1101   #  ];
1102   #}
1103
1104   #get & print payments
1105   foreach ( $self->cust_bill_pay ) {
1106
1107     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1108
1109     push @buf,[
1110       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1111       $money_char. sprintf("%10.2f",$_->amount )
1112     ];
1113   }
1114
1115   #balance due
1116   my $balance_due_msg = $self->balance_due_msg;
1117
1118   push @buf,['','-----------'];
1119   push @buf,[$balance_due_msg, $money_char. 
1120     sprintf("%10.2f", $balance_due ) ];
1121
1122   #create the template
1123   $template ||= $self->_agent_template;
1124   my $templatefile = 'invoice_template';
1125   $templatefile .= "_$template" if length($template);
1126   my @invoice_template = $conf->config($templatefile)
1127     or die "cannot load config file $templatefile";
1128   $invoice_lines = 0;
1129   my $wasfunc = 0;
1130   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1131     /invoice_lines\((\d*)\)/;
1132     $invoice_lines += $1 || scalar(@buf);
1133     $wasfunc=1;
1134   }
1135   die "no invoice_lines() functions in template?" unless $wasfunc;
1136   my $invoice_template = new Text::Template (
1137     TYPE   => 'ARRAY',
1138     SOURCE => [ map "$_\n", @invoice_template ],
1139   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1140   $invoice_template->compile()
1141     or die "can't compile template: $Text::Template::ERROR";
1142
1143   #setup template variables
1144   package FS::cust_bill::_template; #!
1145   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1146
1147   $invnum = $self->invnum;
1148   $date = $self->_date;
1149   $page = 1;
1150   $agent = $self->cust_main->agent->agent;
1151
1152   if ( $FS::cust_bill::invoice_lines ) {
1153     $total_pages =
1154       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1155     $total_pages++
1156       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1157   } else {
1158     $total_pages = 1;
1159   }
1160
1161   #format address (variable for the template)
1162   my $l = 0;
1163   @address = ( '', '', '', '', '', '' );
1164   package FS::cust_bill; #!
1165   $FS::cust_bill::_template::address[$l++] =
1166     $cust_main->payname.
1167       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1168         ? " (P.O. #". $cust_main->payinfo. ")"
1169         : ''
1170       )
1171   ;
1172   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1173     if $cust_main->company;
1174   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1175   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1176     if $cust_main->address2;
1177   $FS::cust_bill::_template::address[$l++] =
1178     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1179
1180   my $countrydefault = $conf->config('countrydefault') || 'US';
1181   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1182     unless $cust_main->country eq $countrydefault;
1183
1184         #  #overdue? (variable for the template)
1185         #  $FS::cust_bill::_template::overdue = ( 
1186         #    $balance_due > 0
1187         #    && $today > $self->_date 
1188         ##    && $self->printed > 1
1189         #    && $self->printed > 0
1190         #  );
1191
1192   #and subroutine for the template
1193   sub FS::cust_bill::_template::invoice_lines {
1194     my $lines = shift || scalar(@buf);
1195     map { 
1196       scalar(@buf) ? shift @buf : [ '', '' ];
1197     }
1198     ( 1 .. $lines );
1199   }
1200
1201   #and fill it in
1202   $FS::cust_bill::_template::page = 1;
1203   my $lines;
1204   my @collect;
1205   while (@buf) {
1206     push @collect, split("\n",
1207       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1208     );
1209     $FS::cust_bill::_template::page++;
1210   }
1211
1212   map "$_\n", @collect;
1213
1214 }
1215
1216 =item print_latex [ TIME [ , TEMPLATE ] ]
1217
1218 Internal method - returns a filename of a filled-in LaTeX template for this
1219 invoice (Note: add ".tex" to get the actual filename).
1220
1221 See print_ps and print_pdf for methods that return PostScript and PDF output.
1222
1223 TIME an optional value used to control the printing of overdue messages.  The
1224 default is now.  It isn't the date of the invoice; that's the `_date' field.
1225 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1226 L<Time::Local> and L<Date::Parse> for conversion functions.
1227
1228 =cut
1229
1230 #still some false laziness w/print_text
1231 sub print_latex {
1232
1233   my( $self, $today, $template ) = @_;
1234   $today ||= time;
1235   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1236     if $DEBUG;
1237
1238   my $cust_main = $self->cust_main;
1239   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1240     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1241
1242   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1243 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1244   #my $balance_due = $self->owed + $pr_total - $cr_total;
1245   my $balance_due = $self->owed + $pr_total;
1246
1247   #create the template
1248   $template ||= $self->_agent_template;
1249   my $templatefile = 'invoice_latex';
1250   my $suffix = length($template) ? "_$template" : '';
1251   $templatefile .= $suffix;
1252   my @invoice_template = map "$_\n", $conf->config($templatefile)
1253     or die "cannot load config file $templatefile";
1254
1255   my($format, $text_template);
1256   if ( grep { /^%%Detail/ } @invoice_template ) {
1257     #change this to a die when the old code is removed
1258     warn "old-style invoice template $templatefile; ".
1259          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1260     $format = 'old';
1261   } else {
1262     $format = 'Text::Template';
1263     $text_template = new Text::Template(
1264       TYPE => 'ARRAY',
1265       SOURCE => \@invoice_template,
1266       DELIMITERS => [ '[@--', '--@]' ],
1267     );
1268
1269     $text_template->compile()
1270       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1271   }
1272
1273   my $returnaddress;
1274   if ( $conf->exists('invoice_latexreturnaddress')
1275        && length($conf->exists('invoice_latexreturnaddress'))
1276      )
1277   {
1278     $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1279   } else {
1280     $returnaddress = '~';
1281   }
1282
1283   my %invoice_data = (
1284     'invnum'       => $self->invnum,
1285     'date'         => time2str('%b %o, %Y', $self->_date),
1286     'today'        => time2str('%b %o, %Y', $today),
1287     'agent'        => _latex_escape($cust_main->agent->agent),
1288     'payname'      => _latex_escape($cust_main->payname),
1289     'company'      => _latex_escape($cust_main->company),
1290     'address1'     => _latex_escape($cust_main->address1),
1291     'address2'     => _latex_escape($cust_main->address2),
1292     'city'         => _latex_escape($cust_main->city),
1293     'state'        => _latex_escape($cust_main->state),
1294     'zip'          => _latex_escape($cust_main->zip),
1295     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1296     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1297     'returnaddress' => $returnaddress,
1298     'quantity'     => 1,
1299     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1300     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1301     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1302   );
1303
1304   my $countrydefault = $conf->config('countrydefault') || 'US';
1305   if ( $cust_main->country eq $countrydefault ) {
1306     $invoice_data{'country'} = '';
1307   } else {
1308     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1309   }
1310
1311 #  #do variable substitutions in notes
1312 #  $invoice_data{'notes'} =
1313 #    join("\n",
1314 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1315 #        $conf->config_orbase('invoice_latexnotes', $template)
1316 #    );
1317 #  warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1318 #    if $DEBUG;
1319
1320   $invoice_data{'footer'} =~ s/\n+$//;
1321   $invoice_data{'smallfooter'} =~ s/\n+$//;
1322   $invoice_data{'notes'} =~ s/\n+$//;
1323
1324   $invoice_data{'po_line'} =
1325     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1326       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1327       : '~';
1328
1329   my @filled_in = ();
1330   if ( $format eq 'old' ) {
1331   
1332     my @line_item = ();
1333     my @total_item = ();
1334     while ( @invoice_template ) {
1335       my $line = shift @invoice_template;
1336   
1337       if ( $line =~ /^%%Detail\s*$/ ) {
1338   
1339         while ( ( my $line_item_line = shift @invoice_template )
1340                 !~ /^%%EndDetail\s*$/                            ) {
1341           push @line_item, $line_item_line;
1342         }
1343         foreach my $line_item ( $self->_items ) {
1344         #foreach my $line_item ( $self->_items_pkg ) {
1345           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1346           $invoice_data{'description'} =
1347             _latex_escape($line_item->{'description'});
1348           if ( exists $line_item->{'ext_description'} ) {
1349             $invoice_data{'description'} .=
1350               "\\tabularnewline\n~~".
1351               join( "\\tabularnewline\n~~",
1352                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1353                   );
1354           }
1355           $invoice_data{'amount'} = $line_item->{'amount'};
1356           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1357           push @filled_in,
1358             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1359         }
1360   
1361       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1362   
1363         while ( ( my $total_item_line = shift @invoice_template )
1364                 !~ /^%%EndTotalDetails\s*$/                      ) {
1365           push @total_item, $total_item_line;
1366         }
1367   
1368         my @total_fill = ();
1369   
1370         my $taxtotal = 0;
1371         foreach my $tax ( $self->_items_tax ) {
1372           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1373           $taxtotal += $tax->{'amount'};
1374           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1375           push @total_fill,
1376             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1377                 @total_item;
1378         }
1379
1380         if ( $taxtotal ) {
1381           $invoice_data{'total_item'} = 'Sub-total';
1382           $invoice_data{'total_amount'} =
1383             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1384           unshift @total_fill,
1385             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1386                 @total_item;
1387         }
1388   
1389         $invoice_data{'total_item'} = '\textbf{Total}';
1390         $invoice_data{'total_amount'} =
1391           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1392         push @total_fill,
1393           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1394               @total_item;
1395   
1396         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1397   
1398         # credits
1399         foreach my $credit ( $self->_items_credits ) {
1400           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1401           #$credittotal
1402           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1403           push @total_fill, 
1404             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1405                 @total_item;
1406         }
1407   
1408         # payments
1409         foreach my $payment ( $self->_items_payments ) {
1410           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1411           #$paymenttotal
1412           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1413           push @total_fill, 
1414             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1415                 @total_item;
1416         }
1417   
1418         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1419         $invoice_data{'total_amount'} =
1420           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1421         push @total_fill,
1422           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1423               @total_item;
1424   
1425         push @filled_in, @total_fill;
1426   
1427       } else {
1428         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1429         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1430         push @filled_in, $line;
1431       }
1432   
1433     }
1434
1435     sub nounder {
1436       my $var = $1;
1437       $var =~ s/_/\-/g;
1438       $var;
1439     }
1440
1441   } elsif ( $format eq 'Text::Template' ) {
1442
1443     my @detail_items = ();
1444     my @total_items = ();
1445
1446     $invoice_data{'detail_items'} = \@detail_items;
1447     $invoice_data{'total_items'} = \@total_items;
1448   
1449     foreach my $line_item ( $self->_items ) {
1450       my $detail = {
1451         ext_description => [],
1452       };
1453       $detail->{'ref'} = $line_item->{'pkgnum'};
1454       $detail->{'quantity'} = 1;
1455       $detail->{'description'} = _latex_escape($line_item->{'description'});
1456       if ( exists $line_item->{'ext_description'} ) {
1457         @{$detail->{'ext_description'}} = map {
1458           _latex_escape($_);
1459         } @{$line_item->{'ext_description'}};
1460       }
1461       $detail->{'amount'} = $line_item->{'amount'};
1462       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1463   
1464       push @detail_items, $detail;
1465     }
1466   
1467   
1468     my $taxtotal = 0;
1469     foreach my $tax ( $self->_items_tax ) {
1470       my $total = {};
1471       $total->{'total_item'} = _latex_escape($tax->{'description'});
1472       $taxtotal += $tax->{'amount'};
1473       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1474       push @total_items, $total;
1475     }
1476   
1477     if ( $taxtotal ) {
1478       my $total = {};
1479       $total->{'total_item'} = 'Sub-total';
1480       $total->{'total_amount'} =
1481         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1482       unshift @total_items, $total;
1483     }
1484   
1485     {
1486       my $total = {};
1487       $total->{'total_item'} = '\textbf{Total}';
1488       $total->{'total_amount'} =
1489         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1490       push @total_items, $total;
1491     }
1492   
1493     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1494   
1495     # credits
1496     foreach my $credit ( $self->_items_credits ) {
1497       my $total;
1498       $total->{'total_item'} = _latex_escape($credit->{'description'});
1499       #$credittotal
1500       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1501       push @total_items, $total;
1502     }
1503   
1504     # payments
1505     foreach my $payment ( $self->_items_payments ) {
1506       my $total = {};
1507       $total->{'total_item'} = _latex_escape($payment->{'description'});
1508       #$paymenttotal
1509       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1510       push @total_items, $total;
1511     }
1512   
1513     { 
1514       my $total;
1515       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1516       $total->{'total_amount'} =
1517         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1518       push @total_items, $total;
1519     }
1520
1521   } else {
1522     die "guru meditation #54";
1523   }
1524
1525   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1526   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1527                            DIR      => $dir,
1528                            SUFFIX   => '.tex',
1529                            UNLINK   => 0,
1530                          ) or die "can't open temp file: $!\n";
1531   if ( $format eq 'old' ) {
1532     print $fh join('', @filled_in );
1533   } elsif ( $format eq 'Text::Template' ) {
1534     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1535   } else {
1536     die "guru meditation #32";
1537   }
1538   close $fh;
1539
1540   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1541   return $1;
1542
1543 }
1544
1545 =item print_ps [ TIME [ , TEMPLATE ] ]
1546
1547 Returns an postscript invoice, as a scalar.
1548
1549 TIME an optional value used to control the printing of overdue messages.  The
1550 default is now.  It isn't the date of the invoice; that's the `_date' field.
1551 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1552 L<Time::Local> and L<Date::Parse> for conversion functions.
1553
1554 =cut
1555
1556 sub print_ps {
1557   my $self = shift;
1558
1559   my $file = $self->print_latex(@_);
1560
1561   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1562   chdir($dir);
1563
1564   my $sfile = shell_quote $file;
1565
1566   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1567     or die "pslatex $file.tex failed; see $file.log for details?\n";
1568   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1569     or die "pslatex $file.tex failed; see $file.log for details?\n";
1570
1571   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1572     or die "dvips failed";
1573
1574   open(POSTSCRIPT, "<$file.ps")
1575     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1576
1577   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1578
1579   my $ps = '';
1580   while (<POSTSCRIPT>) {
1581     $ps .= $_;
1582   }
1583
1584   close POSTSCRIPT;
1585
1586   return $ps;
1587
1588 }
1589
1590 =item print_pdf [ TIME [ , TEMPLATE ] ]
1591
1592 Returns an PDF invoice, as a scalar.
1593
1594 TIME an optional value used to control the printing of overdue messages.  The
1595 default is now.  It isn't the date of the invoice; that's the `_date' field.
1596 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1597 L<Time::Local> and L<Date::Parse> for conversion functions.
1598
1599 =cut
1600
1601 sub print_pdf {
1602   my $self = shift;
1603
1604   my $file = $self->print_latex(@_);
1605
1606   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1607   chdir($dir);
1608
1609   #system('pdflatex', "$file.tex");
1610   #system('pdflatex', "$file.tex");
1611   #! LaTeX Error: Unknown graphics extension: .eps.
1612
1613   my $sfile = shell_quote $file;
1614
1615   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1616     or die "pslatex $file.tex failed; see $file.log for details?\n";
1617   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1618     or die "pslatex $file.tex failed; see $file.log for details?\n";
1619
1620   #system('dvipdf', "$file.dvi", "$file.pdf" );
1621   system(
1622     "dvips -q -t letter -f $sfile.dvi ".
1623     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1624     "     -c save pop -"
1625   ) == 0
1626     or die "dvips | gs failed: $!";
1627
1628   open(PDF, "<$file.pdf")
1629     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1630
1631   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1632
1633   my $pdf = '';
1634   while (<PDF>) {
1635     $pdf .= $_;
1636   }
1637
1638   close PDF;
1639
1640   return $pdf;
1641
1642 }
1643
1644 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1645
1646 Returns an HTML invoice, as a scalar.
1647
1648 TIME an optional value used to control the printing of overdue messages.  The
1649 default is now.  It isn't the date of the invoice; that's the `_date' field.
1650 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1651 L<Time::Local> and L<Date::Parse> for conversion functions.
1652
1653 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1654 when emailing the invoice as part of a multipart/related MIME email.
1655
1656 =cut
1657
1658 sub print_html {
1659   my( $self, $today, $template, $cid ) = @_;
1660   $today ||= time;
1661
1662   my $cust_main = $self->cust_main;
1663   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1664     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1665
1666   $template ||= $self->_agent_template;
1667   my $templatefile = 'invoice_html';
1668   my $suffix = length($template) ? "_$template" : '';
1669   $templatefile .= $suffix;
1670   my @html_template = map "$_\n", $conf->config($templatefile)
1671     or die "cannot load config file $templatefile";
1672
1673   my $html_template = new Text::Template(
1674     TYPE   => 'ARRAY',
1675     SOURCE => \@html_template,
1676     DELIMITERS => [ '<%=', '%>' ],
1677   );
1678
1679   $html_template->compile()
1680     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1681
1682   my %invoice_data = (
1683     'invnum'       => $self->invnum,
1684     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
1685     'today'        => time2str('%b %o, %Y', $today),
1686     'agent'        => encode_entities($cust_main->agent->agent),
1687     'payname'      => encode_entities($cust_main->payname),
1688     'company'      => encode_entities($cust_main->company),
1689     'address1'     => encode_entities($cust_main->address1),
1690     'address2'     => encode_entities($cust_main->address2),
1691     'city'         => encode_entities($cust_main->city),
1692     'state'        => encode_entities($cust_main->state),
1693     'zip'          => encode_entities($cust_main->zip),
1694     'terms'        => $conf->config('invoice_default_terms')
1695                       || 'Payable upon receipt',
1696     'cid'          => $cid,
1697 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1698   );
1699
1700   $invoice_data{'returnaddress'} = $conf->exists('invoice_htmlreturnaddress')
1701     ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1702     : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
1703                      $conf->config('invoice_latexreturnaddress')
1704           );
1705
1706   my $countrydefault = $conf->config('countrydefault') || 'US';
1707   if ( $cust_main->country eq $countrydefault ) {
1708     $invoice_data{'country'} = '';
1709   } else {
1710     $invoice_data{'country'} =
1711       encode_entities(code2country($cust_main->country));
1712   }
1713
1714   $invoice_data{'notes'} =
1715     length($conf->config_orbase('invoice_htmlnotes', $template))
1716       ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1717       : join("\n", map { 
1718                          s/%%(.*)$/<!-- $1 -->/;
1719                          s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1720                          s/\\begin\{enumerate\}/<ol>/;
1721                          s/\\item /  <li>/;
1722                          s/\\end\{enumerate\}/<\/ol>/;
1723                          s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1724                          $_;
1725                        } 
1726                        $conf->config_orbase('invoice_latexnotes', $template)
1727             );
1728
1729 #  #do variable substitutions in notes
1730 #  $invoice_data{'notes'} =
1731 #    join("\n",
1732 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1733 #        $conf->config_orbase('invoice_latexnotes', $suffix)
1734 #    );
1735
1736    $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1737      ? join("\n", $conf->config('invoice_htmlfooter') )
1738      : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
1739                       $conf->config('invoice_latexfooter')
1740            );
1741
1742   $invoice_data{'po_line'} =
1743     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1744       ? encode_entities("Purchase Order #". $cust_main->payinfo)
1745       : '';
1746
1747   my $money_char = $conf->config('money_char') || '$';
1748
1749   foreach my $line_item ( $self->_items ) {
1750     my $detail = {
1751       ext_description => [],
1752     };
1753     $detail->{'ref'} = $line_item->{'pkgnum'};
1754     $detail->{'description'} = encode_entities($line_item->{'description'});
1755     if ( exists $line_item->{'ext_description'} ) {
1756       @{$detail->{'ext_description'}} = map {
1757         encode_entities($_);
1758       } @{$line_item->{'ext_description'}};
1759     }
1760     $detail->{'amount'} = $money_char. $line_item->{'amount'};
1761     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1762
1763     push @{$invoice_data{'detail_items'}}, $detail;
1764   }
1765
1766
1767   my $taxtotal = 0;
1768   foreach my $tax ( $self->_items_tax ) {
1769     my $total = {};
1770     $total->{'total_item'} = encode_entities($tax->{'description'});
1771     $taxtotal += $tax->{'amount'};
1772     $total->{'total_amount'} = $money_char. $tax->{'amount'};
1773     push @{$invoice_data{'total_items'}}, $total;
1774   }
1775
1776   if ( $taxtotal ) {
1777     my $total = {};
1778     $total->{'total_item'} = 'Sub-total';
1779     $total->{'total_amount'} =
1780       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1781     unshift @{$invoice_data{'total_items'}}, $total;
1782   }
1783
1784   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1785   {
1786     my $total = {};
1787     $total->{'total_item'} = '<b>Total</b>';
1788     $total->{'total_amount'} =
1789       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1790     push @{$invoice_data{'total_items'}}, $total;
1791   }
1792
1793   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1794
1795   # credits
1796   foreach my $credit ( $self->_items_credits ) {
1797     my $total;
1798     $total->{'total_item'} = encode_entities($credit->{'description'});
1799     #$credittotal
1800     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1801     push @{$invoice_data{'total_items'}}, $total;
1802   }
1803
1804   # payments
1805   foreach my $payment ( $self->_items_payments ) {
1806     my $total = {};
1807     $total->{'total_item'} = encode_entities($payment->{'description'});
1808     #$paymenttotal
1809     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1810     push @{$invoice_data{'total_items'}}, $total;
1811   }
1812
1813   { 
1814     my $total;
1815     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1816     $total->{'total_amount'} =
1817       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1818     push @{$invoice_data{'total_items'}}, $total;
1819   }
1820
1821   $html_template->fill_in( HASH => \%invoice_data);
1822 }
1823
1824 # quick subroutine for print_latex
1825 #
1826 # There are ten characters that LaTeX treats as special characters, which
1827 # means that they do not simply typeset themselves: 
1828 #      # $ % & ~ _ ^ \ { }
1829 #
1830 # TeX ignores blanks following an escaped character; if you want a blank (as
1831 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1832
1833 sub _latex_escape {
1834   my $value = shift;
1835   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1836   $value =~ s/([<>])/\$$1\$/g;
1837   $value;
1838 }
1839
1840 #utility methods for print_*
1841
1842 sub balance_due_msg {
1843   my $self = shift;
1844   my $msg = 'Balance Due';
1845   return $msg unless $conf->exists('invoice_default_terms');
1846   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1847     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1848   } elsif ( $conf->config('invoice_default_terms') ) {
1849     $msg .= ' - '. $conf->config('invoice_default_terms');
1850   }
1851   $msg;
1852 }
1853
1854 sub _items {
1855   my $self = shift;
1856   my @display = scalar(@_)
1857                 ? @_
1858                 : qw( _items_previous _items_pkg );
1859                 #: qw( _items_pkg );
1860                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1861   my @b = ();
1862   foreach my $display ( @display ) {
1863     push @b, $self->$display(@_);
1864   }
1865   @b;
1866 }
1867
1868 sub _items_previous {
1869   my $self = shift;
1870   my $cust_main = $self->cust_main;
1871   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1872   my @b = ();
1873   foreach ( @pr_cust_bill ) {
1874     push @b, {
1875       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1876                        ' ('. time2str('%x',$_->_date). ')',
1877       #'pkgpart'     => 'N/A',
1878       'pkgnum'      => 'N/A',
1879       'amount'      => sprintf("%.2f", $_->owed),
1880     };
1881   }
1882   @b;
1883
1884   #{
1885   #    'description'     => 'Previous Balance',
1886   #    #'pkgpart'         => 'N/A',
1887   #    'pkgnum'          => 'N/A',
1888   #    'amount'          => sprintf("%10.2f", $pr_total ),
1889   #    'ext_description' => [ map {
1890   #                                 "Invoice ". $_->invnum.
1891   #                                 " (". time2str("%x",$_->_date). ") ".
1892   #                                 sprintf("%10.2f", $_->owed)
1893   #                         } @pr_cust_bill ],
1894
1895   #};
1896 }
1897
1898 sub _items_pkg {
1899   my $self = shift;
1900   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1901   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1902 }
1903
1904 sub _items_tax {
1905   my $self = shift;
1906   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1907   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1908 }
1909
1910 sub _items_cust_bill_pkg {
1911   my $self = shift;
1912   my $cust_bill_pkg = shift;
1913
1914   my @b = ();
1915   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1916
1917     if ( $cust_bill_pkg->pkgnum ) {
1918
1919       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1920       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1921       my $pkg = $part_pkg->pkg;
1922
1923       if ( $cust_bill_pkg->setup != 0 ) {
1924         my $description = $pkg;
1925         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1926         my @d = $cust_pkg->h_labels_short($self->_date);
1927         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1928         push @b, {
1929           description     => $description,
1930           #pkgpart         => $part_pkg->pkgpart,
1931           pkgnum          => $cust_pkg->pkgnum,
1932           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
1933           ext_description => \@d,
1934         };
1935       }
1936
1937       if ( $cust_bill_pkg->recur != 0 ) {
1938         push @b, {
1939           description     => "$pkg (" .
1940                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1941                                time2str('%x', $cust_bill_pkg->edate). ')',
1942           #pkgpart         => $part_pkg->pkgpart,
1943           pkgnum          => $cust_pkg->pkgnum,
1944           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
1945           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1946                                                          $cust_bill_pkg->sdate),
1947                                $cust_bill_pkg->details,
1948                              ],
1949         };
1950       }
1951
1952     } else { #pkgnum tax or one-shot line item (??)
1953
1954       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1955                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1956                      : 'Tax';
1957       if ( $cust_bill_pkg->setup != 0 ) {
1958         push @b, {
1959           'description' => $itemdesc,
1960           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
1961         };
1962       }
1963       if ( $cust_bill_pkg->recur != 0 ) {
1964         push @b, {
1965           'description' => "$itemdesc (".
1966                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1967                            time2str("%x", $cust_bill_pkg->edate). ')',
1968           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
1969         };
1970       }
1971
1972     }
1973
1974   }
1975
1976   @b;
1977
1978 }
1979
1980 sub _items_credits {
1981   my $self = shift;
1982
1983   my @b;
1984   #credits
1985   foreach ( $self->cust_credited ) {
1986
1987     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1988
1989     my $reason = $_->cust_credit->reason;
1990     #my $reason = substr($_->cust_credit->reason,0,32);
1991     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1992     $reason = " ($reason) " if $reason;
1993     push @b, {
1994       #'description' => 'Credit ref\#'. $_->crednum.
1995       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1996       #                 $reason,
1997       'description' => 'Credit applied '.
1998                        time2str("%x",$_->cust_credit->_date). $reason,
1999       'amount'      => sprintf("%.2f",$_->amount),
2000     };
2001   }
2002   #foreach ( @cr_cust_credit ) {
2003   #  push @buf,[
2004   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2005   #    $money_char. sprintf("%10.2f",$_->credited)
2006   #  ];
2007   #}
2008
2009   @b;
2010
2011 }
2012
2013 sub _items_payments {
2014   my $self = shift;
2015
2016   my @b;
2017   #get & print payments
2018   foreach ( $self->cust_bill_pay ) {
2019
2020     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2021
2022     push @b, {
2023       'description' => "Payment received ".
2024                        time2str("%x",$_->cust_pay->_date ),
2025       'amount'      => sprintf("%.2f", $_->amount )
2026     };
2027   }
2028
2029   @b;
2030
2031 }
2032
2033 =back
2034
2035 =head1 BUGS
2036
2037 The delete method.
2038
2039 print_text formatting (and some logic :/) is in source, but needs to be
2040 slurped in from a file.  Also number of lines ($=).
2041
2042 =head1 SEE ALSO
2043
2044 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2045 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2046 documentation.
2047
2048 =cut
2049
2050 1;
2051