bcae4d6469c83d8665eea364425f7e9554e5273c
[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 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
1286   my $cust_pay_batch = new FS::cust_pay_batch ( {
1287     'invnum'   => $self->getfield('invnum'),
1288     'custnum'  => $cust_main->getfield('custnum'),
1289     'last'     => $cust_main->getfield('last'),
1290     'first'    => $cust_main->getfield('first'),
1291     'address1' => $cust_main->getfield('address1'),
1292     'address2' => $cust_main->getfield('address2'),
1293     'city'     => $cust_main->getfield('city'),
1294     'state'    => $cust_main->getfield('state'),
1295     'zip'      => $cust_main->getfield('zip'),
1296     'country'  => $cust_main->getfield('country'),
1297     'cardnum'  => $cust_main->payinfo,
1298     'exp'      => $cust_main->getfield('paydate'),
1299     'payname'  => $cust_main->getfield('payname'),
1300     'amount'   => $self->owed,
1301   } );
1302   my $error = $cust_pay_batch->insert;
1303   die $error if $error;
1304
1305   '';
1306 }
1307
1308 sub _agent_template {
1309   my $self = shift;
1310   $self->_agent_plandata('agent_templatename');
1311 }
1312
1313 sub _agent_invoice_from {
1314   my $self = shift;
1315   $self->_agent_plandata('agent_invoice_from');
1316 }
1317
1318 sub _agent_plandata {
1319   my( $self, $option ) = @_;
1320
1321   my $part_bill_event = qsearchs( 'part_bill_event',
1322     {
1323       'payby'     => $self->cust_main->payby,
1324       'plan'      => 'send_agent',
1325       'plandata'  => { 'op'    => '~',
1326                        'value' => "(^|\n)agentnum ".
1327                                    '([0-9]*, )*'.
1328                                   $self->cust_main->agentnum.
1329                                    '(, [0-9]*)*'.
1330                                   "(\n|\$)",
1331                      },
1332     },
1333     '',
1334     'ORDER BY seconds LIMIT 1'
1335   );
1336
1337   return '' unless $part_bill_event;
1338
1339   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1340     return $1;
1341   } else {
1342     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1343          " plandata for $option";
1344     return '';
1345   }
1346
1347 }
1348
1349 =item print_text [ TIME [ , TEMPLATE ] ]
1350
1351 Returns an text invoice, as a list of lines.
1352
1353 TIME an optional value used to control the printing of overdue messages.  The
1354 default is now.  It isn't the date of the invoice; that's the `_date' field.
1355 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1356 L<Time::Local> and L<Date::Parse> for conversion functions.
1357
1358 =cut
1359
1360 #still some false laziness w/_items stuff (and send_csv)
1361 sub print_text {
1362
1363   my( $self, $today, $template ) = @_;
1364   $today ||= time;
1365
1366 #  my $invnum = $self->invnum;
1367   my $cust_main = $self->cust_main;
1368   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1369     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1370
1371   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1372 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1373   #my $balance_due = $self->owed + $pr_total - $cr_total;
1374   my $balance_due = $self->owed + $pr_total;
1375
1376   #my @collect = ();
1377   #my($description,$amount);
1378   @buf = ();
1379
1380   #previous balance
1381   foreach ( @pr_cust_bill ) {
1382     push @buf, [
1383       "Previous Balance, Invoice #". $_->invnum. 
1384                  " (". time2str("%x",$_->_date). ")",
1385       $money_char. sprintf("%10.2f",$_->owed)
1386     ];
1387   }
1388   if (@pr_cust_bill) {
1389     push @buf,['','-----------'];
1390     push @buf,[ 'Total Previous Balance',
1391                 $money_char. sprintf("%10.2f",$pr_total ) ];
1392     push @buf,['',''];
1393   }
1394
1395   #new charges
1396   foreach my $cust_bill_pkg (
1397     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1398     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1399   ) {
1400
1401     my $desc = $cust_bill_pkg->desc;
1402
1403     if ( $cust_bill_pkg->pkgnum > 0 ) {
1404
1405       if ( $cust_bill_pkg->setup != 0 ) {
1406         my $description = $desc;
1407         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1408         push @buf, [ $description,
1409                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1410         push @buf,
1411           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1412               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1413       }
1414
1415       if ( $cust_bill_pkg->recur != 0 ) {
1416         push @buf, [
1417           "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1418                       time2str("%x", $cust_bill_pkg->edate) . ")",
1419           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1420         ];
1421         push @buf,
1422           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1423               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1424                                                   $cust_bill_pkg->sdate );
1425       }
1426
1427       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1428
1429     } else { #pkgnum tax or one-shot line item
1430
1431       if ( $cust_bill_pkg->setup != 0 ) {
1432         push @buf, [ $desc,
1433                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1434       }
1435       if ( $cust_bill_pkg->recur != 0 ) {
1436         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1437                               . time2str("%x", $cust_bill_pkg->edate). ")",
1438                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1439                    ];
1440       }
1441
1442     }
1443
1444   }
1445
1446   push @buf,['','-----------'];
1447   push @buf,['Total New Charges',
1448              $money_char. sprintf("%10.2f",$self->charged) ];
1449   push @buf,['',''];
1450
1451   push @buf,['','-----------'];
1452   push @buf,['Total Charges',
1453              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1454   push @buf,['',''];
1455
1456   #credits
1457   foreach ( $self->cust_credited ) {
1458
1459     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1460
1461     my $reason = substr($_->cust_credit->reason,0,32);
1462     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1463     $reason = " ($reason) " if $reason;
1464     push @buf,[
1465       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1466         $reason,
1467       $money_char. sprintf("%10.2f",$_->amount)
1468     ];
1469   }
1470   #foreach ( @cr_cust_credit ) {
1471   #  push @buf,[
1472   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1473   #    $money_char. sprintf("%10.2f",$_->credited)
1474   #  ];
1475   #}
1476
1477   #get & print payments
1478   foreach ( $self->cust_bill_pay ) {
1479
1480     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1481
1482     push @buf,[
1483       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1484       $money_char. sprintf("%10.2f",$_->amount )
1485     ];
1486   }
1487
1488   #balance due
1489   my $balance_due_msg = $self->balance_due_msg;
1490
1491   push @buf,['','-----------'];
1492   push @buf,[$balance_due_msg, $money_char. 
1493     sprintf("%10.2f", $balance_due ) ];
1494
1495   #create the template
1496   $template ||= $self->_agent_template;
1497   my $templatefile = 'invoice_template';
1498   $templatefile .= "_$template" if length($template);
1499   my @invoice_template = $conf->config($templatefile)
1500     or die "cannot load config file $templatefile";
1501   $invoice_lines = 0;
1502   my $wasfunc = 0;
1503   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1504     /invoice_lines\((\d*)\)/;
1505     $invoice_lines += $1 || scalar(@buf);
1506     $wasfunc=1;
1507   }
1508   die "no invoice_lines() functions in template?" unless $wasfunc;
1509   my $invoice_template = new Text::Template (
1510     TYPE   => 'ARRAY',
1511     SOURCE => [ map "$_\n", @invoice_template ],
1512   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1513   $invoice_template->compile()
1514     or die "can't compile template: $Text::Template::ERROR";
1515
1516   #setup template variables
1517   package FS::cust_bill::_template; #!
1518   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1519
1520   $invnum = $self->invnum;
1521   $date = $self->_date;
1522   $page = 1;
1523   $agent = $self->cust_main->agent->agent;
1524
1525   if ( $FS::cust_bill::invoice_lines ) {
1526     $total_pages =
1527       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1528     $total_pages++
1529       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1530   } else {
1531     $total_pages = 1;
1532   }
1533
1534   #format address (variable for the template)
1535   my $l = 0;
1536   @address = ( '', '', '', '', '', '' );
1537   package FS::cust_bill; #!
1538   $FS::cust_bill::_template::address[$l++] =
1539     $cust_main->payname.
1540       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1541         ? " (P.O. #". $cust_main->payinfo. ")"
1542         : ''
1543       )
1544   ;
1545   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1546     if $cust_main->company;
1547   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1548   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1549     if $cust_main->address2;
1550   $FS::cust_bill::_template::address[$l++] =
1551     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1552
1553   my $countrydefault = $conf->config('countrydefault') || 'US';
1554   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1555     unless $cust_main->country eq $countrydefault;
1556
1557         #  #overdue? (variable for the template)
1558         #  $FS::cust_bill::_template::overdue = ( 
1559         #    $balance_due > 0
1560         #    && $today > $self->_date 
1561         ##    && $self->printed > 1
1562         #    && $self->printed > 0
1563         #  );
1564
1565   #and subroutine for the template
1566   sub FS::cust_bill::_template::invoice_lines {
1567     my $lines = shift || scalar(@buf);
1568     map { 
1569       scalar(@buf) ? shift @buf : [ '', '' ];
1570     }
1571     ( 1 .. $lines );
1572   }
1573
1574   #and fill it in
1575   $FS::cust_bill::_template::page = 1;
1576   my $lines;
1577   my @collect;
1578   while (@buf) {
1579     push @collect, split("\n",
1580       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1581     );
1582     $FS::cust_bill::_template::page++;
1583   }
1584
1585   map "$_\n", @collect;
1586
1587 }
1588
1589 =item print_latex [ TIME [ , TEMPLATE ] ]
1590
1591 Internal method - returns a filename of a filled-in LaTeX template for this
1592 invoice (Note: add ".tex" to get the actual filename).
1593
1594 See print_ps and print_pdf for methods that return PostScript and PDF output.
1595
1596 TIME an optional value used to control the printing of overdue messages.  The
1597 default is now.  It isn't the date of the invoice; that's the `_date' field.
1598 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1599 L<Time::Local> and L<Date::Parse> for conversion functions.
1600
1601 =cut
1602
1603 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1604 sub print_latex {
1605
1606   my( $self, $today, $template ) = @_;
1607   $today ||= time;
1608   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1609     if $DEBUG;
1610
1611   my $cust_main = $self->cust_main;
1612   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1613     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1614
1615   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1616 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1617   #my $balance_due = $self->owed + $pr_total - $cr_total;
1618   my $balance_due = $self->owed + $pr_total;
1619
1620   #create the template
1621   $template ||= $self->_agent_template;
1622   my $templatefile = 'invoice_latex';
1623   my $suffix = length($template) ? "_$template" : '';
1624   $templatefile .= $suffix;
1625   my @invoice_template = map "$_\n", $conf->config($templatefile)
1626     or die "cannot load config file $templatefile";
1627
1628   my($format, $text_template);
1629   if ( grep { /^%%Detail/ } @invoice_template ) {
1630     #change this to a die when the old code is removed
1631     warn "old-style invoice template $templatefile; ".
1632          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1633     $format = 'old';
1634   } else {
1635     $format = 'Text::Template';
1636     $text_template = new Text::Template(
1637       TYPE => 'ARRAY',
1638       SOURCE => \@invoice_template,
1639       DELIMITERS => [ '[@--', '--@]' ],
1640     );
1641
1642     $text_template->compile()
1643       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1644   }
1645
1646   my $returnaddress;
1647   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1648     $returnaddress = join("\n",
1649       $conf->config_orbase('invoice_latexreturnaddress', $template)
1650     );
1651   } else {
1652     $returnaddress = '~';
1653   }
1654
1655   my %invoice_data = (
1656     'invnum'       => $self->invnum,
1657     'date'         => time2str('%b %o, %Y', $self->_date),
1658     'today'        => time2str('%b %o, %Y', $today),
1659     'agent'        => _latex_escape($cust_main->agent->agent),
1660     'payname'      => _latex_escape($cust_main->payname),
1661     'company'      => _latex_escape($cust_main->company),
1662     'address1'     => _latex_escape($cust_main->address1),
1663     'address2'     => _latex_escape($cust_main->address2),
1664     'city'         => _latex_escape($cust_main->city),
1665     'state'        => _latex_escape($cust_main->state),
1666     'zip'          => _latex_escape($cust_main->zip),
1667     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1668     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1669     'returnaddress' => $returnaddress,
1670     'quantity'     => 1,
1671     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1672     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1673     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1674   );
1675
1676   my $countrydefault = $conf->config('countrydefault') || 'US';
1677   if ( $cust_main->country eq $countrydefault ) {
1678     $invoice_data{'country'} = '';
1679   } else {
1680     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1681   }
1682
1683   $invoice_data{'notes'} =
1684     join("\n",
1685 #  #do variable substitutions in notes
1686 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1687         $conf->config_orbase('invoice_latexnotes', $template)
1688     );
1689   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1690     if $DEBUG;
1691
1692   $invoice_data{'footer'} =~ s/\n+$//;
1693   $invoice_data{'smallfooter'} =~ s/\n+$//;
1694   $invoice_data{'notes'} =~ s/\n+$//;
1695
1696   $invoice_data{'po_line'} =
1697     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1698       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1699       : '~';
1700
1701   my @filled_in = ();
1702   if ( $format eq 'old' ) {
1703   
1704     my @line_item = ();
1705     my @total_item = ();
1706     while ( @invoice_template ) {
1707       my $line = shift @invoice_template;
1708   
1709       if ( $line =~ /^%%Detail\s*$/ ) {
1710   
1711         while ( ( my $line_item_line = shift @invoice_template )
1712                 !~ /^%%EndDetail\s*$/                            ) {
1713           push @line_item, $line_item_line;
1714         }
1715         foreach my $line_item ( $self->_items ) {
1716         #foreach my $line_item ( $self->_items_pkg ) {
1717           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1718           $invoice_data{'description'} =
1719             _latex_escape($line_item->{'description'});
1720           if ( exists $line_item->{'ext_description'} ) {
1721             $invoice_data{'description'} .=
1722               "\\tabularnewline\n~~".
1723               join( "\\tabularnewline\n~~",
1724                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1725                   );
1726           }
1727           $invoice_data{'amount'} = $line_item->{'amount'};
1728           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1729           push @filled_in,
1730             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1731         }
1732   
1733       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1734   
1735         while ( ( my $total_item_line = shift @invoice_template )
1736                 !~ /^%%EndTotalDetails\s*$/                      ) {
1737           push @total_item, $total_item_line;
1738         }
1739   
1740         my @total_fill = ();
1741   
1742         my $taxtotal = 0;
1743         foreach my $tax ( $self->_items_tax ) {
1744           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1745           $taxtotal += $tax->{'amount'};
1746           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1747           push @total_fill,
1748             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1749                 @total_item;
1750         }
1751
1752         if ( $taxtotal ) {
1753           $invoice_data{'total_item'} = 'Sub-total';
1754           $invoice_data{'total_amount'} =
1755             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1756           unshift @total_fill,
1757             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1758                 @total_item;
1759         }
1760   
1761         $invoice_data{'total_item'} = '\textbf{Total}';
1762         $invoice_data{'total_amount'} =
1763           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1764         push @total_fill,
1765           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1766               @total_item;
1767   
1768         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1769   
1770         # credits
1771         foreach my $credit ( $self->_items_credits ) {
1772           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1773           #$credittotal
1774           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1775           push @total_fill, 
1776             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1777                 @total_item;
1778         }
1779   
1780         # payments
1781         foreach my $payment ( $self->_items_payments ) {
1782           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1783           #$paymenttotal
1784           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1785           push @total_fill, 
1786             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1787                 @total_item;
1788         }
1789   
1790         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1791         $invoice_data{'total_amount'} =
1792           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1793         push @total_fill,
1794           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1795               @total_item;
1796   
1797         push @filled_in, @total_fill;
1798   
1799       } else {
1800         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1801         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1802         push @filled_in, $line;
1803       }
1804   
1805     }
1806
1807     sub nounder {
1808       my $var = $1;
1809       $var =~ s/_/\-/g;
1810       $var;
1811     }
1812
1813   } elsif ( $format eq 'Text::Template' ) {
1814
1815     my @detail_items = ();
1816     my @total_items = ();
1817
1818     $invoice_data{'detail_items'} = \@detail_items;
1819     $invoice_data{'total_items'} = \@total_items;
1820   
1821     foreach my $line_item ( $self->_items ) {
1822       my $detail = {
1823         ext_description => [],
1824       };
1825       $detail->{'ref'} = $line_item->{'pkgnum'};
1826       $detail->{'quantity'} = 1;
1827       $detail->{'description'} = _latex_escape($line_item->{'description'});
1828       if ( exists $line_item->{'ext_description'} ) {
1829         @{$detail->{'ext_description'}} = map {
1830           _latex_escape($_);
1831         } @{$line_item->{'ext_description'}};
1832       }
1833       $detail->{'amount'} = $line_item->{'amount'};
1834       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1835   
1836       push @detail_items, $detail;
1837     }
1838   
1839   
1840     my $taxtotal = 0;
1841     foreach my $tax ( $self->_items_tax ) {
1842       my $total = {};
1843       $total->{'total_item'} = _latex_escape($tax->{'description'});
1844       $taxtotal += $tax->{'amount'};
1845       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1846       push @total_items, $total;
1847     }
1848   
1849     if ( $taxtotal ) {
1850       my $total = {};
1851       $total->{'total_item'} = 'Sub-total';
1852       $total->{'total_amount'} =
1853         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1854       unshift @total_items, $total;
1855     }
1856   
1857     {
1858       my $total = {};
1859       $total->{'total_item'} = '\textbf{Total}';
1860       $total->{'total_amount'} =
1861         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1862       push @total_items, $total;
1863     }
1864   
1865     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1866   
1867     # credits
1868     foreach my $credit ( $self->_items_credits ) {
1869       my $total;
1870       $total->{'total_item'} = _latex_escape($credit->{'description'});
1871       #$credittotal
1872       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1873       push @total_items, $total;
1874     }
1875   
1876     # payments
1877     foreach my $payment ( $self->_items_payments ) {
1878       my $total = {};
1879       $total->{'total_item'} = _latex_escape($payment->{'description'});
1880       #$paymenttotal
1881       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1882       push @total_items, $total;
1883     }
1884   
1885     { 
1886       my $total;
1887       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1888       $total->{'total_amount'} =
1889         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1890       push @total_items, $total;
1891     }
1892
1893   } else {
1894     die "guru meditation #54";
1895   }
1896
1897   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1898   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1899                            DIR      => $dir,
1900                            SUFFIX   => '.tex',
1901                            UNLINK   => 0,
1902                          ) or die "can't open temp file: $!\n";
1903   if ( $format eq 'old' ) {
1904     print $fh join('', @filled_in );
1905   } elsif ( $format eq 'Text::Template' ) {
1906     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1907   } else {
1908     die "guru meditation #32";
1909   }
1910   close $fh;
1911
1912   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1913   return $1;
1914
1915 }
1916
1917 =item print_ps [ TIME [ , TEMPLATE ] ]
1918
1919 Returns an postscript invoice, as a scalar.
1920
1921 TIME an optional value used to control the printing of overdue messages.  The
1922 default is now.  It isn't the date of the invoice; that's the `_date' field.
1923 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1924 L<Time::Local> and L<Date::Parse> for conversion functions.
1925
1926 =cut
1927
1928 sub print_ps {
1929   my $self = shift;
1930
1931   my $file = $self->print_latex(@_);
1932
1933   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1934   chdir($dir);
1935
1936   my $sfile = shell_quote $file;
1937
1938   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1939     or die "pslatex $file.tex failed; see $file.log for details?\n";
1940   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1941     or die "pslatex $file.tex failed; see $file.log for details?\n";
1942
1943   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1944     or die "dvips failed";
1945
1946   open(POSTSCRIPT, "<$file.ps")
1947     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1948
1949   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1950
1951   my $ps = '';
1952   while (<POSTSCRIPT>) {
1953     $ps .= $_;
1954   }
1955
1956   close POSTSCRIPT;
1957
1958   return $ps;
1959
1960 }
1961
1962 =item print_pdf [ TIME [ , TEMPLATE ] ]
1963
1964 Returns an PDF invoice, as a scalar.
1965
1966 TIME an optional value used to control the printing of overdue messages.  The
1967 default is now.  It isn't the date of the invoice; that's the `_date' field.
1968 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1969 L<Time::Local> and L<Date::Parse> for conversion functions.
1970
1971 =cut
1972
1973 sub print_pdf {
1974   my $self = shift;
1975
1976   my $file = $self->print_latex(@_);
1977
1978   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1979   chdir($dir);
1980
1981   #system('pdflatex', "$file.tex");
1982   #system('pdflatex', "$file.tex");
1983   #! LaTeX Error: Unknown graphics extension: .eps.
1984
1985   my $sfile = shell_quote $file;
1986
1987   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1988     or die "pslatex $file.tex failed; see $file.log for details?\n";
1989   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1990     or die "pslatex $file.tex failed; see $file.log for details?\n";
1991
1992   #system('dvipdf', "$file.dvi", "$file.pdf" );
1993   system(
1994     "dvips -q -t letter -f $sfile.dvi ".
1995     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1996     "     -c save pop -"
1997   ) == 0
1998     or die "dvips | gs failed: $!";
1999
2000   open(PDF, "<$file.pdf")
2001     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2002
2003   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2004
2005   my $pdf = '';
2006   while (<PDF>) {
2007     $pdf .= $_;
2008   }
2009
2010   close PDF;
2011
2012   return $pdf;
2013
2014 }
2015
2016 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2017
2018 Returns an HTML invoice, as a scalar.
2019
2020 TIME an optional value used to control the printing of overdue messages.  The
2021 default is now.  It isn't the date of the invoice; that's the `_date' field.
2022 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2023 L<Time::Local> and L<Date::Parse> for conversion functions.
2024
2025 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2026 when emailing the invoice as part of a multipart/related MIME email.
2027
2028 =cut
2029
2030 #some falze laziness w/print_text and print_latex (and send_csv)
2031 sub print_html {
2032   my( $self, $today, $template, $cid ) = @_;
2033   $today ||= time;
2034
2035   my $cust_main = $self->cust_main;
2036   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2037     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2038
2039   $template ||= $self->_agent_template;
2040   my $templatefile = 'invoice_html';
2041   my $suffix = length($template) ? "_$template" : '';
2042   $templatefile .= $suffix;
2043   my @html_template = map "$_\n", $conf->config($templatefile)
2044     or die "cannot load config file $templatefile";
2045
2046   my $html_template = new Text::Template(
2047     TYPE   => 'ARRAY',
2048     SOURCE => \@html_template,
2049     DELIMITERS => [ '<%=', '%>' ],
2050   );
2051
2052   $html_template->compile()
2053     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2054
2055   my %invoice_data = (
2056     'invnum'       => $self->invnum,
2057     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2058     'today'        => time2str('%b %o, %Y', $today),
2059     'agent'        => encode_entities($cust_main->agent->agent),
2060     'payname'      => encode_entities($cust_main->payname),
2061     'company'      => encode_entities($cust_main->company),
2062     'address1'     => encode_entities($cust_main->address1),
2063     'address2'     => encode_entities($cust_main->address2),
2064     'city'         => encode_entities($cust_main->city),
2065     'state'        => encode_entities($cust_main->state),
2066     'zip'          => encode_entities($cust_main->zip),
2067     'terms'        => $conf->config('invoice_default_terms')
2068                       || 'Payable upon receipt',
2069     'cid'          => $cid,
2070     'template'     => $template,
2071 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2072   );
2073
2074   if (
2075          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2076       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2077   ) {
2078     $invoice_data{'returnaddress'} =
2079       join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2080   } else {
2081     $invoice_data{'returnaddress'} =
2082       join("\n", map { 
2083                        s/~/&nbsp;/g;
2084                        s/\\\\\*?\s*$/<BR>/;
2085                        s/\\hyphenation\{[\w\s\-]+\}//;
2086                        $_;
2087                      }
2088                      $conf->config_orbase( 'invoice_latexreturnaddress',
2089                                            $template
2090                                          )
2091           );
2092   }
2093
2094   my $countrydefault = $conf->config('countrydefault') || 'US';
2095   if ( $cust_main->country eq $countrydefault ) {
2096     $invoice_data{'country'} = '';
2097   } else {
2098     $invoice_data{'country'} =
2099       encode_entities(code2country($cust_main->country));
2100   }
2101
2102   if (
2103          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2104       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2105   ) {
2106     $invoice_data{'notes'} =
2107       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2108   } else {
2109     $invoice_data{'notes'} = 
2110       join("\n", map { 
2111                        s/%%(.*)$/<!-- $1 -->/;
2112                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2113                        s/\\begin\{enumerate\}/<ol>/;
2114                        s/\\item /  <li>/;
2115                        s/\\end\{enumerate\}/<\/ol>/;
2116                        s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2117                        $_;
2118                      } 
2119                      $conf->config_orbase('invoice_latexnotes', $template)
2120           );
2121   }
2122
2123 #  #do variable substitutions in notes
2124 #  $invoice_data{'notes'} =
2125 #    join("\n",
2126 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2127 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2128 #    );
2129
2130   if (
2131          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2132       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2133   ) {
2134    $invoice_data{'footer'} =
2135      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2136   } else {
2137    $invoice_data{'footer'} =
2138        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2139                       $conf->config_orbase('invoice_latexfooter', $template)
2140            );
2141   }
2142
2143   $invoice_data{'po_line'} =
2144     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2145       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2146       : '';
2147
2148   my $money_char = $conf->config('money_char') || '$';
2149
2150   foreach my $line_item ( $self->_items ) {
2151     my $detail = {
2152       ext_description => [],
2153     };
2154     $detail->{'ref'} = $line_item->{'pkgnum'};
2155     $detail->{'description'} = encode_entities($line_item->{'description'});
2156     if ( exists $line_item->{'ext_description'} ) {
2157       @{$detail->{'ext_description'}} = map {
2158         encode_entities($_);
2159       } @{$line_item->{'ext_description'}};
2160     }
2161     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2162     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2163
2164     push @{$invoice_data{'detail_items'}}, $detail;
2165   }
2166
2167
2168   my $taxtotal = 0;
2169   foreach my $tax ( $self->_items_tax ) {
2170     my $total = {};
2171     $total->{'total_item'} = encode_entities($tax->{'description'});
2172     $taxtotal += $tax->{'amount'};
2173     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2174     push @{$invoice_data{'total_items'}}, $total;
2175   }
2176
2177   if ( $taxtotal ) {
2178     my $total = {};
2179     $total->{'total_item'} = 'Sub-total';
2180     $total->{'total_amount'} =
2181       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2182     unshift @{$invoice_data{'total_items'}}, $total;
2183   }
2184
2185   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2186   {
2187     my $total = {};
2188     $total->{'total_item'} = '<b>Total</b>';
2189     $total->{'total_amount'} =
2190       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2191     push @{$invoice_data{'total_items'}}, $total;
2192   }
2193
2194   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2195
2196   # credits
2197   foreach my $credit ( $self->_items_credits ) {
2198     my $total;
2199     $total->{'total_item'} = encode_entities($credit->{'description'});
2200     #$credittotal
2201     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2202     push @{$invoice_data{'total_items'}}, $total;
2203   }
2204
2205   # payments
2206   foreach my $payment ( $self->_items_payments ) {
2207     my $total = {};
2208     $total->{'total_item'} = encode_entities($payment->{'description'});
2209     #$paymenttotal
2210     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2211     push @{$invoice_data{'total_items'}}, $total;
2212   }
2213
2214   { 
2215     my $total;
2216     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2217     $total->{'total_amount'} =
2218       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2219     push @{$invoice_data{'total_items'}}, $total;
2220   }
2221
2222   $html_template->fill_in( HASH => \%invoice_data);
2223 }
2224
2225 # quick subroutine for print_latex
2226 #
2227 # There are ten characters that LaTeX treats as special characters, which
2228 # means that they do not simply typeset themselves: 
2229 #      # $ % & ~ _ ^ \ { }
2230 #
2231 # TeX ignores blanks following an escaped character; if you want a blank (as
2232 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2233
2234 sub _latex_escape {
2235   my $value = shift;
2236   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2237   $value =~ s/([<>])/\$$1\$/g;
2238   $value;
2239 }
2240
2241 #utility methods for print_*
2242
2243 sub balance_due_msg {
2244   my $self = shift;
2245   my $msg = 'Balance Due';
2246   return $msg unless $conf->exists('invoice_default_terms');
2247   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2248     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2249   } elsif ( $conf->config('invoice_default_terms') ) {
2250     $msg .= ' - '. $conf->config('invoice_default_terms');
2251   }
2252   $msg;
2253 }
2254
2255 sub _items {
2256   my $self = shift;
2257   my @display = scalar(@_)
2258                 ? @_
2259                 : qw( _items_previous _items_pkg );
2260                 #: qw( _items_pkg );
2261                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2262   my @b = ();
2263   foreach my $display ( @display ) {
2264     push @b, $self->$display(@_);
2265   }
2266   @b;
2267 }
2268
2269 sub _items_previous {
2270   my $self = shift;
2271   my $cust_main = $self->cust_main;
2272   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2273   my @b = ();
2274   foreach ( @pr_cust_bill ) {
2275     push @b, {
2276       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2277                        ' ('. time2str('%x',$_->_date). ')',
2278       #'pkgpart'     => 'N/A',
2279       'pkgnum'      => 'N/A',
2280       'amount'      => sprintf("%.2f", $_->owed),
2281     };
2282   }
2283   @b;
2284
2285   #{
2286   #    'description'     => 'Previous Balance',
2287   #    #'pkgpart'         => 'N/A',
2288   #    'pkgnum'          => 'N/A',
2289   #    'amount'          => sprintf("%10.2f", $pr_total ),
2290   #    'ext_description' => [ map {
2291   #                                 "Invoice ". $_->invnum.
2292   #                                 " (". time2str("%x",$_->_date). ") ".
2293   #                                 sprintf("%10.2f", $_->owed)
2294   #                         } @pr_cust_bill ],
2295
2296   #};
2297 }
2298
2299 sub _items_pkg {
2300   my $self = shift;
2301   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2302   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2303 }
2304
2305 sub _items_tax {
2306   my $self = shift;
2307   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2308   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2309 }
2310
2311 sub _items_cust_bill_pkg {
2312   my $self = shift;
2313   my $cust_bill_pkg = shift;
2314
2315   my @b = ();
2316   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2317
2318     my $desc = $cust_bill_pkg->desc;
2319
2320     if ( $cust_bill_pkg->pkgnum > 0 ) {
2321
2322       if ( $cust_bill_pkg->setup != 0 ) {
2323         my $description = $desc;
2324         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2325         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2326         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2327         push @b, {
2328           description     => $description,
2329           #pkgpart         => $part_pkg->pkgpart,
2330           pkgnum          => $cust_bill_pkg->pkgnum,
2331           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2332           ext_description => \@d,
2333         };
2334       }
2335
2336       if ( $cust_bill_pkg->recur != 0 ) {
2337         push @b, {
2338           description     => "$desc (" .
2339                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2340                                time2str('%x', $cust_bill_pkg->edate). ')',
2341           #pkgpart         => $part_pkg->pkgpart,
2342           pkgnum          => $cust_bill_pkg->pkgnum,
2343           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2344           ext_description =>
2345             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2346                                                         $cust_bill_pkg->sdate),
2347               $cust_bill_pkg->details,
2348             ],
2349         };
2350       }
2351
2352     } else { #pkgnum tax or one-shot line item (??)
2353
2354       if ( $cust_bill_pkg->setup != 0 ) {
2355         push @b, {
2356           'description' => $desc,
2357           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2358         };
2359       }
2360       if ( $cust_bill_pkg->recur != 0 ) {
2361         push @b, {
2362           'description' => "$desc (".
2363                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2364                            time2str("%x", $cust_bill_pkg->edate). ')',
2365           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2366         };
2367       }
2368
2369     }
2370
2371   }
2372
2373   @b;
2374
2375 }
2376
2377 sub _items_credits {
2378   my $self = shift;
2379
2380   my @b;
2381   #credits
2382   foreach ( $self->cust_credited ) {
2383
2384     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2385
2386     my $reason = $_->cust_credit->reason;
2387     #my $reason = substr($_->cust_credit->reason,0,32);
2388     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2389     $reason = " ($reason) " if $reason;
2390     push @b, {
2391       #'description' => 'Credit ref\#'. $_->crednum.
2392       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2393       #                 $reason,
2394       'description' => 'Credit applied '.
2395                        time2str("%x",$_->cust_credit->_date). $reason,
2396       'amount'      => sprintf("%.2f",$_->amount),
2397     };
2398   }
2399   #foreach ( @cr_cust_credit ) {
2400   #  push @buf,[
2401   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2402   #    $money_char. sprintf("%10.2f",$_->credited)
2403   #  ];
2404   #}
2405
2406   @b;
2407
2408 }
2409
2410 sub _items_payments {
2411   my $self = shift;
2412
2413   my @b;
2414   #get & print payments
2415   foreach ( $self->cust_bill_pay ) {
2416
2417     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2418
2419     push @b, {
2420       'description' => "Payment received ".
2421                        time2str("%x",$_->cust_pay->_date ),
2422       'amount'      => sprintf("%.2f", $_->amount )
2423     };
2424   }
2425
2426   @b;
2427
2428 }
2429
2430
2431 =back
2432
2433 =head1 SUBROUTINES
2434
2435 =over 4
2436
2437 =item reprint
2438
2439 =cut
2440
2441 sub process_reprint {
2442   process_re_X('print', @_);
2443 }
2444
2445 =item reemail
2446
2447 =cut
2448
2449 sub process_reemail {
2450   process_re_X('email', @_);
2451 }
2452
2453 =item refax
2454
2455 =cut
2456
2457 sub process_refax {
2458   process_re_X('fax', @_);
2459 }
2460
2461 use Storable qw(thaw);
2462 use Data::Dumper;
2463 use MIME::Base64;
2464 sub process_re_X {
2465   my( $method, $job ) = ( shift, shift );
2466   warn "process_re_X $method for job $job\n" if $DEBUG;
2467
2468   my $param = thaw(decode_base64(shift));
2469   warn Dumper($param) if $DEBUG;
2470
2471   re_X(
2472     $method,
2473     $job,
2474     %$param,
2475   );
2476
2477 }
2478
2479 sub re_X {
2480   my($method, $job, %param ) = @_;
2481 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2482   if ( $DEBUG ) {
2483     warn "re_X $method for job $job with param:\n".
2484          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2485   }
2486
2487   #some false laziness w/search/cust_bill.html
2488   my $distinct = '';
2489   my $orderby = 'ORDER BY cust_bill._date';
2490
2491   my @where;
2492
2493   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2494     push @where, "cust_bill._date >= $1";
2495   }
2496   if ( $param{'end'} =~ /^(\d+)$/ ) {
2497     push @where, "cust_bill._date < $1";
2498   }
2499   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2500     push @where, "cust_main.agentnum = $1";
2501   }
2502
2503   my $owed =
2504     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2505                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2506              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2507                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2508
2509   push @where, "0 != $owed"
2510     if $param{'open'};
2511
2512   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2513     if $param{'days'};
2514
2515   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2516
2517   my $addl_from = 'left join cust_main using ( custnum )';
2518
2519   if ( $param{'newest_percust'} ) {
2520     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2521     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2522     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2523   }
2524      
2525   my @cust_bill = qsearch( 'cust_bill',
2526                            {},
2527                            "$distinct cust_bill.*",
2528                            $extra_sql,
2529                            '',
2530                            $addl_from
2531                          );
2532
2533   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2534   foreach my $cust_bill ( @cust_bill ) {
2535     $cust_bill->$method();
2536
2537     if ( $job ) { #progressbar foo
2538       $num++;
2539       if ( time - $min_sec > $last ) {
2540         my $error = $job->update_statustext(
2541           int( 100 * $num / scalar(@cust_bill) )
2542         );
2543         die $error if $error;
2544         $last = time;
2545       }
2546     }
2547
2548   }
2549
2550 }
2551
2552 =back
2553
2554 =head1 BUGS
2555
2556 The delete method.
2557
2558 print_text formatting (and some logic :/) is in source, but needs to be
2559 slurped in from a file.  Also number of lines ($=).
2560
2561 =head1 SEE ALSO
2562
2563 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2564 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2565 documentation.
2566
2567 =cut
2568
2569 1;
2570