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