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