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