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