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