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