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