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