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