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