so Search.tsf and Search.rdf work
[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 = 0;
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   $invoice_data{'notes'} =
1312     join("\n",
1313 #  #do variable substitutions in notes
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 { 
1703                        s/~/&nbsp;/g;
1704                        s/\\\\\*?\s*$/<BR>/;
1705                        s/\\hyphenation\{[\w\s\-]+\}//;
1706                        $_;
1707                      }
1708                      $conf->config('invoice_latexreturnaddress')
1709           );
1710
1711   my $countrydefault = $conf->config('countrydefault') || 'US';
1712   if ( $cust_main->country eq $countrydefault ) {
1713     $invoice_data{'country'} = '';
1714   } else {
1715     $invoice_data{'country'} =
1716       encode_entities(code2country($cust_main->country));
1717   }
1718
1719   $invoice_data{'notes'} =
1720     length($conf->config_orbase('invoice_htmlnotes', $template))
1721       ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1722       : join("\n", map { 
1723                          s/%%(.*)$/<!-- $1 -->/;
1724                          s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1725                          s/\\begin\{enumerate\}/<ol>/;
1726                          s/\\item /  <li>/;
1727                          s/\\end\{enumerate\}/<\/ol>/;
1728                          s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1729                          $_;
1730                        } 
1731                        $conf->config_orbase('invoice_latexnotes', $template)
1732             );
1733
1734 #  #do variable substitutions in notes
1735 #  $invoice_data{'notes'} =
1736 #    join("\n",
1737 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1738 #        $conf->config_orbase('invoice_latexnotes', $suffix)
1739 #    );
1740
1741    $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1742      ? join("\n", $conf->config('invoice_htmlfooter') )
1743      : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
1744                       $conf->config('invoice_latexfooter')
1745            );
1746
1747   $invoice_data{'po_line'} =
1748     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1749       ? encode_entities("Purchase Order #". $cust_main->payinfo)
1750       : '';
1751
1752   my $money_char = $conf->config('money_char') || '$';
1753
1754   foreach my $line_item ( $self->_items ) {
1755     my $detail = {
1756       ext_description => [],
1757     };
1758     $detail->{'ref'} = $line_item->{'pkgnum'};
1759     $detail->{'description'} = encode_entities($line_item->{'description'});
1760     if ( exists $line_item->{'ext_description'} ) {
1761       @{$detail->{'ext_description'}} = map {
1762         encode_entities($_);
1763       } @{$line_item->{'ext_description'}};
1764     }
1765     $detail->{'amount'} = $money_char. $line_item->{'amount'};
1766     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1767
1768     push @{$invoice_data{'detail_items'}}, $detail;
1769   }
1770
1771
1772   my $taxtotal = 0;
1773   foreach my $tax ( $self->_items_tax ) {
1774     my $total = {};
1775     $total->{'total_item'} = encode_entities($tax->{'description'});
1776     $taxtotal += $tax->{'amount'};
1777     $total->{'total_amount'} = $money_char. $tax->{'amount'};
1778     push @{$invoice_data{'total_items'}}, $total;
1779   }
1780
1781   if ( $taxtotal ) {
1782     my $total = {};
1783     $total->{'total_item'} = 'Sub-total';
1784     $total->{'total_amount'} =
1785       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1786     unshift @{$invoice_data{'total_items'}}, $total;
1787   }
1788
1789   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1790   {
1791     my $total = {};
1792     $total->{'total_item'} = '<b>Total</b>';
1793     $total->{'total_amount'} =
1794       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1795     push @{$invoice_data{'total_items'}}, $total;
1796   }
1797
1798   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1799
1800   # credits
1801   foreach my $credit ( $self->_items_credits ) {
1802     my $total;
1803     $total->{'total_item'} = encode_entities($credit->{'description'});
1804     #$credittotal
1805     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1806     push @{$invoice_data{'total_items'}}, $total;
1807   }
1808
1809   # payments
1810   foreach my $payment ( $self->_items_payments ) {
1811     my $total = {};
1812     $total->{'total_item'} = encode_entities($payment->{'description'});
1813     #$paymenttotal
1814     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1815     push @{$invoice_data{'total_items'}}, $total;
1816   }
1817
1818   { 
1819     my $total;
1820     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1821     $total->{'total_amount'} =
1822       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1823     push @{$invoice_data{'total_items'}}, $total;
1824   }
1825
1826   $html_template->fill_in( HASH => \%invoice_data);
1827 }
1828
1829 # quick subroutine for print_latex
1830 #
1831 # There are ten characters that LaTeX treats as special characters, which
1832 # means that they do not simply typeset themselves: 
1833 #      # $ % & ~ _ ^ \ { }
1834 #
1835 # TeX ignores blanks following an escaped character; if you want a blank (as
1836 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1837
1838 sub _latex_escape {
1839   my $value = shift;
1840   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1841   $value =~ s/([<>])/\$$1\$/g;
1842   $value;
1843 }
1844
1845 #utility methods for print_*
1846
1847 sub balance_due_msg {
1848   my $self = shift;
1849   my $msg = 'Balance Due';
1850   return $msg unless $conf->exists('invoice_default_terms');
1851   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1852     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1853   } elsif ( $conf->config('invoice_default_terms') ) {
1854     $msg .= ' - '. $conf->config('invoice_default_terms');
1855   }
1856   $msg;
1857 }
1858
1859 sub _items {
1860   my $self = shift;
1861   my @display = scalar(@_)
1862                 ? @_
1863                 : qw( _items_previous _items_pkg );
1864                 #: qw( _items_pkg );
1865                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1866   my @b = ();
1867   foreach my $display ( @display ) {
1868     push @b, $self->$display(@_);
1869   }
1870   @b;
1871 }
1872
1873 sub _items_previous {
1874   my $self = shift;
1875   my $cust_main = $self->cust_main;
1876   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1877   my @b = ();
1878   foreach ( @pr_cust_bill ) {
1879     push @b, {
1880       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1881                        ' ('. time2str('%x',$_->_date). ')',
1882       #'pkgpart'     => 'N/A',
1883       'pkgnum'      => 'N/A',
1884       'amount'      => sprintf("%.2f", $_->owed),
1885     };
1886   }
1887   @b;
1888
1889   #{
1890   #    'description'     => 'Previous Balance',
1891   #    #'pkgpart'         => 'N/A',
1892   #    'pkgnum'          => 'N/A',
1893   #    'amount'          => sprintf("%10.2f", $pr_total ),
1894   #    'ext_description' => [ map {
1895   #                                 "Invoice ". $_->invnum.
1896   #                                 " (". time2str("%x",$_->_date). ") ".
1897   #                                 sprintf("%10.2f", $_->owed)
1898   #                         } @pr_cust_bill ],
1899
1900   #};
1901 }
1902
1903 sub _items_pkg {
1904   my $self = shift;
1905   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1906   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1907 }
1908
1909 sub _items_tax {
1910   my $self = shift;
1911   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1912   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1913 }
1914
1915 sub _items_cust_bill_pkg {
1916   my $self = shift;
1917   my $cust_bill_pkg = shift;
1918
1919   my @b = ();
1920   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1921
1922     if ( $cust_bill_pkg->pkgnum ) {
1923
1924       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1925       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1926       my $pkg = $part_pkg->pkg;
1927
1928       if ( $cust_bill_pkg->setup != 0 ) {
1929         my $description = $pkg;
1930         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1931         my @d = $cust_pkg->h_labels_short($self->_date);
1932         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1933         push @b, {
1934           description     => $description,
1935           #pkgpart         => $part_pkg->pkgpart,
1936           pkgnum          => $cust_pkg->pkgnum,
1937           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
1938           ext_description => \@d,
1939         };
1940       }
1941
1942       if ( $cust_bill_pkg->recur != 0 ) {
1943         push @b, {
1944           description     => "$pkg (" .
1945                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1946                                time2str('%x', $cust_bill_pkg->edate). ')',
1947           #pkgpart         => $part_pkg->pkgpart,
1948           pkgnum          => $cust_pkg->pkgnum,
1949           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
1950           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1951                                                          $cust_bill_pkg->sdate),
1952                                $cust_bill_pkg->details,
1953                              ],
1954         };
1955       }
1956
1957     } else { #pkgnum tax or one-shot line item (??)
1958
1959       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1960                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1961                      : 'Tax';
1962       if ( $cust_bill_pkg->setup != 0 ) {
1963         push @b, {
1964           'description' => $itemdesc,
1965           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
1966         };
1967       }
1968       if ( $cust_bill_pkg->recur != 0 ) {
1969         push @b, {
1970           'description' => "$itemdesc (".
1971                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1972                            time2str("%x", $cust_bill_pkg->edate). ')',
1973           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
1974         };
1975       }
1976
1977     }
1978
1979   }
1980
1981   @b;
1982
1983 }
1984
1985 sub _items_credits {
1986   my $self = shift;
1987
1988   my @b;
1989   #credits
1990   foreach ( $self->cust_credited ) {
1991
1992     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1993
1994     my $reason = $_->cust_credit->reason;
1995     #my $reason = substr($_->cust_credit->reason,0,32);
1996     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1997     $reason = " ($reason) " if $reason;
1998     push @b, {
1999       #'description' => 'Credit ref\#'. $_->crednum.
2000       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2001       #                 $reason,
2002       'description' => 'Credit applied '.
2003                        time2str("%x",$_->cust_credit->_date). $reason,
2004       'amount'      => sprintf("%.2f",$_->amount),
2005     };
2006   }
2007   #foreach ( @cr_cust_credit ) {
2008   #  push @buf,[
2009   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2010   #    $money_char. sprintf("%10.2f",$_->credited)
2011   #  ];
2012   #}
2013
2014   @b;
2015
2016 }
2017
2018 sub _items_payments {
2019   my $self = shift;
2020
2021   my @b;
2022   #get & print payments
2023   foreach ( $self->cust_bill_pay ) {
2024
2025     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2026
2027     push @b, {
2028       'description' => "Payment received ".
2029                        time2str("%x",$_->cust_pay->_date ),
2030       'amount'      => sprintf("%.2f", $_->amount )
2031     };
2032   }
2033
2034   @b;
2035
2036 }
2037
2038 =back
2039
2040 =head1 BUGS
2041
2042 The delete method.
2043
2044 print_text formatting (and some logic :/) is in source, but needs to be
2045 slurped in from a file.  Also number of lines ($=).
2046
2047 =head1 SEE ALSO
2048
2049 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2050 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2051 documentation.
2052
2053 =cut
2054
2055 1;
2056