spool invoice to billco if no other destinations are set!
[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 format - 'default' or 'billco'
743
744 #???
745 If I<format> is not specified or "default", the file will be named
746 "N-YYYYMMDDHHMMSS.csv" where N is the invoice number and YYMMDDHHMMSS is a
747 timestamp.
748
749 #???
750 If I<format> is "billco", two files will be created and uploaded.  They will be named "N-YYYYMMDDHHMMSS-header.csv" and "N-YYYYMMDDHHMMSS-detail.csv" where N
751 is the invoice number and YYMMDDHHMMSS is a timestamp(???).
752
753 See L</print_csv> for a description of the output format.
754
755 =cut
756
757 sub send_csv {
758   my($self, %opt) = @_;
759
760   #create file(s)
761
762   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
763   mkdir $spooldir, 0700 unless -d $spooldir;
764
765   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
766   my $file = "$spooldir/$tracctnum";
767   if ( lc($opt{'format'}) eq 'billco' ) {
768     $file .= '-header.csv';
769   } else {
770     #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
771     $file .= '.csv';
772   }
773   
774   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
775
776   open(CSV, ">$file") or die "can't open $file: $!";
777   print CSV $header;
778
779   my $oldfile = '';
780   if ( lc($opt{'format'}) eq 'billco' ) {
781     close CSV;
782     $oldfile = $file;
783     $file = "$spooldir/$tracctnum-detail.csv";
784     open(CSV,">$file") or die "can't open $file: $!";
785   }
786
787   print CSV $detail;
788
789   close CSV;
790
791   my $net;
792   if ( $opt{protocol} eq 'ftp' ) {
793     eval "use Net::FTP;";
794     die $@ if $@;
795     $net = Net::FTP->new($opt{server}) or die @$;
796   } else {
797     die "unknown protocol: $opt{protocol}";
798   }
799
800   $net->login( $opt{username}, $opt{password} )
801     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
802
803   $net->binary or die "can't set binary mode";
804
805   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
806
807   if ( $oldfile) {
808     $net->put($oldfile) or die "can't put $oldfile: $!";
809   }
810   $net->put($file) or die "can't put $file: $!";
811
812   $net->quit;
813
814   unlink $oldfile if $oldfile;
815   unlink $file;
816
817 }
818
819 =item spool_csv
820
821 Spools CSV invoice data.
822
823 Options are:
824
825 =over 4
826
827 =item format - 'default' or 'billco'
828
829 =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>).
830
831 =back
832
833 =cut
834
835 sub spool_csv {
836   my($self, %opt) = @_;
837
838   if ( $opt{'dest'} ) {
839     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
840                              $self->cust_main->invoicing_list;
841     return 'N/A' unless $invoicing_list{$opt{'dest'}}
842                      || ! keys %invoicing_list;
843   }
844
845   #create file(s)
846
847   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
848   mkdir $spooldir, 0700 unless -d $spooldir;
849
850   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
851   my $file = "$spooldir/spool";
852   if ( lc($opt{'format'}) eq 'billco' ) {
853     $file .= '-header.csv';
854   } else {
855     #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
856     $file .= '.csv';
857   }
858   
859   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
860
861   open(CSV, ">>$file") or die "can't open $file: $!";
862   flock(CSV, LOCK_EX);
863   seek(CSV, 0, 2);
864
865   print CSV $header;
866
867   my $oldfile = '';
868   if ( lc($opt{'format'}) eq 'billco' ) {
869
870     flock(CSV, LOCK_UN);
871     close CSV;
872
873     $oldfile = $file;
874     $file = "$spooldir/spool-detail.csv";
875
876     open(CSV,">>$file") or die "can't open $file: $!";
877     flock(CSV, LOCK_EX);
878     seek(CSV, 0, 2);
879   }
880
881   print CSV $detail;
882
883   flock(CSV, LOCK_UN);
884   close CSV;
885
886   return '';
887
888 }
889
890 =item print_csv OPTION => VALUE, ...
891
892 Returns CSV data for this invoice.
893
894 Options are:
895
896 format - 'default' or 'billco'
897
898 Returns a list consisting of two scalars.  The first is a single line of CSV
899 header information for this invoice.  The second is one or more lines of CSV
900 detail information for this invoice.
901
902 If I<format> is not specified or "default", the fields of the CSV file are as
903 follows:
904
905 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
906
907 =over 4
908
909 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
910
911 B<record_type> is C<cust_bill> for the initial header line only.  The
912 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
913 fields are filled in.
914
915 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
916 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
917 are filled in.
918
919 =item invnum - invoice number
920
921 =item custnum - customer number
922
923 =item _date - invoice date
924
925 =item charged - total invoice amount
926
927 =item first - customer first name
928
929 =item last - customer first name
930
931 =item company - company name
932
933 =item address1 - address line 1
934
935 =item address2 - address line 1
936
937 =item city
938
939 =item state
940
941 =item zip
942
943 =item country
944
945 =item pkg - line item description
946
947 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
948
949 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
950
951 =item sdate - start date for recurring fee
952
953 =item edate - end date for recurring fee
954
955 =back
956
957 If I<format> is "billco", the fields of the header CSV file are as follows:
958
959   +-------------------------------------------------------------------+
960   |                        FORMAT HEADER FILE                         |
961   |-------------------------------------------------------------------|
962   | Field | Description                   | Name       | Type | Width |
963   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
964   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
965   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
966   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
967   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
968   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
969   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
970   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
971   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
972   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
973   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
974   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
975   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
976   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
977   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
978   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
979   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
980   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
981   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
982   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
983   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
984   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
985   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
986   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
987   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
988   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
989   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
990   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
991   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
992   +-------+-------------------------------+------------+------+-------+
993
994 If I<format> is "billco", the fields of the detail CSV file are as follows:
995
996                                   FORMAT FOR DETAIL FILE
997         |                            |           |      |
998   Field | Description                | Name      | Type | Width
999   1     | N/A-Leave Empty            | RC        | CHAR |     2
1000   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1001   3     | Account Number             | TRACCTNUM | CHAR |    15
1002   4     | Invoice Number             | TRINVOICE | CHAR |    15
1003   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1004   6     | Transaction Detail         | DETAILS   | CHAR |   100
1005   7     | Amount                     | AMT       | NUM* |     9
1006   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1007   9     | Grouping Code              | GROUP     | CHAR |     2
1008   10    | User Defined               | ACCT CODE | CHAR |    15
1009
1010 =cut
1011
1012 sub print_csv {
1013   my($self, %opt) = @_;
1014   
1015   eval "use Text::CSV_XS";
1016   die $@ if $@;
1017
1018   my $cust_main = $self->cust_main;
1019
1020   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1021
1022   if ( lc($opt{'format'}) eq 'billco' ) {
1023
1024     my $taxtotal = 0;
1025     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1026
1027     my $duedate = '';
1028     if (    $conf->exists('invoice_default_terms') 
1029          && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1030       $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1031     }
1032
1033     my( $previous_balance, @unused ) = $self->previous; #previous balance
1034
1035     my $pmt_cr_applied = 0;
1036     $pmt_cr_applied += $_->{'amount'}
1037       foreach ( $self->_items_payments, $self->_items_credits ) ;
1038
1039     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1040
1041     $csv->combine(
1042       '',                         #  1 | N/A-Leave Empty               CHAR   2
1043       '',                         #  2 | N/A-Leave Empty               CHAR  15
1044       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1045       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1046       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1047       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1048       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1049       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1050       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1051       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1052       '',                         # 10 | Ancillary Billing Information CHAR  30
1053       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1054       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1055
1056       # XXX ?
1057       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1058
1059       # XXX ?
1060       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1061
1062       $previous_balance,          # 15 | Previous Balance              NUM*   9
1063       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1064       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1065       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1066       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1067       '',                         # 20 | 30 Day Aging                  NUM*   9
1068       '',                         # 21 | 60 Day Aging                  NUM*   9
1069       '',                         # 22 | 90 Day Aging                  NUM*   9
1070       'N',                        # 23 | Y/N                           CHAR   1
1071       '',                         # 24 | Remittance automation         CHAR 100
1072       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1073       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1074       '0',                        # 27 | Federal Tax***                NUM*   9
1075       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1076       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1077     );
1078
1079   } else {
1080   
1081     $csv->combine(
1082       'cust_bill',
1083       $self->invnum,
1084       $self->custnum,
1085       time2str("%x", $self->_date),
1086       sprintf("%.2f", $self->charged),
1087       ( map { $cust_main->getfield($_) }
1088           qw( first last company address1 address2 city state zip country ) ),
1089       map { '' } (1..5),
1090     ) or die "can't create csv";
1091   }
1092
1093   my $header = $csv->string. "\n";
1094
1095   my $detail = '';
1096   if ( lc($opt{'format'}) eq 'billco' ) {
1097
1098     my $lineseq = 0;
1099     foreach my $item ( $self->_items_pkg ) {
1100
1101       $csv->combine(
1102         '',                     #  1 | N/A-Leave Empty            CHAR   2
1103         '',                     #  2 | N/A-Leave Empty            CHAR  15
1104         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1105         $self->invnum,          #  4 | Invoice Number             CHAR  15
1106         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1107         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1108         $item->{'amount'},      #  7 | Amount                     NUM*   9
1109         '',                     #  8 | Line Format Control**      CHAR   2
1110         '',                     #  9 | Grouping Code              CHAR   2
1111         '',                     # 10 | User Defined               CHAR  15
1112       );
1113
1114       $detail .= $csv->string. "\n";
1115
1116     }
1117
1118   } else {
1119
1120     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1121
1122       my($pkg, $setup, $recur, $sdate, $edate);
1123       if ( $cust_bill_pkg->pkgnum ) {
1124       
1125         ($pkg, $setup, $recur, $sdate, $edate) = (
1126           $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1127           ( $cust_bill_pkg->setup != 0
1128             ? sprintf("%.2f", $cust_bill_pkg->setup )
1129             : '' ),
1130           ( $cust_bill_pkg->recur != 0
1131             ? sprintf("%.2f", $cust_bill_pkg->recur )
1132             : '' ),
1133           ( $cust_bill_pkg->sdate 
1134             ? time2str("%x", $cust_bill_pkg->sdate)
1135             : '' ),
1136           ($cust_bill_pkg->edate 
1137             ?time2str("%x", $cust_bill_pkg->edate)
1138             : '' ),
1139         );
1140   
1141       } else { #pkgnum tax
1142         next unless $cust_bill_pkg->setup != 0;
1143         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1144                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1145                          : 'Tax';
1146         ($pkg, $setup, $recur, $sdate, $edate) =
1147           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1148       }
1149   
1150       $csv->combine(
1151         'cust_bill_pkg',
1152         $self->invnum,
1153         ( map { '' } (1..11) ),
1154         ($pkg, $setup, $recur, $sdate, $edate)
1155       ) or die "can't create csv";
1156
1157       $detail .= $csv->string. "\n";
1158
1159     }
1160
1161   }
1162
1163   ( $header, $detail );
1164
1165 }
1166
1167 =item comp
1168
1169 Pays this invoice with a compliemntary payment.  If there is an error,
1170 returns the error, otherwise returns false.
1171
1172 =cut
1173
1174 sub comp {
1175   my $self = shift;
1176   my $cust_pay = new FS::cust_pay ( {
1177     'invnum'   => $self->invnum,
1178     'paid'     => $self->owed,
1179     '_date'    => '',
1180     'payby'    => 'COMP',
1181     'payinfo'  => $self->cust_main->payinfo,
1182     'paybatch' => '',
1183   } );
1184   $cust_pay->insert;
1185 }
1186
1187 =item realtime_card
1188
1189 Attempts to pay this invoice with a credit card payment via a
1190 Business::OnlinePayment realtime gateway.  See
1191 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1192 for supported processors.
1193
1194 =cut
1195
1196 sub realtime_card {
1197   my $self = shift;
1198   $self->realtime_bop( 'CC', @_ );
1199 }
1200
1201 =item realtime_ach
1202
1203 Attempts to pay this invoice with an electronic check (ACH) payment via a
1204 Business::OnlinePayment realtime gateway.  See
1205 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1206 for supported processors.
1207
1208 =cut
1209
1210 sub realtime_ach {
1211   my $self = shift;
1212   $self->realtime_bop( 'ECHECK', @_ );
1213 }
1214
1215 =item realtime_lec
1216
1217 Attempts to pay this invoice with phone bill (LEC) payment via a
1218 Business::OnlinePayment realtime gateway.  See
1219 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1220 for supported processors.
1221
1222 =cut
1223
1224 sub realtime_lec {
1225   my $self = shift;
1226   $self->realtime_bop( 'LEC', @_ );
1227 }
1228
1229 sub realtime_bop {
1230   my( $self, $method ) = @_;
1231
1232   my $cust_main = $self->cust_main;
1233   my $balance = $cust_main->balance;
1234   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1235   $amount = sprintf("%.2f", $amount);
1236   return "not run (balance $balance)" unless $amount > 0;
1237
1238   my $description = 'Internet Services';
1239   if ( $conf->exists('business-onlinepayment-description') ) {
1240     my $dtempl = $conf->config('business-onlinepayment-description');
1241
1242     my $agent_obj = $cust_main->agent
1243       or die "can't retreive agent for $cust_main (agentnum ".
1244              $cust_main->agentnum. ")";
1245     my $agent = $agent_obj->agent;
1246     my $pkgs = join(', ',
1247       map { $_->cust_pkg->part_pkg->pkg }
1248         grep { $_->pkgnum } $self->cust_bill_pkg
1249     );
1250     $description = eval qq("$dtempl");
1251   }
1252
1253   $cust_main->realtime_bop($method, $amount,
1254     'description' => $description,
1255     'invnum'      => $self->invnum,
1256   );
1257
1258 }
1259
1260 =item batch_card
1261
1262 Adds a payment for this invoice to the pending credit card batch (see
1263 L<FS::cust_pay_batch>).
1264
1265 =cut
1266
1267 sub batch_card {
1268   my $self = shift;
1269   my $cust_main = $self->cust_main;
1270
1271   my $cust_pay_batch = new FS::cust_pay_batch ( {
1272     'invnum'   => $self->getfield('invnum'),
1273     'custnum'  => $cust_main->getfield('custnum'),
1274     'last'     => $cust_main->getfield('last'),
1275     'first'    => $cust_main->getfield('first'),
1276     'address1' => $cust_main->getfield('address1'),
1277     'address2' => $cust_main->getfield('address2'),
1278     'city'     => $cust_main->getfield('city'),
1279     'state'    => $cust_main->getfield('state'),
1280     'zip'      => $cust_main->getfield('zip'),
1281     'country'  => $cust_main->getfield('country'),
1282     'cardnum'  => $cust_main->payinfo,
1283     'exp'      => $cust_main->getfield('paydate'),
1284     'payname'  => $cust_main->getfield('payname'),
1285     'amount'   => $self->owed,
1286   } );
1287   my $error = $cust_pay_batch->insert;
1288   die $error if $error;
1289
1290   '';
1291 }
1292
1293 sub _agent_template {
1294   my $self = shift;
1295   $self->_agent_plandata('agent_templatename');
1296 }
1297
1298 sub _agent_invoice_from {
1299   my $self = shift;
1300   $self->_agent_plandata('agent_invoice_from');
1301 }
1302
1303 sub _agent_plandata {
1304   my( $self, $option ) = @_;
1305
1306   my $part_bill_event = qsearchs( 'part_bill_event',
1307     {
1308       'payby'     => $self->cust_main->payby,
1309       'plan'      => 'send_agent',
1310       'plandata'  => { 'op'    => '~',
1311                        'value' => "(^|\n)agentnum ".
1312                                    '([0-9]*, )*'.
1313                                   $self->cust_main->agentnum.
1314                                    '(, [0-9]*)*'.
1315                                   "(\n|\$)",
1316                      },
1317     },
1318     '',
1319     'ORDER BY seconds LIMIT 1'
1320   );
1321
1322   return '' unless $part_bill_event;
1323
1324   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1325     return $1;
1326   } else {
1327     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1328          " plandata for $option";
1329     return '';
1330   }
1331
1332 }
1333
1334 =item print_text [ TIME [ , TEMPLATE ] ]
1335
1336 Returns an text invoice, as a list of lines.
1337
1338 TIME an optional value used to control the printing of overdue messages.  The
1339 default is now.  It isn't the date of the invoice; that's the `_date' field.
1340 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1341 L<Time::Local> and L<Date::Parse> for conversion functions.
1342
1343 =cut
1344
1345 #still some false laziness w/_items stuff (and send_csv)
1346 sub print_text {
1347
1348   my( $self, $today, $template ) = @_;
1349   $today ||= time;
1350
1351 #  my $invnum = $self->invnum;
1352   my $cust_main = $self->cust_main;
1353   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1354     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1355
1356   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1357 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1358   #my $balance_due = $self->owed + $pr_total - $cr_total;
1359   my $balance_due = $self->owed + $pr_total;
1360
1361   #my @collect = ();
1362   #my($description,$amount);
1363   @buf = ();
1364
1365   #previous balance
1366   foreach ( @pr_cust_bill ) {
1367     push @buf, [
1368       "Previous Balance, Invoice #". $_->invnum. 
1369                  " (". time2str("%x",$_->_date). ")",
1370       $money_char. sprintf("%10.2f",$_->owed)
1371     ];
1372   }
1373   if (@pr_cust_bill) {
1374     push @buf,['','-----------'];
1375     push @buf,[ 'Total Previous Balance',
1376                 $money_char. sprintf("%10.2f",$pr_total ) ];
1377     push @buf,['',''];
1378   }
1379
1380   #new charges
1381   foreach my $cust_bill_pkg (
1382     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1383     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1384   ) {
1385
1386     my $desc = $cust_bill_pkg->desc;
1387
1388     if ( $cust_bill_pkg->pkgnum > 0 ) {
1389
1390       if ( $cust_bill_pkg->setup != 0 ) {
1391         my $description = $desc;
1392         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1393         push @buf, [ $description,
1394                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1395         push @buf,
1396           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1397               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1398       }
1399
1400       if ( $cust_bill_pkg->recur != 0 ) {
1401         push @buf, [
1402           "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1403                       time2str("%x", $cust_bill_pkg->edate) . ")",
1404           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1405         ];
1406         push @buf,
1407           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1408               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1409                                                   $cust_bill_pkg->sdate );
1410       }
1411
1412       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1413
1414     } else { #pkgnum tax or one-shot line item
1415
1416       if ( $cust_bill_pkg->setup != 0 ) {
1417         push @buf, [ $desc,
1418                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1419       }
1420       if ( $cust_bill_pkg->recur != 0 ) {
1421         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1422                               . time2str("%x", $cust_bill_pkg->edate). ")",
1423                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1424                    ];
1425       }
1426
1427     }
1428
1429   }
1430
1431   push @buf,['','-----------'];
1432   push @buf,['Total New Charges',
1433              $money_char. sprintf("%10.2f",$self->charged) ];
1434   push @buf,['',''];
1435
1436   push @buf,['','-----------'];
1437   push @buf,['Total Charges',
1438              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1439   push @buf,['',''];
1440
1441   #credits
1442   foreach ( $self->cust_credited ) {
1443
1444     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1445
1446     my $reason = substr($_->cust_credit->reason,0,32);
1447     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1448     $reason = " ($reason) " if $reason;
1449     push @buf,[
1450       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1451         $reason,
1452       $money_char. sprintf("%10.2f",$_->amount)
1453     ];
1454   }
1455   #foreach ( @cr_cust_credit ) {
1456   #  push @buf,[
1457   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1458   #    $money_char. sprintf("%10.2f",$_->credited)
1459   #  ];
1460   #}
1461
1462   #get & print payments
1463   foreach ( $self->cust_bill_pay ) {
1464
1465     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1466
1467     push @buf,[
1468       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1469       $money_char. sprintf("%10.2f",$_->amount )
1470     ];
1471   }
1472
1473   #balance due
1474   my $balance_due_msg = $self->balance_due_msg;
1475
1476   push @buf,['','-----------'];
1477   push @buf,[$balance_due_msg, $money_char. 
1478     sprintf("%10.2f", $balance_due ) ];
1479
1480   #create the template
1481   $template ||= $self->_agent_template;
1482   my $templatefile = 'invoice_template';
1483   $templatefile .= "_$template" if length($template);
1484   my @invoice_template = $conf->config($templatefile)
1485     or die "cannot load config file $templatefile";
1486   $invoice_lines = 0;
1487   my $wasfunc = 0;
1488   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1489     /invoice_lines\((\d*)\)/;
1490     $invoice_lines += $1 || scalar(@buf);
1491     $wasfunc=1;
1492   }
1493   die "no invoice_lines() functions in template?" unless $wasfunc;
1494   my $invoice_template = new Text::Template (
1495     TYPE   => 'ARRAY',
1496     SOURCE => [ map "$_\n", @invoice_template ],
1497   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1498   $invoice_template->compile()
1499     or die "can't compile template: $Text::Template::ERROR";
1500
1501   #setup template variables
1502   package FS::cust_bill::_template; #!
1503   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1504
1505   $invnum = $self->invnum;
1506   $date = $self->_date;
1507   $page = 1;
1508   $agent = $self->cust_main->agent->agent;
1509
1510   if ( $FS::cust_bill::invoice_lines ) {
1511     $total_pages =
1512       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1513     $total_pages++
1514       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1515   } else {
1516     $total_pages = 1;
1517   }
1518
1519   #format address (variable for the template)
1520   my $l = 0;
1521   @address = ( '', '', '', '', '', '' );
1522   package FS::cust_bill; #!
1523   $FS::cust_bill::_template::address[$l++] =
1524     $cust_main->payname.
1525       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1526         ? " (P.O. #". $cust_main->payinfo. ")"
1527         : ''
1528       )
1529   ;
1530   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1531     if $cust_main->company;
1532   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1533   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1534     if $cust_main->address2;
1535   $FS::cust_bill::_template::address[$l++] =
1536     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1537
1538   my $countrydefault = $conf->config('countrydefault') || 'US';
1539   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1540     unless $cust_main->country eq $countrydefault;
1541
1542         #  #overdue? (variable for the template)
1543         #  $FS::cust_bill::_template::overdue = ( 
1544         #    $balance_due > 0
1545         #    && $today > $self->_date 
1546         ##    && $self->printed > 1
1547         #    && $self->printed > 0
1548         #  );
1549
1550   #and subroutine for the template
1551   sub FS::cust_bill::_template::invoice_lines {
1552     my $lines = shift || scalar(@buf);
1553     map { 
1554       scalar(@buf) ? shift @buf : [ '', '' ];
1555     }
1556     ( 1 .. $lines );
1557   }
1558
1559   #and fill it in
1560   $FS::cust_bill::_template::page = 1;
1561   my $lines;
1562   my @collect;
1563   while (@buf) {
1564     push @collect, split("\n",
1565       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1566     );
1567     $FS::cust_bill::_template::page++;
1568   }
1569
1570   map "$_\n", @collect;
1571
1572 }
1573
1574 =item print_latex [ TIME [ , TEMPLATE ] ]
1575
1576 Internal method - returns a filename of a filled-in LaTeX template for this
1577 invoice (Note: add ".tex" to get the actual filename).
1578
1579 See print_ps and print_pdf for methods that return PostScript and PDF output.
1580
1581 TIME an optional value used to control the printing of overdue messages.  The
1582 default is now.  It isn't the date of the invoice; that's the `_date' field.
1583 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1584 L<Time::Local> and L<Date::Parse> for conversion functions.
1585
1586 =cut
1587
1588 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1589 sub print_latex {
1590
1591   my( $self, $today, $template ) = @_;
1592   $today ||= time;
1593   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1594     if $DEBUG;
1595
1596   my $cust_main = $self->cust_main;
1597   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1598     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1599
1600   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1601 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1602   #my $balance_due = $self->owed + $pr_total - $cr_total;
1603   my $balance_due = $self->owed + $pr_total;
1604
1605   #create the template
1606   $template ||= $self->_agent_template;
1607   my $templatefile = 'invoice_latex';
1608   my $suffix = length($template) ? "_$template" : '';
1609   $templatefile .= $suffix;
1610   my @invoice_template = map "$_\n", $conf->config($templatefile)
1611     or die "cannot load config file $templatefile";
1612
1613   my($format, $text_template);
1614   if ( grep { /^%%Detail/ } @invoice_template ) {
1615     #change this to a die when the old code is removed
1616     warn "old-style invoice template $templatefile; ".
1617          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1618     $format = 'old';
1619   } else {
1620     $format = 'Text::Template';
1621     $text_template = new Text::Template(
1622       TYPE => 'ARRAY',
1623       SOURCE => \@invoice_template,
1624       DELIMITERS => [ '[@--', '--@]' ],
1625     );
1626
1627     $text_template->compile()
1628       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1629   }
1630
1631   my $returnaddress;
1632   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1633     $returnaddress = join("\n",
1634       $conf->config_orbase('invoice_latexreturnaddress', $template)
1635     );
1636   } else {
1637     $returnaddress = '~';
1638   }
1639
1640   my %invoice_data = (
1641     'invnum'       => $self->invnum,
1642     'date'         => time2str('%b %o, %Y', $self->_date),
1643     'today'        => time2str('%b %o, %Y', $today),
1644     'agent'        => _latex_escape($cust_main->agent->agent),
1645     'payname'      => _latex_escape($cust_main->payname),
1646     'company'      => _latex_escape($cust_main->company),
1647     'address1'     => _latex_escape($cust_main->address1),
1648     'address2'     => _latex_escape($cust_main->address2),
1649     'city'         => _latex_escape($cust_main->city),
1650     'state'        => _latex_escape($cust_main->state),
1651     'zip'          => _latex_escape($cust_main->zip),
1652     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1653     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1654     'returnaddress' => $returnaddress,
1655     'quantity'     => 1,
1656     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1657     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1658     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1659   );
1660
1661   my $countrydefault = $conf->config('countrydefault') || 'US';
1662   if ( $cust_main->country eq $countrydefault ) {
1663     $invoice_data{'country'} = '';
1664   } else {
1665     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1666   }
1667
1668   $invoice_data{'notes'} =
1669     join("\n",
1670 #  #do variable substitutions in notes
1671 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1672         $conf->config_orbase('invoice_latexnotes', $template)
1673     );
1674   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1675     if $DEBUG;
1676
1677   $invoice_data{'footer'} =~ s/\n+$//;
1678   $invoice_data{'smallfooter'} =~ s/\n+$//;
1679   $invoice_data{'notes'} =~ s/\n+$//;
1680
1681   $invoice_data{'po_line'} =
1682     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1683       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1684       : '~';
1685
1686   my @filled_in = ();
1687   if ( $format eq 'old' ) {
1688   
1689     my @line_item = ();
1690     my @total_item = ();
1691     while ( @invoice_template ) {
1692       my $line = shift @invoice_template;
1693   
1694       if ( $line =~ /^%%Detail\s*$/ ) {
1695   
1696         while ( ( my $line_item_line = shift @invoice_template )
1697                 !~ /^%%EndDetail\s*$/                            ) {
1698           push @line_item, $line_item_line;
1699         }
1700         foreach my $line_item ( $self->_items ) {
1701         #foreach my $line_item ( $self->_items_pkg ) {
1702           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1703           $invoice_data{'description'} =
1704             _latex_escape($line_item->{'description'});
1705           if ( exists $line_item->{'ext_description'} ) {
1706             $invoice_data{'description'} .=
1707               "\\tabularnewline\n~~".
1708               join( "\\tabularnewline\n~~",
1709                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1710                   );
1711           }
1712           $invoice_data{'amount'} = $line_item->{'amount'};
1713           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1714           push @filled_in,
1715             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1716         }
1717   
1718       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1719   
1720         while ( ( my $total_item_line = shift @invoice_template )
1721                 !~ /^%%EndTotalDetails\s*$/                      ) {
1722           push @total_item, $total_item_line;
1723         }
1724   
1725         my @total_fill = ();
1726   
1727         my $taxtotal = 0;
1728         foreach my $tax ( $self->_items_tax ) {
1729           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1730           $taxtotal += $tax->{'amount'};
1731           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1732           push @total_fill,
1733             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1734                 @total_item;
1735         }
1736
1737         if ( $taxtotal ) {
1738           $invoice_data{'total_item'} = 'Sub-total';
1739           $invoice_data{'total_amount'} =
1740             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1741           unshift @total_fill,
1742             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1743                 @total_item;
1744         }
1745   
1746         $invoice_data{'total_item'} = '\textbf{Total}';
1747         $invoice_data{'total_amount'} =
1748           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1749         push @total_fill,
1750           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1751               @total_item;
1752   
1753         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1754   
1755         # credits
1756         foreach my $credit ( $self->_items_credits ) {
1757           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1758           #$credittotal
1759           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1760           push @total_fill, 
1761             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1762                 @total_item;
1763         }
1764   
1765         # payments
1766         foreach my $payment ( $self->_items_payments ) {
1767           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1768           #$paymenttotal
1769           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1770           push @total_fill, 
1771             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1772                 @total_item;
1773         }
1774   
1775         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1776         $invoice_data{'total_amount'} =
1777           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1778         push @total_fill,
1779           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1780               @total_item;
1781   
1782         push @filled_in, @total_fill;
1783   
1784       } else {
1785         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1786         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1787         push @filled_in, $line;
1788       }
1789   
1790     }
1791
1792     sub nounder {
1793       my $var = $1;
1794       $var =~ s/_/\-/g;
1795       $var;
1796     }
1797
1798   } elsif ( $format eq 'Text::Template' ) {
1799
1800     my @detail_items = ();
1801     my @total_items = ();
1802
1803     $invoice_data{'detail_items'} = \@detail_items;
1804     $invoice_data{'total_items'} = \@total_items;
1805   
1806     foreach my $line_item ( $self->_items ) {
1807       my $detail = {
1808         ext_description => [],
1809       };
1810       $detail->{'ref'} = $line_item->{'pkgnum'};
1811       $detail->{'quantity'} = 1;
1812       $detail->{'description'} = _latex_escape($line_item->{'description'});
1813       if ( exists $line_item->{'ext_description'} ) {
1814         @{$detail->{'ext_description'}} = map {
1815           _latex_escape($_);
1816         } @{$line_item->{'ext_description'}};
1817       }
1818       $detail->{'amount'} = $line_item->{'amount'};
1819       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1820   
1821       push @detail_items, $detail;
1822     }
1823   
1824   
1825     my $taxtotal = 0;
1826     foreach my $tax ( $self->_items_tax ) {
1827       my $total = {};
1828       $total->{'total_item'} = _latex_escape($tax->{'description'});
1829       $taxtotal += $tax->{'amount'};
1830       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1831       push @total_items, $total;
1832     }
1833   
1834     if ( $taxtotal ) {
1835       my $total = {};
1836       $total->{'total_item'} = 'Sub-total';
1837       $total->{'total_amount'} =
1838         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1839       unshift @total_items, $total;
1840     }
1841   
1842     {
1843       my $total = {};
1844       $total->{'total_item'} = '\textbf{Total}';
1845       $total->{'total_amount'} =
1846         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1847       push @total_items, $total;
1848     }
1849   
1850     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1851   
1852     # credits
1853     foreach my $credit ( $self->_items_credits ) {
1854       my $total;
1855       $total->{'total_item'} = _latex_escape($credit->{'description'});
1856       #$credittotal
1857       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1858       push @total_items, $total;
1859     }
1860   
1861     # payments
1862     foreach my $payment ( $self->_items_payments ) {
1863       my $total = {};
1864       $total->{'total_item'} = _latex_escape($payment->{'description'});
1865       #$paymenttotal
1866       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1867       push @total_items, $total;
1868     }
1869   
1870     { 
1871       my $total;
1872       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1873       $total->{'total_amount'} =
1874         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1875       push @total_items, $total;
1876     }
1877
1878   } else {
1879     die "guru meditation #54";
1880   }
1881
1882   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1883   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1884                            DIR      => $dir,
1885                            SUFFIX   => '.tex',
1886                            UNLINK   => 0,
1887                          ) or die "can't open temp file: $!\n";
1888   if ( $format eq 'old' ) {
1889     print $fh join('', @filled_in );
1890   } elsif ( $format eq 'Text::Template' ) {
1891     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1892   } else {
1893     die "guru meditation #32";
1894   }
1895   close $fh;
1896
1897   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1898   return $1;
1899
1900 }
1901
1902 =item print_ps [ TIME [ , TEMPLATE ] ]
1903
1904 Returns an postscript invoice, as a scalar.
1905
1906 TIME an optional value used to control the printing of overdue messages.  The
1907 default is now.  It isn't the date of the invoice; that's the `_date' field.
1908 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1909 L<Time::Local> and L<Date::Parse> for conversion functions.
1910
1911 =cut
1912
1913 sub print_ps {
1914   my $self = shift;
1915
1916   my $file = $self->print_latex(@_);
1917
1918   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1919   chdir($dir);
1920
1921   my $sfile = shell_quote $file;
1922
1923   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1924     or die "pslatex $file.tex failed; see $file.log for details?\n";
1925   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1926     or die "pslatex $file.tex failed; see $file.log for details?\n";
1927
1928   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1929     or die "dvips failed";
1930
1931   open(POSTSCRIPT, "<$file.ps")
1932     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1933
1934   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1935
1936   my $ps = '';
1937   while (<POSTSCRIPT>) {
1938     $ps .= $_;
1939   }
1940
1941   close POSTSCRIPT;
1942
1943   return $ps;
1944
1945 }
1946
1947 =item print_pdf [ TIME [ , TEMPLATE ] ]
1948
1949 Returns an PDF invoice, as a scalar.
1950
1951 TIME an optional value used to control the printing of overdue messages.  The
1952 default is now.  It isn't the date of the invoice; that's the `_date' field.
1953 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1954 L<Time::Local> and L<Date::Parse> for conversion functions.
1955
1956 =cut
1957
1958 sub print_pdf {
1959   my $self = shift;
1960
1961   my $file = $self->print_latex(@_);
1962
1963   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1964   chdir($dir);
1965
1966   #system('pdflatex', "$file.tex");
1967   #system('pdflatex', "$file.tex");
1968   #! LaTeX Error: Unknown graphics extension: .eps.
1969
1970   my $sfile = shell_quote $file;
1971
1972   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1973     or die "pslatex $file.tex failed; see $file.log for details?\n";
1974   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1975     or die "pslatex $file.tex failed; see $file.log for details?\n";
1976
1977   #system('dvipdf', "$file.dvi", "$file.pdf" );
1978   system(
1979     "dvips -q -t letter -f $sfile.dvi ".
1980     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1981     "     -c save pop -"
1982   ) == 0
1983     or die "dvips | gs failed: $!";
1984
1985   open(PDF, "<$file.pdf")
1986     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1987
1988   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1989
1990   my $pdf = '';
1991   while (<PDF>) {
1992     $pdf .= $_;
1993   }
1994
1995   close PDF;
1996
1997   return $pdf;
1998
1999 }
2000
2001 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2002
2003 Returns an HTML invoice, as a scalar.
2004
2005 TIME an optional value used to control the printing of overdue messages.  The
2006 default is now.  It isn't the date of the invoice; that's the `_date' field.
2007 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2008 L<Time::Local> and L<Date::Parse> for conversion functions.
2009
2010 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2011 when emailing the invoice as part of a multipart/related MIME email.
2012
2013 =cut
2014
2015 #some falze laziness w/print_text and print_latex (and send_csv)
2016 sub print_html {
2017   my( $self, $today, $template, $cid ) = @_;
2018   $today ||= time;
2019
2020   my $cust_main = $self->cust_main;
2021   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2022     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2023
2024   $template ||= $self->_agent_template;
2025   my $templatefile = 'invoice_html';
2026   my $suffix = length($template) ? "_$template" : '';
2027   $templatefile .= $suffix;
2028   my @html_template = map "$_\n", $conf->config($templatefile)
2029     or die "cannot load config file $templatefile";
2030
2031   my $html_template = new Text::Template(
2032     TYPE   => 'ARRAY',
2033     SOURCE => \@html_template,
2034     DELIMITERS => [ '<%=', '%>' ],
2035   );
2036
2037   $html_template->compile()
2038     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2039
2040   my %invoice_data = (
2041     'invnum'       => $self->invnum,
2042     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2043     'today'        => time2str('%b %o, %Y', $today),
2044     'agent'        => encode_entities($cust_main->agent->agent),
2045     'payname'      => encode_entities($cust_main->payname),
2046     'company'      => encode_entities($cust_main->company),
2047     'address1'     => encode_entities($cust_main->address1),
2048     'address2'     => encode_entities($cust_main->address2),
2049     'city'         => encode_entities($cust_main->city),
2050     'state'        => encode_entities($cust_main->state),
2051     'zip'          => encode_entities($cust_main->zip),
2052     'terms'        => $conf->config('invoice_default_terms')
2053                       || 'Payable upon receipt',
2054     'cid'          => $cid,
2055     'template'     => $template,
2056 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2057   );
2058
2059   if (
2060          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2061       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2062   ) {
2063     $invoice_data{'returnaddress'} =
2064       join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2065   } else {
2066     $invoice_data{'returnaddress'} =
2067       join("\n", map { 
2068                        s/~/&nbsp;/g;
2069                        s/\\\\\*?\s*$/<BR>/;
2070                        s/\\hyphenation\{[\w\s\-]+\}//;
2071                        $_;
2072                      }
2073                      $conf->config_orbase( 'invoice_latexreturnaddress',
2074                                            $template
2075                                          )
2076           );
2077   }
2078
2079   my $countrydefault = $conf->config('countrydefault') || 'US';
2080   if ( $cust_main->country eq $countrydefault ) {
2081     $invoice_data{'country'} = '';
2082   } else {
2083     $invoice_data{'country'} =
2084       encode_entities(code2country($cust_main->country));
2085   }
2086
2087   if (
2088          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2089       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2090   ) {
2091     $invoice_data{'notes'} =
2092       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2093   } else {
2094     $invoice_data{'notes'} = 
2095       join("\n", map { 
2096                        s/%%(.*)$/<!-- $1 -->/;
2097                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2098                        s/\\begin\{enumerate\}/<ol>/;
2099                        s/\\item /  <li>/;
2100                        s/\\end\{enumerate\}/<\/ol>/;
2101                        s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2102                        $_;
2103                      } 
2104                      $conf->config_orbase('invoice_latexnotes', $template)
2105           );
2106   }
2107
2108 #  #do variable substitutions in notes
2109 #  $invoice_data{'notes'} =
2110 #    join("\n",
2111 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2112 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2113 #    );
2114
2115   if (
2116          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2117       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2118   ) {
2119    $invoice_data{'footer'} =
2120      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2121   } else {
2122    $invoice_data{'footer'} =
2123        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2124                       $conf->config_orbase('invoice_latexfooter', $template)
2125            );
2126   }
2127
2128   $invoice_data{'po_line'} =
2129     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2130       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2131       : '';
2132
2133   my $money_char = $conf->config('money_char') || '$';
2134
2135   foreach my $line_item ( $self->_items ) {
2136     my $detail = {
2137       ext_description => [],
2138     };
2139     $detail->{'ref'} = $line_item->{'pkgnum'};
2140     $detail->{'description'} = encode_entities($line_item->{'description'});
2141     if ( exists $line_item->{'ext_description'} ) {
2142       @{$detail->{'ext_description'}} = map {
2143         encode_entities($_);
2144       } @{$line_item->{'ext_description'}};
2145     }
2146     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2147     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2148
2149     push @{$invoice_data{'detail_items'}}, $detail;
2150   }
2151
2152
2153   my $taxtotal = 0;
2154   foreach my $tax ( $self->_items_tax ) {
2155     my $total = {};
2156     $total->{'total_item'} = encode_entities($tax->{'description'});
2157     $taxtotal += $tax->{'amount'};
2158     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2159     push @{$invoice_data{'total_items'}}, $total;
2160   }
2161
2162   if ( $taxtotal ) {
2163     my $total = {};
2164     $total->{'total_item'} = 'Sub-total';
2165     $total->{'total_amount'} =
2166       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2167     unshift @{$invoice_data{'total_items'}}, $total;
2168   }
2169
2170   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2171   {
2172     my $total = {};
2173     $total->{'total_item'} = '<b>Total</b>';
2174     $total->{'total_amount'} =
2175       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2176     push @{$invoice_data{'total_items'}}, $total;
2177   }
2178
2179   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2180
2181   # credits
2182   foreach my $credit ( $self->_items_credits ) {
2183     my $total;
2184     $total->{'total_item'} = encode_entities($credit->{'description'});
2185     #$credittotal
2186     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2187     push @{$invoice_data{'total_items'}}, $total;
2188   }
2189
2190   # payments
2191   foreach my $payment ( $self->_items_payments ) {
2192     my $total = {};
2193     $total->{'total_item'} = encode_entities($payment->{'description'});
2194     #$paymenttotal
2195     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2196     push @{$invoice_data{'total_items'}}, $total;
2197   }
2198
2199   { 
2200     my $total;
2201     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2202     $total->{'total_amount'} =
2203       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2204     push @{$invoice_data{'total_items'}}, $total;
2205   }
2206
2207   $html_template->fill_in( HASH => \%invoice_data);
2208 }
2209
2210 # quick subroutine for print_latex
2211 #
2212 # There are ten characters that LaTeX treats as special characters, which
2213 # means that they do not simply typeset themselves: 
2214 #      # $ % & ~ _ ^ \ { }
2215 #
2216 # TeX ignores blanks following an escaped character; if you want a blank (as
2217 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2218
2219 sub _latex_escape {
2220   my $value = shift;
2221   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2222   $value =~ s/([<>])/\$$1\$/g;
2223   $value;
2224 }
2225
2226 #utility methods for print_*
2227
2228 sub balance_due_msg {
2229   my $self = shift;
2230   my $msg = 'Balance Due';
2231   return $msg unless $conf->exists('invoice_default_terms');
2232   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2233     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2234   } elsif ( $conf->config('invoice_default_terms') ) {
2235     $msg .= ' - '. $conf->config('invoice_default_terms');
2236   }
2237   $msg;
2238 }
2239
2240 sub _items {
2241   my $self = shift;
2242   my @display = scalar(@_)
2243                 ? @_
2244                 : qw( _items_previous _items_pkg );
2245                 #: qw( _items_pkg );
2246                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2247   my @b = ();
2248   foreach my $display ( @display ) {
2249     push @b, $self->$display(@_);
2250   }
2251   @b;
2252 }
2253
2254 sub _items_previous {
2255   my $self = shift;
2256   my $cust_main = $self->cust_main;
2257   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2258   my @b = ();
2259   foreach ( @pr_cust_bill ) {
2260     push @b, {
2261       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2262                        ' ('. time2str('%x',$_->_date). ')',
2263       #'pkgpart'     => 'N/A',
2264       'pkgnum'      => 'N/A',
2265       'amount'      => sprintf("%.2f", $_->owed),
2266     };
2267   }
2268   @b;
2269
2270   #{
2271   #    'description'     => 'Previous Balance',
2272   #    #'pkgpart'         => 'N/A',
2273   #    'pkgnum'          => 'N/A',
2274   #    'amount'          => sprintf("%10.2f", $pr_total ),
2275   #    'ext_description' => [ map {
2276   #                                 "Invoice ". $_->invnum.
2277   #                                 " (". time2str("%x",$_->_date). ") ".
2278   #                                 sprintf("%10.2f", $_->owed)
2279   #                         } @pr_cust_bill ],
2280
2281   #};
2282 }
2283
2284 sub _items_pkg {
2285   my $self = shift;
2286   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2287   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2288 }
2289
2290 sub _items_tax {
2291   my $self = shift;
2292   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2293   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2294 }
2295
2296 sub _items_cust_bill_pkg {
2297   my $self = shift;
2298   my $cust_bill_pkg = shift;
2299
2300   my @b = ();
2301   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2302
2303     my $desc = $cust_bill_pkg->desc;
2304
2305     if ( $cust_bill_pkg->pkgnum > 0 ) {
2306
2307       if ( $cust_bill_pkg->setup != 0 ) {
2308         my $description = $desc;
2309         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2310         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2311         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2312         push @b, {
2313           description     => $description,
2314           #pkgpart         => $part_pkg->pkgpart,
2315           pkgnum          => $cust_bill_pkg->pkgnum,
2316           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2317           ext_description => \@d,
2318         };
2319       }
2320
2321       if ( $cust_bill_pkg->recur != 0 ) {
2322         push @b, {
2323           description     => "$desc (" .
2324                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2325                                time2str('%x', $cust_bill_pkg->edate). ')',
2326           #pkgpart         => $part_pkg->pkgpart,
2327           pkgnum          => $cust_bill_pkg->pkgnum,
2328           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2329           ext_description =>
2330             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2331                                                         $cust_bill_pkg->sdate),
2332               $cust_bill_pkg->details,
2333             ],
2334         };
2335       }
2336
2337     } else { #pkgnum tax or one-shot line item (??)
2338
2339       if ( $cust_bill_pkg->setup != 0 ) {
2340         push @b, {
2341           'description' => $desc,
2342           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2343         };
2344       }
2345       if ( $cust_bill_pkg->recur != 0 ) {
2346         push @b, {
2347           'description' => "$desc (".
2348                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2349                            time2str("%x", $cust_bill_pkg->edate). ')',
2350           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2351         };
2352       }
2353
2354     }
2355
2356   }
2357
2358   @b;
2359
2360 }
2361
2362 sub _items_credits {
2363   my $self = shift;
2364
2365   my @b;
2366   #credits
2367   foreach ( $self->cust_credited ) {
2368
2369     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2370
2371     my $reason = $_->cust_credit->reason;
2372     #my $reason = substr($_->cust_credit->reason,0,32);
2373     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2374     $reason = " ($reason) " if $reason;
2375     push @b, {
2376       #'description' => 'Credit ref\#'. $_->crednum.
2377       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2378       #                 $reason,
2379       'description' => 'Credit applied '.
2380                        time2str("%x",$_->cust_credit->_date). $reason,
2381       'amount'      => sprintf("%.2f",$_->amount),
2382     };
2383   }
2384   #foreach ( @cr_cust_credit ) {
2385   #  push @buf,[
2386   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2387   #    $money_char. sprintf("%10.2f",$_->credited)
2388   #  ];
2389   #}
2390
2391   @b;
2392
2393 }
2394
2395 sub _items_payments {
2396   my $self = shift;
2397
2398   my @b;
2399   #get & print payments
2400   foreach ( $self->cust_bill_pay ) {
2401
2402     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2403
2404     push @b, {
2405       'description' => "Payment received ".
2406                        time2str("%x",$_->cust_pay->_date ),
2407       'amount'      => sprintf("%.2f", $_->amount )
2408     };
2409   }
2410
2411   @b;
2412
2413 }
2414
2415 =back
2416
2417 =head1 SUBROUTINES
2418
2419 =over 4
2420
2421 =item reprint
2422
2423 =cut
2424
2425 sub process_reprint {
2426   process_re_X('print', @_);
2427 }
2428
2429 =item reemail
2430
2431 =cut
2432
2433 sub process_reemail {
2434   process_re_X('email', @_);
2435 }
2436
2437 =item refax
2438
2439 =cut
2440
2441 sub process_refax {
2442   process_re_X('fax', @_);
2443 }
2444
2445 use Storable qw(thaw);
2446 use Data::Dumper;
2447 use MIME::Base64;
2448 sub process_re_X {
2449   my( $method, $job ) = ( shift, shift );
2450
2451   my $param = thaw(decode_base64(shift));
2452   warn Dumper($param) if $DEBUG;
2453
2454   re_X(
2455     $method,
2456     $job,
2457     %$param,
2458   );
2459
2460 }
2461
2462 sub re_X {
2463   my($method, $job, %param ) = @_;
2464 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2465
2466   #some false laziness w/search/cust_bill.html
2467   my $distinct = '';
2468   my $orderby = 'ORDER BY cust_bill._date';
2469
2470   my @where;
2471
2472   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2473     push @where, "cust_bill._date >= $1";
2474   }
2475   if ( $param{'end'} =~ /^(\d+)$/ ) {
2476     push @where, "cust_bill._date < $1";
2477   }
2478   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2479     push @where, "cust_main.agentnum = $1";
2480   }
2481
2482   my $owed =
2483     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2484                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2485              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2486                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2487
2488   push @where, "0 != $owed"
2489     if $param{'open'};
2490
2491   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2492     if $param{'days'};
2493
2494   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2495
2496   my $addl_from = 'left join cust_main using ( custnum )';
2497
2498   if ( $param{'newest_percust'} ) {
2499     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2500     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2501     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2502   }
2503      
2504   my @cust_bill = qsearch( 'cust_bill',
2505                            {},
2506                            "$distinct cust_bill.*",
2507                            $extra_sql,
2508                            '',
2509                            $addl_from
2510                          );
2511
2512   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2513   foreach my $cust_bill ( @cust_bill ) {
2514     $cust_bill->$method();
2515
2516     if ( $job ) { #progressbar foo
2517       $num++;
2518       if ( time - $min_sec > $last ) {
2519         my $error = $job->update_statustext(
2520           int( 100 * $num / scalar(@cust_bill) )
2521         );
2522         die $error if $error;
2523         $last = time;
2524       }
2525     }
2526
2527   }
2528
2529 }
2530
2531 =back
2532
2533 =head1 BUGS
2534
2535 The delete method.
2536
2537 print_text formatting (and some logic :/) is in source, but needs to be
2538 slurped in from a file.  Also number of lines ($=).
2539
2540 =head1 SEE ALSO
2541
2542 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2543 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2544 documentation.
2545
2546 =cut
2547
2548 1;
2549