add billco format option to FTP invoice send, add invoice event to spool one giant...
[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 format - 'default' or 'billco'
826
827 =cut
828
829 sub spool_csv {
830   my($self, %opt) = @_;
831
832   #create file(s)
833
834   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
835   mkdir $spooldir, 0700 unless -d $spooldir;
836
837   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
838   my $file = "$spooldir/spool";
839   if ( lc($opt{'format'}) eq 'billco' ) {
840     $file .= '-header.csv';
841   } else {
842     #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
843     $file .= '.csv';
844   }
845   
846   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
847
848   open(CSV, ">>$file") or die "can't open $file: $!";
849   flock(CSV, LOCK_EX);
850   seek(CSV, 0, 2);
851
852   print CSV $header;
853
854   my $oldfile = '';
855   if ( lc($opt{'format'}) eq 'billco' ) {
856
857     flock(CSV, LOCK_UN);
858     close CSV;
859
860     $oldfile = $file;
861     $file = "$spooldir/spool-detail.csv";
862
863     open(CSV,">>$file") or die "can't open $file: $!";
864     flock(CSV, LOCK_EX);
865     seek(CSV, 0, 2);
866   }
867
868   print CSV $detail;
869
870   flock(CSV, LOCK_UN);
871   close CSV;
872
873 }
874
875 =item print_csv OPTION => VALUE, ...
876
877 Returns CSV data for this invoice.
878
879 Options are:
880
881 format - 'default' or 'billco'
882
883 Returns a list consisting of two scalars.  The first is a single line of CSV
884 header information for this invoice.  The second is one or more lines of CSV
885 detail information for this invoice.
886
887 If I<format> is not specified or "default", the fields of the CSV file are as
888 follows:
889
890 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
891
892 =over 4
893
894 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
895
896 B<record_type> is C<cust_bill> for the initial header line only.  The
897 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
898 fields are filled in.
899
900 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
901 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
902 are filled in.
903
904 =item invnum - invoice number
905
906 =item custnum - customer number
907
908 =item _date - invoice date
909
910 =item charged - total invoice amount
911
912 =item first - customer first name
913
914 =item last - customer first name
915
916 =item company - company name
917
918 =item address1 - address line 1
919
920 =item address2 - address line 1
921
922 =item city
923
924 =item state
925
926 =item zip
927
928 =item country
929
930 =item pkg - line item description
931
932 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
933
934 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
935
936 =item sdate - start date for recurring fee
937
938 =item edate - end date for recurring fee
939
940 =back
941
942 If I<format> is "billco", the fields of the header CSV file are as follows:
943
944   +-------------------------------------------------------------------+
945   |                        FORMAT HEADER FILE                         |
946   |-------------------------------------------------------------------|
947   | Field | Description                   | Name       | Type | Width |
948   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
949   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
950   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
951   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
952   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
953   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
954   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
955   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
956   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
957   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
958   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
959   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
960   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
961   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
962   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
963   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
964   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
965   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
966   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
967   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
968   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
969   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
970   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
971   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
972   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
973   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
974   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
975   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
976   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
977   +-------+-------------------------------+------------+------+-------+
978
979 If I<format> is "billco", the fields of the detail CSV file are as follows:
980
981                                   FORMAT FOR DETAIL FILE
982         |                            |           |      |
983   Field | Description                | Name      | Type | Width
984   1     | N/A-Leave Empty            | RC        | CHAR |     2
985   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
986   3     | Account Number             | TRACCTNUM | CHAR |    15
987   4     | Invoice Number             | TRINVOICE | CHAR |    15
988   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
989   6     | Transaction Detail         | DETAILS   | CHAR |   100
990   7     | Amount                     | AMT       | NUM* |     9
991   8     | Line Format Control**      | LNCTRL    | CHAR |     2
992   9     | Grouping Code              | GROUP     | CHAR |     2
993   10    | User Defined               | ACCT CODE | CHAR |    15
994
995 =cut
996
997 sub print_csv {
998   my($self, %opt) = @_;
999   
1000   eval "use Text::CSV_XS";
1001   die $@ if $@;
1002
1003   my $cust_main = $self->cust_main;
1004
1005   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1006
1007   if ( lc($opt{'format'}) eq 'billco' ) {
1008
1009     my $taxtotal = 0;
1010     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1011
1012     my $duedate = '';
1013     if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1014       $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1015     }
1016
1017     my( $previous_balance, @unused ) = $self->previous; #previous balance
1018
1019     my $pmt_cr_applied = 0;
1020     $pmt_cr_applied += $_->{'amount'}
1021       foreach ( $self->_items_payments, $self->_items_credits ) ;
1022
1023     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1024
1025     $csv->combine(
1026       '',                         #  1 | N/A-Leave Empty               CHAR   2
1027       '',                         #  2 | N/A-Leave Empty               CHAR  15
1028       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1029       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1030       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1031       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1032       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1033       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1034       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1035       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1036       '',                         # 10 | Ancillary Billing Information CHAR  30
1037       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1038       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1039
1040       # XXX ?
1041       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1042
1043       # XXX ?
1044       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1045
1046       $previous_balance,          # 15 | Previous Balance              NUM*   9
1047       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1048       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1049       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1050       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1051       '',                         # 20 | 30 Day Aging                  NUM*   9
1052       '',                         # 21 | 60 Day Aging                  NUM*   9
1053       '',                         # 22 | 90 Day Aging                  NUM*   9
1054       'N',                        # 23 | Y/N                           CHAR   1
1055       '',                         # 24 | Remittance automation         CHAR 100
1056       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1057       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1058       '0',                        # 27 | Federal Tax***                NUM*   9
1059       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1060       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1061     );
1062
1063   } else {
1064   
1065     $csv->combine(
1066       'cust_bill',
1067       $self->invnum,
1068       $self->custnum,
1069       time2str("%x", $self->_date),
1070       sprintf("%.2f", $self->charged),
1071       ( map { $cust_main->getfield($_) }
1072           qw( first last company address1 address2 city state zip country ) ),
1073       map { '' } (1..5),
1074     ) or die "can't create csv";
1075   }
1076
1077   my $header = $csv->string. "\n";
1078
1079   my $detail = '';
1080   if ( lc($opt{'format'}) eq 'billco' ) {
1081
1082     my $lineseq = 0;
1083     foreach my $item ( $self->_items_pkg ) {
1084
1085       $csv->combine(
1086         '',                     #  1 | N/A-Leave Empty            CHAR   2
1087         '',                     #  2 | N/A-Leave Empty            CHAR  15
1088         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1089         $self->invnum,          #  4 | Invoice Number             CHAR  15
1090         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1091         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1092         $item->{'amount'},      #  7 | Amount                     NUM*   9
1093         '',                     #  8 | Line Format Control**      CHAR   2
1094         '',                     #  9 | Grouping Code              CHAR   2
1095         '',                     # 10 | User Defined               CHAR  15
1096       );
1097
1098       $detail .= $csv->string. "\n";
1099
1100     }
1101
1102   } else {
1103
1104     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1105
1106       my($pkg, $setup, $recur, $sdate, $edate);
1107       if ( $cust_bill_pkg->pkgnum ) {
1108       
1109         ($pkg, $setup, $recur, $sdate, $edate) = (
1110           $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1111           ( $cust_bill_pkg->setup != 0
1112             ? sprintf("%.2f", $cust_bill_pkg->setup )
1113             : '' ),
1114           ( $cust_bill_pkg->recur != 0
1115             ? sprintf("%.2f", $cust_bill_pkg->recur )
1116             : '' ),
1117           ( $cust_bill_pkg->sdate 
1118             ? time2str("%x", $cust_bill_pkg->sdate)
1119             : '' ),
1120           ($cust_bill_pkg->edate 
1121             ?time2str("%x", $cust_bill_pkg->edate)
1122             : '' ),
1123         );
1124   
1125       } else { #pkgnum tax
1126         next unless $cust_bill_pkg->setup != 0;
1127         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1128                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1129                          : 'Tax';
1130         ($pkg, $setup, $recur, $sdate, $edate) =
1131           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1132       }
1133   
1134       $csv->combine(
1135         'cust_bill_pkg',
1136         $self->invnum,
1137         ( map { '' } (1..11) ),
1138         ($pkg, $setup, $recur, $sdate, $edate)
1139       ) or die "can't create csv";
1140
1141       $detail .= $csv->string. "\n";
1142
1143     }
1144
1145   }
1146
1147   ( $header, $detail );
1148
1149 }
1150
1151 =item comp
1152
1153 Pays this invoice with a compliemntary payment.  If there is an error,
1154 returns the error, otherwise returns false.
1155
1156 =cut
1157
1158 sub comp {
1159   my $self = shift;
1160   my $cust_pay = new FS::cust_pay ( {
1161     'invnum'   => $self->invnum,
1162     'paid'     => $self->owed,
1163     '_date'    => '',
1164     'payby'    => 'COMP',
1165     'payinfo'  => $self->cust_main->payinfo,
1166     'paybatch' => '',
1167   } );
1168   $cust_pay->insert;
1169 }
1170
1171 =item realtime_card
1172
1173 Attempts to pay this invoice with a credit card payment via a
1174 Business::OnlinePayment realtime gateway.  See
1175 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1176 for supported processors.
1177
1178 =cut
1179
1180 sub realtime_card {
1181   my $self = shift;
1182   $self->realtime_bop( 'CC', @_ );
1183 }
1184
1185 =item realtime_ach
1186
1187 Attempts to pay this invoice with an electronic check (ACH) payment via a
1188 Business::OnlinePayment realtime gateway.  See
1189 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1190 for supported processors.
1191
1192 =cut
1193
1194 sub realtime_ach {
1195   my $self = shift;
1196   $self->realtime_bop( 'ECHECK', @_ );
1197 }
1198
1199 =item realtime_lec
1200
1201 Attempts to pay this invoice with phone bill (LEC) payment via a
1202 Business::OnlinePayment realtime gateway.  See
1203 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1204 for supported processors.
1205
1206 =cut
1207
1208 sub realtime_lec {
1209   my $self = shift;
1210   $self->realtime_bop( 'LEC', @_ );
1211 }
1212
1213 sub realtime_bop {
1214   my( $self, $method ) = @_;
1215
1216   my $cust_main = $self->cust_main;
1217   my $balance = $cust_main->balance;
1218   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1219   $amount = sprintf("%.2f", $amount);
1220   return "not run (balance $balance)" unless $amount > 0;
1221
1222   my $description = 'Internet Services';
1223   if ( $conf->exists('business-onlinepayment-description') ) {
1224     my $dtempl = $conf->config('business-onlinepayment-description');
1225
1226     my $agent_obj = $cust_main->agent
1227       or die "can't retreive agent for $cust_main (agentnum ".
1228              $cust_main->agentnum. ")";
1229     my $agent = $agent_obj->agent;
1230     my $pkgs = join(', ',
1231       map { $_->cust_pkg->part_pkg->pkg }
1232         grep { $_->pkgnum } $self->cust_bill_pkg
1233     );
1234     $description = eval qq("$dtempl");
1235   }
1236
1237   $cust_main->realtime_bop($method, $amount,
1238     'description' => $description,
1239     'invnum'      => $self->invnum,
1240   );
1241
1242 }
1243
1244 =item batch_card
1245
1246 Adds a payment for this invoice to the pending credit card batch (see
1247 L<FS::cust_pay_batch>).
1248
1249 =cut
1250
1251 sub batch_card {
1252   my $self = shift;
1253   my $cust_main = $self->cust_main;
1254
1255   my $cust_pay_batch = new FS::cust_pay_batch ( {
1256     'invnum'   => $self->getfield('invnum'),
1257     'custnum'  => $cust_main->getfield('custnum'),
1258     'last'     => $cust_main->getfield('last'),
1259     'first'    => $cust_main->getfield('first'),
1260     'address1' => $cust_main->getfield('address1'),
1261     'address2' => $cust_main->getfield('address2'),
1262     'city'     => $cust_main->getfield('city'),
1263     'state'    => $cust_main->getfield('state'),
1264     'zip'      => $cust_main->getfield('zip'),
1265     'country'  => $cust_main->getfield('country'),
1266     'cardnum'  => $cust_main->payinfo,
1267     'exp'      => $cust_main->getfield('paydate'),
1268     'payname'  => $cust_main->getfield('payname'),
1269     'amount'   => $self->owed,
1270   } );
1271   my $error = $cust_pay_batch->insert;
1272   die $error if $error;
1273
1274   '';
1275 }
1276
1277 sub _agent_template {
1278   my $self = shift;
1279   $self->_agent_plandata('agent_templatename');
1280 }
1281
1282 sub _agent_invoice_from {
1283   my $self = shift;
1284   $self->_agent_plandata('agent_invoice_from');
1285 }
1286
1287 sub _agent_plandata {
1288   my( $self, $option ) = @_;
1289
1290   my $part_bill_event = qsearchs( 'part_bill_event',
1291     {
1292       'payby'     => $self->cust_main->payby,
1293       'plan'      => 'send_agent',
1294       'plandata'  => { 'op'    => '~',
1295                        'value' => "(^|\n)agentnum ".
1296                                    '([0-9]*, )*'.
1297                                   $self->cust_main->agentnum.
1298                                    '(, [0-9]*)*'.
1299                                   "(\n|\$)",
1300                      },
1301     },
1302     '',
1303     'ORDER BY seconds LIMIT 1'
1304   );
1305
1306   return '' unless $part_bill_event;
1307
1308   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1309     return $1;
1310   } else {
1311     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1312          " plandata for $option";
1313     return '';
1314   }
1315
1316 }
1317
1318 =item print_text [ TIME [ , TEMPLATE ] ]
1319
1320 Returns an text invoice, as a list of lines.
1321
1322 TIME an optional value used to control the printing of overdue messages.  The
1323 default is now.  It isn't the date of the invoice; that's the `_date' field.
1324 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1325 L<Time::Local> and L<Date::Parse> for conversion functions.
1326
1327 =cut
1328
1329 #still some false laziness w/_items stuff (and send_csv)
1330 sub print_text {
1331
1332   my( $self, $today, $template ) = @_;
1333   $today ||= time;
1334
1335 #  my $invnum = $self->invnum;
1336   my $cust_main = $self->cust_main;
1337   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1338     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1339
1340   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1341 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1342   #my $balance_due = $self->owed + $pr_total - $cr_total;
1343   my $balance_due = $self->owed + $pr_total;
1344
1345   #my @collect = ();
1346   #my($description,$amount);
1347   @buf = ();
1348
1349   #previous balance
1350   foreach ( @pr_cust_bill ) {
1351     push @buf, [
1352       "Previous Balance, Invoice #". $_->invnum. 
1353                  " (". time2str("%x",$_->_date). ")",
1354       $money_char. sprintf("%10.2f",$_->owed)
1355     ];
1356   }
1357   if (@pr_cust_bill) {
1358     push @buf,['','-----------'];
1359     push @buf,[ 'Total Previous Balance',
1360                 $money_char. sprintf("%10.2f",$pr_total ) ];
1361     push @buf,['',''];
1362   }
1363
1364   #new charges
1365   foreach my $cust_bill_pkg (
1366     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1367     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1368   ) {
1369
1370     my $desc = $cust_bill_pkg->desc;
1371
1372     if ( $cust_bill_pkg->pkgnum > 0 ) {
1373
1374       if ( $cust_bill_pkg->setup != 0 ) {
1375         my $description = $desc;
1376         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1377         push @buf, [ $description,
1378                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1379         push @buf,
1380           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1381               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1382       }
1383
1384       if ( $cust_bill_pkg->recur != 0 ) {
1385         push @buf, [
1386           "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1387                       time2str("%x", $cust_bill_pkg->edate) . ")",
1388           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1389         ];
1390         push @buf,
1391           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1392               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1393                                                   $cust_bill_pkg->sdate );
1394       }
1395
1396       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1397
1398     } else { #pkgnum tax or one-shot line item
1399
1400       if ( $cust_bill_pkg->setup != 0 ) {
1401         push @buf, [ $desc,
1402                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1403       }
1404       if ( $cust_bill_pkg->recur != 0 ) {
1405         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1406                               . time2str("%x", $cust_bill_pkg->edate). ")",
1407                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1408                    ];
1409       }
1410
1411     }
1412
1413   }
1414
1415   push @buf,['','-----------'];
1416   push @buf,['Total New Charges',
1417              $money_char. sprintf("%10.2f",$self->charged) ];
1418   push @buf,['',''];
1419
1420   push @buf,['','-----------'];
1421   push @buf,['Total Charges',
1422              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1423   push @buf,['',''];
1424
1425   #credits
1426   foreach ( $self->cust_credited ) {
1427
1428     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1429
1430     my $reason = substr($_->cust_credit->reason,0,32);
1431     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1432     $reason = " ($reason) " if $reason;
1433     push @buf,[
1434       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1435         $reason,
1436       $money_char. sprintf("%10.2f",$_->amount)
1437     ];
1438   }
1439   #foreach ( @cr_cust_credit ) {
1440   #  push @buf,[
1441   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1442   #    $money_char. sprintf("%10.2f",$_->credited)
1443   #  ];
1444   #}
1445
1446   #get & print payments
1447   foreach ( $self->cust_bill_pay ) {
1448
1449     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1450
1451     push @buf,[
1452       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1453       $money_char. sprintf("%10.2f",$_->amount )
1454     ];
1455   }
1456
1457   #balance due
1458   my $balance_due_msg = $self->balance_due_msg;
1459
1460   push @buf,['','-----------'];
1461   push @buf,[$balance_due_msg, $money_char. 
1462     sprintf("%10.2f", $balance_due ) ];
1463
1464   #create the template
1465   $template ||= $self->_agent_template;
1466   my $templatefile = 'invoice_template';
1467   $templatefile .= "_$template" if length($template);
1468   my @invoice_template = $conf->config($templatefile)
1469     or die "cannot load config file $templatefile";
1470   $invoice_lines = 0;
1471   my $wasfunc = 0;
1472   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1473     /invoice_lines\((\d*)\)/;
1474     $invoice_lines += $1 || scalar(@buf);
1475     $wasfunc=1;
1476   }
1477   die "no invoice_lines() functions in template?" unless $wasfunc;
1478   my $invoice_template = new Text::Template (
1479     TYPE   => 'ARRAY',
1480     SOURCE => [ map "$_\n", @invoice_template ],
1481   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1482   $invoice_template->compile()
1483     or die "can't compile template: $Text::Template::ERROR";
1484
1485   #setup template variables
1486   package FS::cust_bill::_template; #!
1487   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1488
1489   $invnum = $self->invnum;
1490   $date = $self->_date;
1491   $page = 1;
1492   $agent = $self->cust_main->agent->agent;
1493
1494   if ( $FS::cust_bill::invoice_lines ) {
1495     $total_pages =
1496       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1497     $total_pages++
1498       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1499   } else {
1500     $total_pages = 1;
1501   }
1502
1503   #format address (variable for the template)
1504   my $l = 0;
1505   @address = ( '', '', '', '', '', '' );
1506   package FS::cust_bill; #!
1507   $FS::cust_bill::_template::address[$l++] =
1508     $cust_main->payname.
1509       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1510         ? " (P.O. #". $cust_main->payinfo. ")"
1511         : ''
1512       )
1513   ;
1514   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1515     if $cust_main->company;
1516   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1517   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1518     if $cust_main->address2;
1519   $FS::cust_bill::_template::address[$l++] =
1520     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1521
1522   my $countrydefault = $conf->config('countrydefault') || 'US';
1523   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1524     unless $cust_main->country eq $countrydefault;
1525
1526         #  #overdue? (variable for the template)
1527         #  $FS::cust_bill::_template::overdue = ( 
1528         #    $balance_due > 0
1529         #    && $today > $self->_date 
1530         ##    && $self->printed > 1
1531         #    && $self->printed > 0
1532         #  );
1533
1534   #and subroutine for the template
1535   sub FS::cust_bill::_template::invoice_lines {
1536     my $lines = shift || scalar(@buf);
1537     map { 
1538       scalar(@buf) ? shift @buf : [ '', '' ];
1539     }
1540     ( 1 .. $lines );
1541   }
1542
1543   #and fill it in
1544   $FS::cust_bill::_template::page = 1;
1545   my $lines;
1546   my @collect;
1547   while (@buf) {
1548     push @collect, split("\n",
1549       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1550     );
1551     $FS::cust_bill::_template::page++;
1552   }
1553
1554   map "$_\n", @collect;
1555
1556 }
1557
1558 =item print_latex [ TIME [ , TEMPLATE ] ]
1559
1560 Internal method - returns a filename of a filled-in LaTeX template for this
1561 invoice (Note: add ".tex" to get the actual filename).
1562
1563 See print_ps and print_pdf for methods that return PostScript and PDF output.
1564
1565 TIME an optional value used to control the printing of overdue messages.  The
1566 default is now.  It isn't the date of the invoice; that's the `_date' field.
1567 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1568 L<Time::Local> and L<Date::Parse> for conversion functions.
1569
1570 =cut
1571
1572 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1573 sub print_latex {
1574
1575   my( $self, $today, $template ) = @_;
1576   $today ||= time;
1577   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1578     if $DEBUG;
1579
1580   my $cust_main = $self->cust_main;
1581   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1582     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1583
1584   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1585 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1586   #my $balance_due = $self->owed + $pr_total - $cr_total;
1587   my $balance_due = $self->owed + $pr_total;
1588
1589   #create the template
1590   $template ||= $self->_agent_template;
1591   my $templatefile = 'invoice_latex';
1592   my $suffix = length($template) ? "_$template" : '';
1593   $templatefile .= $suffix;
1594   my @invoice_template = map "$_\n", $conf->config($templatefile)
1595     or die "cannot load config file $templatefile";
1596
1597   my($format, $text_template);
1598   if ( grep { /^%%Detail/ } @invoice_template ) {
1599     #change this to a die when the old code is removed
1600     warn "old-style invoice template $templatefile; ".
1601          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1602     $format = 'old';
1603   } else {
1604     $format = 'Text::Template';
1605     $text_template = new Text::Template(
1606       TYPE => 'ARRAY',
1607       SOURCE => \@invoice_template,
1608       DELIMITERS => [ '[@--', '--@]' ],
1609     );
1610
1611     $text_template->compile()
1612       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1613   }
1614
1615   my $returnaddress;
1616   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1617     $returnaddress = join("\n",
1618       $conf->config_orbase('invoice_latexreturnaddress', $template)
1619     );
1620   } else {
1621     $returnaddress = '~';
1622   }
1623
1624   my %invoice_data = (
1625     'invnum'       => $self->invnum,
1626     'date'         => time2str('%b %o, %Y', $self->_date),
1627     'today'        => time2str('%b %o, %Y', $today),
1628     'agent'        => _latex_escape($cust_main->agent->agent),
1629     'payname'      => _latex_escape($cust_main->payname),
1630     'company'      => _latex_escape($cust_main->company),
1631     'address1'     => _latex_escape($cust_main->address1),
1632     'address2'     => _latex_escape($cust_main->address2),
1633     'city'         => _latex_escape($cust_main->city),
1634     'state'        => _latex_escape($cust_main->state),
1635     'zip'          => _latex_escape($cust_main->zip),
1636     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1637     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1638     'returnaddress' => $returnaddress,
1639     'quantity'     => 1,
1640     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1641     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1642     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1643   );
1644
1645   my $countrydefault = $conf->config('countrydefault') || 'US';
1646   if ( $cust_main->country eq $countrydefault ) {
1647     $invoice_data{'country'} = '';
1648   } else {
1649     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1650   }
1651
1652   $invoice_data{'notes'} =
1653     join("\n",
1654 #  #do variable substitutions in notes
1655 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1656         $conf->config_orbase('invoice_latexnotes', $template)
1657     );
1658   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1659     if $DEBUG;
1660
1661   $invoice_data{'footer'} =~ s/\n+$//;
1662   $invoice_data{'smallfooter'} =~ s/\n+$//;
1663   $invoice_data{'notes'} =~ s/\n+$//;
1664
1665   $invoice_data{'po_line'} =
1666     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1667       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1668       : '~';
1669
1670   my @filled_in = ();
1671   if ( $format eq 'old' ) {
1672   
1673     my @line_item = ();
1674     my @total_item = ();
1675     while ( @invoice_template ) {
1676       my $line = shift @invoice_template;
1677   
1678       if ( $line =~ /^%%Detail\s*$/ ) {
1679   
1680         while ( ( my $line_item_line = shift @invoice_template )
1681                 !~ /^%%EndDetail\s*$/                            ) {
1682           push @line_item, $line_item_line;
1683         }
1684         foreach my $line_item ( $self->_items ) {
1685         #foreach my $line_item ( $self->_items_pkg ) {
1686           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1687           $invoice_data{'description'} =
1688             _latex_escape($line_item->{'description'});
1689           if ( exists $line_item->{'ext_description'} ) {
1690             $invoice_data{'description'} .=
1691               "\\tabularnewline\n~~".
1692               join( "\\tabularnewline\n~~",
1693                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1694                   );
1695           }
1696           $invoice_data{'amount'} = $line_item->{'amount'};
1697           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1698           push @filled_in,
1699             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1700         }
1701   
1702       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1703   
1704         while ( ( my $total_item_line = shift @invoice_template )
1705                 !~ /^%%EndTotalDetails\s*$/                      ) {
1706           push @total_item, $total_item_line;
1707         }
1708   
1709         my @total_fill = ();
1710   
1711         my $taxtotal = 0;
1712         foreach my $tax ( $self->_items_tax ) {
1713           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1714           $taxtotal += $tax->{'amount'};
1715           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1716           push @total_fill,
1717             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1718                 @total_item;
1719         }
1720
1721         if ( $taxtotal ) {
1722           $invoice_data{'total_item'} = 'Sub-total';
1723           $invoice_data{'total_amount'} =
1724             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1725           unshift @total_fill,
1726             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1727                 @total_item;
1728         }
1729   
1730         $invoice_data{'total_item'} = '\textbf{Total}';
1731         $invoice_data{'total_amount'} =
1732           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1733         push @total_fill,
1734           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1735               @total_item;
1736   
1737         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1738   
1739         # credits
1740         foreach my $credit ( $self->_items_credits ) {
1741           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1742           #$credittotal
1743           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1744           push @total_fill, 
1745             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1746                 @total_item;
1747         }
1748   
1749         # payments
1750         foreach my $payment ( $self->_items_payments ) {
1751           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1752           #$paymenttotal
1753           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1754           push @total_fill, 
1755             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1756                 @total_item;
1757         }
1758   
1759         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1760         $invoice_data{'total_amount'} =
1761           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1762         push @total_fill,
1763           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1764               @total_item;
1765   
1766         push @filled_in, @total_fill;
1767   
1768       } else {
1769         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1770         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1771         push @filled_in, $line;
1772       }
1773   
1774     }
1775
1776     sub nounder {
1777       my $var = $1;
1778       $var =~ s/_/\-/g;
1779       $var;
1780     }
1781
1782   } elsif ( $format eq 'Text::Template' ) {
1783
1784     my @detail_items = ();
1785     my @total_items = ();
1786
1787     $invoice_data{'detail_items'} = \@detail_items;
1788     $invoice_data{'total_items'} = \@total_items;
1789   
1790     foreach my $line_item ( $self->_items ) {
1791       my $detail = {
1792         ext_description => [],
1793       };
1794       $detail->{'ref'} = $line_item->{'pkgnum'};
1795       $detail->{'quantity'} = 1;
1796       $detail->{'description'} = _latex_escape($line_item->{'description'});
1797       if ( exists $line_item->{'ext_description'} ) {
1798         @{$detail->{'ext_description'}} = map {
1799           _latex_escape($_);
1800         } @{$line_item->{'ext_description'}};
1801       }
1802       $detail->{'amount'} = $line_item->{'amount'};
1803       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1804   
1805       push @detail_items, $detail;
1806     }
1807   
1808   
1809     my $taxtotal = 0;
1810     foreach my $tax ( $self->_items_tax ) {
1811       my $total = {};
1812       $total->{'total_item'} = _latex_escape($tax->{'description'});
1813       $taxtotal += $tax->{'amount'};
1814       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1815       push @total_items, $total;
1816     }
1817   
1818     if ( $taxtotal ) {
1819       my $total = {};
1820       $total->{'total_item'} = 'Sub-total';
1821       $total->{'total_amount'} =
1822         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1823       unshift @total_items, $total;
1824     }
1825   
1826     {
1827       my $total = {};
1828       $total->{'total_item'} = '\textbf{Total}';
1829       $total->{'total_amount'} =
1830         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1831       push @total_items, $total;
1832     }
1833   
1834     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1835   
1836     # credits
1837     foreach my $credit ( $self->_items_credits ) {
1838       my $total;
1839       $total->{'total_item'} = _latex_escape($credit->{'description'});
1840       #$credittotal
1841       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1842       push @total_items, $total;
1843     }
1844   
1845     # payments
1846     foreach my $payment ( $self->_items_payments ) {
1847       my $total = {};
1848       $total->{'total_item'} = _latex_escape($payment->{'description'});
1849       #$paymenttotal
1850       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1851       push @total_items, $total;
1852     }
1853   
1854     { 
1855       my $total;
1856       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1857       $total->{'total_amount'} =
1858         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1859       push @total_items, $total;
1860     }
1861
1862   } else {
1863     die "guru meditation #54";
1864   }
1865
1866   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1867   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1868                            DIR      => $dir,
1869                            SUFFIX   => '.tex',
1870                            UNLINK   => 0,
1871                          ) or die "can't open temp file: $!\n";
1872   if ( $format eq 'old' ) {
1873     print $fh join('', @filled_in );
1874   } elsif ( $format eq 'Text::Template' ) {
1875     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1876   } else {
1877     die "guru meditation #32";
1878   }
1879   close $fh;
1880
1881   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1882   return $1;
1883
1884 }
1885
1886 =item print_ps [ TIME [ , TEMPLATE ] ]
1887
1888 Returns an postscript invoice, as a scalar.
1889
1890 TIME an optional value used to control the printing of overdue messages.  The
1891 default is now.  It isn't the date of the invoice; that's the `_date' field.
1892 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1893 L<Time::Local> and L<Date::Parse> for conversion functions.
1894
1895 =cut
1896
1897 sub print_ps {
1898   my $self = shift;
1899
1900   my $file = $self->print_latex(@_);
1901
1902   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1903   chdir($dir);
1904
1905   my $sfile = shell_quote $file;
1906
1907   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1908     or die "pslatex $file.tex failed; see $file.log for details?\n";
1909   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1910     or die "pslatex $file.tex failed; see $file.log for details?\n";
1911
1912   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1913     or die "dvips failed";
1914
1915   open(POSTSCRIPT, "<$file.ps")
1916     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1917
1918   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1919
1920   my $ps = '';
1921   while (<POSTSCRIPT>) {
1922     $ps .= $_;
1923   }
1924
1925   close POSTSCRIPT;
1926
1927   return $ps;
1928
1929 }
1930
1931 =item print_pdf [ TIME [ , TEMPLATE ] ]
1932
1933 Returns an PDF invoice, as a scalar.
1934
1935 TIME an optional value used to control the printing of overdue messages.  The
1936 default is now.  It isn't the date of the invoice; that's the `_date' field.
1937 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1938 L<Time::Local> and L<Date::Parse> for conversion functions.
1939
1940 =cut
1941
1942 sub print_pdf {
1943   my $self = shift;
1944
1945   my $file = $self->print_latex(@_);
1946
1947   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1948   chdir($dir);
1949
1950   #system('pdflatex', "$file.tex");
1951   #system('pdflatex', "$file.tex");
1952   #! LaTeX Error: Unknown graphics extension: .eps.
1953
1954   my $sfile = shell_quote $file;
1955
1956   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1957     or die "pslatex $file.tex failed; see $file.log for details?\n";
1958   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1959     or die "pslatex $file.tex failed; see $file.log for details?\n";
1960
1961   #system('dvipdf', "$file.dvi", "$file.pdf" );
1962   system(
1963     "dvips -q -t letter -f $sfile.dvi ".
1964     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1965     "     -c save pop -"
1966   ) == 0
1967     or die "dvips | gs failed: $!";
1968
1969   open(PDF, "<$file.pdf")
1970     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1971
1972   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1973
1974   my $pdf = '';
1975   while (<PDF>) {
1976     $pdf .= $_;
1977   }
1978
1979   close PDF;
1980
1981   return $pdf;
1982
1983 }
1984
1985 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1986
1987 Returns an HTML invoice, as a scalar.
1988
1989 TIME an optional value used to control the printing of overdue messages.  The
1990 default is now.  It isn't the date of the invoice; that's the `_date' field.
1991 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1992 L<Time::Local> and L<Date::Parse> for conversion functions.
1993
1994 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1995 when emailing the invoice as part of a multipart/related MIME email.
1996
1997 =cut
1998
1999 #some falze laziness w/print_text and print_latex (and send_csv)
2000 sub print_html {
2001   my( $self, $today, $template, $cid ) = @_;
2002   $today ||= time;
2003
2004   my $cust_main = $self->cust_main;
2005   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2006     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2007
2008   $template ||= $self->_agent_template;
2009   my $templatefile = 'invoice_html';
2010   my $suffix = length($template) ? "_$template" : '';
2011   $templatefile .= $suffix;
2012   my @html_template = map "$_\n", $conf->config($templatefile)
2013     or die "cannot load config file $templatefile";
2014
2015   my $html_template = new Text::Template(
2016     TYPE   => 'ARRAY',
2017     SOURCE => \@html_template,
2018     DELIMITERS => [ '<%=', '%>' ],
2019   );
2020
2021   $html_template->compile()
2022     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2023
2024   my %invoice_data = (
2025     'invnum'       => $self->invnum,
2026     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2027     'today'        => time2str('%b %o, %Y', $today),
2028     'agent'        => encode_entities($cust_main->agent->agent),
2029     'payname'      => encode_entities($cust_main->payname),
2030     'company'      => encode_entities($cust_main->company),
2031     'address1'     => encode_entities($cust_main->address1),
2032     'address2'     => encode_entities($cust_main->address2),
2033     'city'         => encode_entities($cust_main->city),
2034     'state'        => encode_entities($cust_main->state),
2035     'zip'          => encode_entities($cust_main->zip),
2036     'terms'        => $conf->config('invoice_default_terms')
2037                       || 'Payable upon receipt',
2038     'cid'          => $cid,
2039     'template'     => $template,
2040 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2041   );
2042
2043   if (
2044          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2045       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2046   ) {
2047     $invoice_data{'returnaddress'} =
2048       join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2049   } else {
2050     $invoice_data{'returnaddress'} =
2051       join("\n", map { 
2052                        s/~/&nbsp;/g;
2053                        s/\\\\\*?\s*$/<BR>/;
2054                        s/\\hyphenation\{[\w\s\-]+\}//;
2055                        $_;
2056                      }
2057                      $conf->config_orbase( 'invoice_latexreturnaddress',
2058                                            $template
2059                                          )
2060           );
2061   }
2062
2063   my $countrydefault = $conf->config('countrydefault') || 'US';
2064   if ( $cust_main->country eq $countrydefault ) {
2065     $invoice_data{'country'} = '';
2066   } else {
2067     $invoice_data{'country'} =
2068       encode_entities(code2country($cust_main->country));
2069   }
2070
2071   if (
2072          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2073       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2074   ) {
2075     $invoice_data{'notes'} =
2076       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2077   } else {
2078     $invoice_data{'notes'} = 
2079       join("\n", map { 
2080                        s/%%(.*)$/<!-- $1 -->/;
2081                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2082                        s/\\begin\{enumerate\}/<ol>/;
2083                        s/\\item /  <li>/;
2084                        s/\\end\{enumerate\}/<\/ol>/;
2085                        s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2086                        $_;
2087                      } 
2088                      $conf->config_orbase('invoice_latexnotes', $template)
2089           );
2090   }
2091
2092 #  #do variable substitutions in notes
2093 #  $invoice_data{'notes'} =
2094 #    join("\n",
2095 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2096 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2097 #    );
2098
2099   if (
2100          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2101       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2102   ) {
2103    $invoice_data{'footer'} =
2104      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2105   } else {
2106    $invoice_data{'footer'} =
2107        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2108                       $conf->config_orbase('invoice_latexfooter', $template)
2109            );
2110   }
2111
2112   $invoice_data{'po_line'} =
2113     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2114       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2115       : '';
2116
2117   my $money_char = $conf->config('money_char') || '$';
2118
2119   foreach my $line_item ( $self->_items ) {
2120     my $detail = {
2121       ext_description => [],
2122     };
2123     $detail->{'ref'} = $line_item->{'pkgnum'};
2124     $detail->{'description'} = encode_entities($line_item->{'description'});
2125     if ( exists $line_item->{'ext_description'} ) {
2126       @{$detail->{'ext_description'}} = map {
2127         encode_entities($_);
2128       } @{$line_item->{'ext_description'}};
2129     }
2130     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2131     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2132
2133     push @{$invoice_data{'detail_items'}}, $detail;
2134   }
2135
2136
2137   my $taxtotal = 0;
2138   foreach my $tax ( $self->_items_tax ) {
2139     my $total = {};
2140     $total->{'total_item'} = encode_entities($tax->{'description'});
2141     $taxtotal += $tax->{'amount'};
2142     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2143     push @{$invoice_data{'total_items'}}, $total;
2144   }
2145
2146   if ( $taxtotal ) {
2147     my $total = {};
2148     $total->{'total_item'} = 'Sub-total';
2149     $total->{'total_amount'} =
2150       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2151     unshift @{$invoice_data{'total_items'}}, $total;
2152   }
2153
2154   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2155   {
2156     my $total = {};
2157     $total->{'total_item'} = '<b>Total</b>';
2158     $total->{'total_amount'} =
2159       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2160     push @{$invoice_data{'total_items'}}, $total;
2161   }
2162
2163   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2164
2165   # credits
2166   foreach my $credit ( $self->_items_credits ) {
2167     my $total;
2168     $total->{'total_item'} = encode_entities($credit->{'description'});
2169     #$credittotal
2170     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2171     push @{$invoice_data{'total_items'}}, $total;
2172   }
2173
2174   # payments
2175   foreach my $payment ( $self->_items_payments ) {
2176     my $total = {};
2177     $total->{'total_item'} = encode_entities($payment->{'description'});
2178     #$paymenttotal
2179     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2180     push @{$invoice_data{'total_items'}}, $total;
2181   }
2182
2183   { 
2184     my $total;
2185     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2186     $total->{'total_amount'} =
2187       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2188     push @{$invoice_data{'total_items'}}, $total;
2189   }
2190
2191   $html_template->fill_in( HASH => \%invoice_data);
2192 }
2193
2194 # quick subroutine for print_latex
2195 #
2196 # There are ten characters that LaTeX treats as special characters, which
2197 # means that they do not simply typeset themselves: 
2198 #      # $ % & ~ _ ^ \ { }
2199 #
2200 # TeX ignores blanks following an escaped character; if you want a blank (as
2201 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2202
2203 sub _latex_escape {
2204   my $value = shift;
2205   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2206   $value =~ s/([<>])/\$$1\$/g;
2207   $value;
2208 }
2209
2210 #utility methods for print_*
2211
2212 sub balance_due_msg {
2213   my $self = shift;
2214   my $msg = 'Balance Due';
2215   return $msg unless $conf->exists('invoice_default_terms');
2216   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2217     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2218   } elsif ( $conf->config('invoice_default_terms') ) {
2219     $msg .= ' - '. $conf->config('invoice_default_terms');
2220   }
2221   $msg;
2222 }
2223
2224 sub _items {
2225   my $self = shift;
2226   my @display = scalar(@_)
2227                 ? @_
2228                 : qw( _items_previous _items_pkg );
2229                 #: qw( _items_pkg );
2230                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2231   my @b = ();
2232   foreach my $display ( @display ) {
2233     push @b, $self->$display(@_);
2234   }
2235   @b;
2236 }
2237
2238 sub _items_previous {
2239   my $self = shift;
2240   my $cust_main = $self->cust_main;
2241   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2242   my @b = ();
2243   foreach ( @pr_cust_bill ) {
2244     push @b, {
2245       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2246                        ' ('. time2str('%x',$_->_date). ')',
2247       #'pkgpart'     => 'N/A',
2248       'pkgnum'      => 'N/A',
2249       'amount'      => sprintf("%.2f", $_->owed),
2250     };
2251   }
2252   @b;
2253
2254   #{
2255   #    'description'     => 'Previous Balance',
2256   #    #'pkgpart'         => 'N/A',
2257   #    'pkgnum'          => 'N/A',
2258   #    'amount'          => sprintf("%10.2f", $pr_total ),
2259   #    'ext_description' => [ map {
2260   #                                 "Invoice ". $_->invnum.
2261   #                                 " (". time2str("%x",$_->_date). ") ".
2262   #                                 sprintf("%10.2f", $_->owed)
2263   #                         } @pr_cust_bill ],
2264
2265   #};
2266 }
2267
2268 sub _items_pkg {
2269   my $self = shift;
2270   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2271   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2272 }
2273
2274 sub _items_tax {
2275   my $self = shift;
2276   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2277   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2278 }
2279
2280 sub _items_cust_bill_pkg {
2281   my $self = shift;
2282   my $cust_bill_pkg = shift;
2283
2284   my @b = ();
2285   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2286
2287     my $desc = $cust_bill_pkg->desc;
2288
2289     if ( $cust_bill_pkg->pkgnum > 0 ) {
2290
2291       if ( $cust_bill_pkg->setup != 0 ) {
2292         my $description = $desc;
2293         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2294         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2295         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2296         push @b, {
2297           description     => $description,
2298           #pkgpart         => $part_pkg->pkgpart,
2299           pkgnum          => $cust_bill_pkg->pkgnum,
2300           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2301           ext_description => \@d,
2302         };
2303       }
2304
2305       if ( $cust_bill_pkg->recur != 0 ) {
2306         push @b, {
2307           description     => "$desc (" .
2308                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2309                                time2str('%x', $cust_bill_pkg->edate). ')',
2310           #pkgpart         => $part_pkg->pkgpart,
2311           pkgnum          => $cust_bill_pkg->pkgnum,
2312           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2313           ext_description =>
2314             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2315                                                         $cust_bill_pkg->sdate),
2316               $cust_bill_pkg->details,
2317             ],
2318         };
2319       }
2320
2321     } else { #pkgnum tax or one-shot line item (??)
2322
2323       if ( $cust_bill_pkg->setup != 0 ) {
2324         push @b, {
2325           'description' => $desc,
2326           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
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           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2335         };
2336       }
2337
2338     }
2339
2340   }
2341
2342   @b;
2343
2344 }
2345
2346 sub _items_credits {
2347   my $self = shift;
2348
2349   my @b;
2350   #credits
2351   foreach ( $self->cust_credited ) {
2352
2353     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2354
2355     my $reason = $_->cust_credit->reason;
2356     #my $reason = substr($_->cust_credit->reason,0,32);
2357     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2358     $reason = " ($reason) " if $reason;
2359     push @b, {
2360       #'description' => 'Credit ref\#'. $_->crednum.
2361       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2362       #                 $reason,
2363       'description' => 'Credit applied '.
2364                        time2str("%x",$_->cust_credit->_date). $reason,
2365       'amount'      => sprintf("%.2f",$_->amount),
2366     };
2367   }
2368   #foreach ( @cr_cust_credit ) {
2369   #  push @buf,[
2370   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2371   #    $money_char. sprintf("%10.2f",$_->credited)
2372   #  ];
2373   #}
2374
2375   @b;
2376
2377 }
2378
2379 sub _items_payments {
2380   my $self = shift;
2381
2382   my @b;
2383   #get & print payments
2384   foreach ( $self->cust_bill_pay ) {
2385
2386     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2387
2388     push @b, {
2389       'description' => "Payment received ".
2390                        time2str("%x",$_->cust_pay->_date ),
2391       'amount'      => sprintf("%.2f", $_->amount )
2392     };
2393   }
2394
2395   @b;
2396
2397 }
2398
2399 =back
2400
2401 =head1 SUBROUTINES
2402
2403 =over 4
2404
2405 =item reprint
2406
2407 =cut
2408
2409 sub process_reprint {
2410   process_re_X('print', @_);
2411 }
2412
2413 =item reemail
2414
2415 =cut
2416
2417 sub process_reemail {
2418   process_re_X('email', @_);
2419 }
2420
2421 =item refax
2422
2423 =cut
2424
2425 sub process_refax {
2426   process_re_X('fax', @_);
2427 }
2428
2429 use Storable qw(thaw);
2430 use Data::Dumper;
2431 use MIME::Base64;
2432 sub process_re_X {
2433   my( $method, $job ) = ( shift, shift );
2434
2435   my $param = thaw(decode_base64(shift));
2436   warn Dumper($param) if $DEBUG;
2437
2438   re_X(
2439     $method,
2440     $job,
2441     %$param,
2442   );
2443
2444 }
2445
2446 sub re_X {
2447   my($method, $job, %param ) = @_;
2448 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2449
2450   #some false laziness w/search/cust_bill.html
2451   my $distinct = '';
2452   my $orderby = 'ORDER BY cust_bill._date';
2453
2454   my @where;
2455
2456   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2457     push @where, "cust_bill._date >= $1";
2458   }
2459   if ( $param{'end'} =~ /^(\d+)$/ ) {
2460     push @where, "cust_bill._date < $1";
2461   }
2462   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2463     push @where, "cust_main.agentnum = $1";
2464   }
2465
2466   my $owed =
2467     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2468                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2469              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2470                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2471
2472   push @where, "0 != $owed"
2473     if $param{'open'};
2474
2475   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2476     if $param{'days'};
2477
2478   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2479
2480   my $addl_from = 'left join cust_main using ( custnum )';
2481
2482   if ( $param{'newest_percust'} ) {
2483     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2484     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2485     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2486   }
2487      
2488   my @cust_bill = qsearch( 'cust_bill',
2489                            {},
2490                            "$distinct cust_bill.*",
2491                            $extra_sql,
2492                            '',
2493                            $addl_from
2494                          );
2495
2496   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2497   foreach my $cust_bill ( @cust_bill ) {
2498     $cust_bill->$method();
2499
2500     if ( $job ) { #progressbar foo
2501       $num++;
2502       if ( time - $min_sec > $last ) {
2503         my $error = $job->update_statustext(
2504           int( 100 * $num / scalar(@cust_bill) )
2505         );
2506         die $error if $error;
2507         $last = time;
2508       }
2509     }
2510
2511   }
2512
2513 }
2514
2515 =back
2516
2517 =head1 BUGS
2518
2519 The delete method.
2520
2521 print_text formatting (and some logic :/) is in source, but needs to be
2522 slurped in from a file.  Also number of lines ($=).
2523
2524 =head1 SEE ALSO
2525
2526 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2527 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2528 documentation.
2529
2530 =cut
2531
2532 1;
2533