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