add desc method to cust_bill_pkg and use it in cust_bill... this should help with...
[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 and _items stuff)
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/_items stuff (and send_csv)
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     my $desc = $cust_bill_pkg->desc;
1106
1107     if ( $cust_bill_pkg->pkgnum > 0 ) {
1108
1109       if ( $cust_bill_pkg->setup != 0 ) {
1110         my $description = $desc;
1111         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1112         push @buf, [ $description,
1113                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1114         push @buf,
1115           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1116               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1117       }
1118
1119       if ( $cust_bill_pkg->recur != 0 ) {
1120         push @buf, [
1121           "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1122                       time2str("%x", $cust_bill_pkg->edate) . ")",
1123           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1124         ];
1125         push @buf,
1126           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1127               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1128                                                   $cust_bill_pkg->sdate );
1129       }
1130
1131       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1132
1133     } else { #pkgnum tax or one-shot line item
1134
1135       if ( $cust_bill_pkg->setup != 0 ) {
1136         push @buf, [ $desc,
1137                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1138       }
1139       if ( $cust_bill_pkg->recur != 0 ) {
1140         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1141                               . time2str("%x", $cust_bill_pkg->edate). ")",
1142                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1143                    ];
1144       }
1145
1146     }
1147
1148   }
1149
1150   push @buf,['','-----------'];
1151   push @buf,['Total New Charges',
1152              $money_char. sprintf("%10.2f",$self->charged) ];
1153   push @buf,['',''];
1154
1155   push @buf,['','-----------'];
1156   push @buf,['Total Charges',
1157              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1158   push @buf,['',''];
1159
1160   #credits
1161   foreach ( $self->cust_credited ) {
1162
1163     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1164
1165     my $reason = substr($_->cust_credit->reason,0,32);
1166     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1167     $reason = " ($reason) " if $reason;
1168     push @buf,[
1169       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1170         $reason,
1171       $money_char. sprintf("%10.2f",$_->amount)
1172     ];
1173   }
1174   #foreach ( @cr_cust_credit ) {
1175   #  push @buf,[
1176   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1177   #    $money_char. sprintf("%10.2f",$_->credited)
1178   #  ];
1179   #}
1180
1181   #get & print payments
1182   foreach ( $self->cust_bill_pay ) {
1183
1184     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1185
1186     push @buf,[
1187       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1188       $money_char. sprintf("%10.2f",$_->amount )
1189     ];
1190   }
1191
1192   #balance due
1193   my $balance_due_msg = $self->balance_due_msg;
1194
1195   push @buf,['','-----------'];
1196   push @buf,[$balance_due_msg, $money_char. 
1197     sprintf("%10.2f", $balance_due ) ];
1198
1199   #create the template
1200   $template ||= $self->_agent_template;
1201   my $templatefile = 'invoice_template';
1202   $templatefile .= "_$template" if length($template);
1203   my @invoice_template = $conf->config($templatefile)
1204     or die "cannot load config file $templatefile";
1205   $invoice_lines = 0;
1206   my $wasfunc = 0;
1207   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1208     /invoice_lines\((\d*)\)/;
1209     $invoice_lines += $1 || scalar(@buf);
1210     $wasfunc=1;
1211   }
1212   die "no invoice_lines() functions in template?" unless $wasfunc;
1213   my $invoice_template = new Text::Template (
1214     TYPE   => 'ARRAY',
1215     SOURCE => [ map "$_\n", @invoice_template ],
1216   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1217   $invoice_template->compile()
1218     or die "can't compile template: $Text::Template::ERROR";
1219
1220   #setup template variables
1221   package FS::cust_bill::_template; #!
1222   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1223
1224   $invnum = $self->invnum;
1225   $date = $self->_date;
1226   $page = 1;
1227   $agent = $self->cust_main->agent->agent;
1228
1229   if ( $FS::cust_bill::invoice_lines ) {
1230     $total_pages =
1231       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1232     $total_pages++
1233       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1234   } else {
1235     $total_pages = 1;
1236   }
1237
1238   #format address (variable for the template)
1239   my $l = 0;
1240   @address = ( '', '', '', '', '', '' );
1241   package FS::cust_bill; #!
1242   $FS::cust_bill::_template::address[$l++] =
1243     $cust_main->payname.
1244       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1245         ? " (P.O. #". $cust_main->payinfo. ")"
1246         : ''
1247       )
1248   ;
1249   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1250     if $cust_main->company;
1251   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1252   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1253     if $cust_main->address2;
1254   $FS::cust_bill::_template::address[$l++] =
1255     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1256
1257   my $countrydefault = $conf->config('countrydefault') || 'US';
1258   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1259     unless $cust_main->country eq $countrydefault;
1260
1261         #  #overdue? (variable for the template)
1262         #  $FS::cust_bill::_template::overdue = ( 
1263         #    $balance_due > 0
1264         #    && $today > $self->_date 
1265         ##    && $self->printed > 1
1266         #    && $self->printed > 0
1267         #  );
1268
1269   #and subroutine for the template
1270   sub FS::cust_bill::_template::invoice_lines {
1271     my $lines = shift || scalar(@buf);
1272     map { 
1273       scalar(@buf) ? shift @buf : [ '', '' ];
1274     }
1275     ( 1 .. $lines );
1276   }
1277
1278   #and fill it in
1279   $FS::cust_bill::_template::page = 1;
1280   my $lines;
1281   my @collect;
1282   while (@buf) {
1283     push @collect, split("\n",
1284       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1285     );
1286     $FS::cust_bill::_template::page++;
1287   }
1288
1289   map "$_\n", @collect;
1290
1291 }
1292
1293 =item print_latex [ TIME [ , TEMPLATE ] ]
1294
1295 Internal method - returns a filename of a filled-in LaTeX template for this
1296 invoice (Note: add ".tex" to get the actual filename).
1297
1298 See print_ps and print_pdf for methods that return PostScript and PDF output.
1299
1300 TIME an optional value used to control the printing of overdue messages.  The
1301 default is now.  It isn't the date of the invoice; that's the `_date' field.
1302 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1303 L<Time::Local> and L<Date::Parse> for conversion functions.
1304
1305 =cut
1306
1307 #still some false laziness w/print_text (mostly print_text should use _items stuff though)
1308 sub print_latex {
1309
1310   my( $self, $today, $template ) = @_;
1311   $today ||= time;
1312   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1313     if $DEBUG;
1314
1315   my $cust_main = $self->cust_main;
1316   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1317     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1318
1319   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1320 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1321   #my $balance_due = $self->owed + $pr_total - $cr_total;
1322   my $balance_due = $self->owed + $pr_total;
1323
1324   #create the template
1325   $template ||= $self->_agent_template;
1326   my $templatefile = 'invoice_latex';
1327   my $suffix = length($template) ? "_$template" : '';
1328   $templatefile .= $suffix;
1329   my @invoice_template = map "$_\n", $conf->config($templatefile)
1330     or die "cannot load config file $templatefile";
1331
1332   my($format, $text_template);
1333   if ( grep { /^%%Detail/ } @invoice_template ) {
1334     #change this to a die when the old code is removed
1335     warn "old-style invoice template $templatefile; ".
1336          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1337     $format = 'old';
1338   } else {
1339     $format = 'Text::Template';
1340     $text_template = new Text::Template(
1341       TYPE => 'ARRAY',
1342       SOURCE => \@invoice_template,
1343       DELIMITERS => [ '[@--', '--@]' ],
1344     );
1345
1346     $text_template->compile()
1347       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1348   }
1349
1350   my $returnaddress;
1351   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1352     $returnaddress = join("\n",
1353       $conf->config_orbase('invoice_latexreturnaddress', $template)
1354     );
1355   } else {
1356     $returnaddress = '~';
1357   }
1358
1359   my %invoice_data = (
1360     'invnum'       => $self->invnum,
1361     'date'         => time2str('%b %o, %Y', $self->_date),
1362     'today'        => time2str('%b %o, %Y', $today),
1363     'agent'        => _latex_escape($cust_main->agent->agent),
1364     'payname'      => _latex_escape($cust_main->payname),
1365     'company'      => _latex_escape($cust_main->company),
1366     'address1'     => _latex_escape($cust_main->address1),
1367     'address2'     => _latex_escape($cust_main->address2),
1368     'city'         => _latex_escape($cust_main->city),
1369     'state'        => _latex_escape($cust_main->state),
1370     'zip'          => _latex_escape($cust_main->zip),
1371     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1372     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1373     'returnaddress' => $returnaddress,
1374     'quantity'     => 1,
1375     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1376     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1377     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1378   );
1379
1380   my $countrydefault = $conf->config('countrydefault') || 'US';
1381   if ( $cust_main->country eq $countrydefault ) {
1382     $invoice_data{'country'} = '';
1383   } else {
1384     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1385   }
1386
1387   $invoice_data{'notes'} =
1388     join("\n",
1389 #  #do variable substitutions in notes
1390 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1391         $conf->config_orbase('invoice_latexnotes', $template)
1392     );
1393   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1394     if $DEBUG;
1395
1396   $invoice_data{'footer'} =~ s/\n+$//;
1397   $invoice_data{'smallfooter'} =~ s/\n+$//;
1398   $invoice_data{'notes'} =~ s/\n+$//;
1399
1400   $invoice_data{'po_line'} =
1401     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1402       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1403       : '~';
1404
1405   my @filled_in = ();
1406   if ( $format eq 'old' ) {
1407   
1408     my @line_item = ();
1409     my @total_item = ();
1410     while ( @invoice_template ) {
1411       my $line = shift @invoice_template;
1412   
1413       if ( $line =~ /^%%Detail\s*$/ ) {
1414   
1415         while ( ( my $line_item_line = shift @invoice_template )
1416                 !~ /^%%EndDetail\s*$/                            ) {
1417           push @line_item, $line_item_line;
1418         }
1419         foreach my $line_item ( $self->_items ) {
1420         #foreach my $line_item ( $self->_items_pkg ) {
1421           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1422           $invoice_data{'description'} =
1423             _latex_escape($line_item->{'description'});
1424           if ( exists $line_item->{'ext_description'} ) {
1425             $invoice_data{'description'} .=
1426               "\\tabularnewline\n~~".
1427               join( "\\tabularnewline\n~~",
1428                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1429                   );
1430           }
1431           $invoice_data{'amount'} = $line_item->{'amount'};
1432           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1433           push @filled_in,
1434             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1435         }
1436   
1437       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1438   
1439         while ( ( my $total_item_line = shift @invoice_template )
1440                 !~ /^%%EndTotalDetails\s*$/                      ) {
1441           push @total_item, $total_item_line;
1442         }
1443   
1444         my @total_fill = ();
1445   
1446         my $taxtotal = 0;
1447         foreach my $tax ( $self->_items_tax ) {
1448           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1449           $taxtotal += $tax->{'amount'};
1450           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1451           push @total_fill,
1452             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1453                 @total_item;
1454         }
1455
1456         if ( $taxtotal ) {
1457           $invoice_data{'total_item'} = 'Sub-total';
1458           $invoice_data{'total_amount'} =
1459             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1460           unshift @total_fill,
1461             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1462                 @total_item;
1463         }
1464   
1465         $invoice_data{'total_item'} = '\textbf{Total}';
1466         $invoice_data{'total_amount'} =
1467           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1468         push @total_fill,
1469           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1470               @total_item;
1471   
1472         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1473   
1474         # credits
1475         foreach my $credit ( $self->_items_credits ) {
1476           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1477           #$credittotal
1478           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1479           push @total_fill, 
1480             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1481                 @total_item;
1482         }
1483   
1484         # payments
1485         foreach my $payment ( $self->_items_payments ) {
1486           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1487           #$paymenttotal
1488           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1489           push @total_fill, 
1490             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1491                 @total_item;
1492         }
1493   
1494         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1495         $invoice_data{'total_amount'} =
1496           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1497         push @total_fill,
1498           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1499               @total_item;
1500   
1501         push @filled_in, @total_fill;
1502   
1503       } else {
1504         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1505         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1506         push @filled_in, $line;
1507       }
1508   
1509     }
1510
1511     sub nounder {
1512       my $var = $1;
1513       $var =~ s/_/\-/g;
1514       $var;
1515     }
1516
1517   } elsif ( $format eq 'Text::Template' ) {
1518
1519     my @detail_items = ();
1520     my @total_items = ();
1521
1522     $invoice_data{'detail_items'} = \@detail_items;
1523     $invoice_data{'total_items'} = \@total_items;
1524   
1525     foreach my $line_item ( $self->_items ) {
1526       my $detail = {
1527         ext_description => [],
1528       };
1529       $detail->{'ref'} = $line_item->{'pkgnum'};
1530       $detail->{'quantity'} = 1;
1531       $detail->{'description'} = _latex_escape($line_item->{'description'});
1532       if ( exists $line_item->{'ext_description'} ) {
1533         @{$detail->{'ext_description'}} = map {
1534           _latex_escape($_);
1535         } @{$line_item->{'ext_description'}};
1536       }
1537       $detail->{'amount'} = $line_item->{'amount'};
1538       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1539   
1540       push @detail_items, $detail;
1541     }
1542   
1543   
1544     my $taxtotal = 0;
1545     foreach my $tax ( $self->_items_tax ) {
1546       my $total = {};
1547       $total->{'total_item'} = _latex_escape($tax->{'description'});
1548       $taxtotal += $tax->{'amount'};
1549       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1550       push @total_items, $total;
1551     }
1552   
1553     if ( $taxtotal ) {
1554       my $total = {};
1555       $total->{'total_item'} = 'Sub-total';
1556       $total->{'total_amount'} =
1557         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1558       unshift @total_items, $total;
1559     }
1560   
1561     {
1562       my $total = {};
1563       $total->{'total_item'} = '\textbf{Total}';
1564       $total->{'total_amount'} =
1565         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1566       push @total_items, $total;
1567     }
1568   
1569     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1570   
1571     # credits
1572     foreach my $credit ( $self->_items_credits ) {
1573       my $total;
1574       $total->{'total_item'} = _latex_escape($credit->{'description'});
1575       #$credittotal
1576       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1577       push @total_items, $total;
1578     }
1579   
1580     # payments
1581     foreach my $payment ( $self->_items_payments ) {
1582       my $total = {};
1583       $total->{'total_item'} = _latex_escape($payment->{'description'});
1584       #$paymenttotal
1585       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1586       push @total_items, $total;
1587     }
1588   
1589     { 
1590       my $total;
1591       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1592       $total->{'total_amount'} =
1593         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1594       push @total_items, $total;
1595     }
1596
1597   } else {
1598     die "guru meditation #54";
1599   }
1600
1601   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1602   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1603                            DIR      => $dir,
1604                            SUFFIX   => '.tex',
1605                            UNLINK   => 0,
1606                          ) or die "can't open temp file: $!\n";
1607   if ( $format eq 'old' ) {
1608     print $fh join('', @filled_in );
1609   } elsif ( $format eq 'Text::Template' ) {
1610     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1611   } else {
1612     die "guru meditation #32";
1613   }
1614   close $fh;
1615
1616   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1617   return $1;
1618
1619 }
1620
1621 =item print_ps [ TIME [ , TEMPLATE ] ]
1622
1623 Returns an postscript invoice, as a scalar.
1624
1625 TIME an optional value used to control the printing of overdue messages.  The
1626 default is now.  It isn't the date of the invoice; that's the `_date' field.
1627 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1628 L<Time::Local> and L<Date::Parse> for conversion functions.
1629
1630 =cut
1631
1632 sub print_ps {
1633   my $self = shift;
1634
1635   my $file = $self->print_latex(@_);
1636
1637   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1638   chdir($dir);
1639
1640   my $sfile = shell_quote $file;
1641
1642   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1643     or die "pslatex $file.tex failed; see $file.log for details?\n";
1644   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1645     or die "pslatex $file.tex failed; see $file.log for details?\n";
1646
1647   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1648     or die "dvips failed";
1649
1650   open(POSTSCRIPT, "<$file.ps")
1651     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1652
1653   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1654
1655   my $ps = '';
1656   while (<POSTSCRIPT>) {
1657     $ps .= $_;
1658   }
1659
1660   close POSTSCRIPT;
1661
1662   return $ps;
1663
1664 }
1665
1666 =item print_pdf [ TIME [ , TEMPLATE ] ]
1667
1668 Returns an PDF invoice, as a scalar.
1669
1670 TIME an optional value used to control the printing of overdue messages.  The
1671 default is now.  It isn't the date of the invoice; that's the `_date' field.
1672 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1673 L<Time::Local> and L<Date::Parse> for conversion functions.
1674
1675 =cut
1676
1677 sub print_pdf {
1678   my $self = shift;
1679
1680   my $file = $self->print_latex(@_);
1681
1682   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1683   chdir($dir);
1684
1685   #system('pdflatex', "$file.tex");
1686   #system('pdflatex', "$file.tex");
1687   #! LaTeX Error: Unknown graphics extension: .eps.
1688
1689   my $sfile = shell_quote $file;
1690
1691   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1692     or die "pslatex $file.tex failed; see $file.log for details?\n";
1693   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1694     or die "pslatex $file.tex failed; see $file.log for details?\n";
1695
1696   #system('dvipdf', "$file.dvi", "$file.pdf" );
1697   system(
1698     "dvips -q -t letter -f $sfile.dvi ".
1699     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1700     "     -c save pop -"
1701   ) == 0
1702     or die "dvips | gs failed: $!";
1703
1704   open(PDF, "<$file.pdf")
1705     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1706
1707   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1708
1709   my $pdf = '';
1710   while (<PDF>) {
1711     $pdf .= $_;
1712   }
1713
1714   close PDF;
1715
1716   return $pdf;
1717
1718 }
1719
1720 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1721
1722 Returns an HTML invoice, as a scalar.
1723
1724 TIME an optional value used to control the printing of overdue messages.  The
1725 default is now.  It isn't the date of the invoice; that's the `_date' field.
1726 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1727 L<Time::Local> and L<Date::Parse> for conversion functions.
1728
1729 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1730 when emailing the invoice as part of a multipart/related MIME email.
1731
1732 =cut
1733
1734 sub print_html {
1735   my( $self, $today, $template, $cid ) = @_;
1736   $today ||= time;
1737
1738   my $cust_main = $self->cust_main;
1739   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1740     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1741
1742   $template ||= $self->_agent_template;
1743   my $templatefile = 'invoice_html';
1744   my $suffix = length($template) ? "_$template" : '';
1745   $templatefile .= $suffix;
1746   my @html_template = map "$_\n", $conf->config($templatefile)
1747     or die "cannot load config file $templatefile";
1748
1749   my $html_template = new Text::Template(
1750     TYPE   => 'ARRAY',
1751     SOURCE => \@html_template,
1752     DELIMITERS => [ '<%=', '%>' ],
1753   );
1754
1755   $html_template->compile()
1756     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1757
1758   my %invoice_data = (
1759     'invnum'       => $self->invnum,
1760     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
1761     'today'        => time2str('%b %o, %Y', $today),
1762     'agent'        => encode_entities($cust_main->agent->agent),
1763     'payname'      => encode_entities($cust_main->payname),
1764     'company'      => encode_entities($cust_main->company),
1765     'address1'     => encode_entities($cust_main->address1),
1766     'address2'     => encode_entities($cust_main->address2),
1767     'city'         => encode_entities($cust_main->city),
1768     'state'        => encode_entities($cust_main->state),
1769     'zip'          => encode_entities($cust_main->zip),
1770     'terms'        => $conf->config('invoice_default_terms')
1771                       || 'Payable upon receipt',
1772     'cid'          => $cid,
1773     'template'     => $template,
1774 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1775   );
1776
1777   $invoice_data{'returnaddress'} =
1778     length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1779       ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1780       : join("\n", map { 
1781                          s/~/&nbsp;/g;
1782                          s/\\\\\*?\s*$/<BR>/;
1783                          s/\\hyphenation\{[\w\s\-]+\}//;
1784                          $_;
1785                        }
1786                        $conf->config_orbase('invoice_latexreturnaddress', $template)
1787             );
1788
1789   my $countrydefault = $conf->config('countrydefault') || 'US';
1790   if ( $cust_main->country eq $countrydefault ) {
1791     $invoice_data{'country'} = '';
1792   } else {
1793     $invoice_data{'country'} =
1794       encode_entities(code2country($cust_main->country));
1795   }
1796
1797   $invoice_data{'notes'} =
1798     length($conf->config_orbase('invoice_htmlnotes', $template))
1799       ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1800       : join("\n", map { 
1801                          s/%%(.*)$/<!-- $1 -->/;
1802                          s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1803                          s/\\begin\{enumerate\}/<ol>/;
1804                          s/\\item /  <li>/;
1805                          s/\\end\{enumerate\}/<\/ol>/;
1806                          s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1807                          $_;
1808                        } 
1809                        $conf->config_orbase('invoice_latexnotes', $template)
1810             );
1811
1812 #  #do variable substitutions in notes
1813 #  $invoice_data{'notes'} =
1814 #    join("\n",
1815 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1816 #        $conf->config_orbase('invoice_latexnotes', $suffix)
1817 #    );
1818
1819    $invoice_data{'footer'} =
1820      length($conf->config_orbase('invoice_htmlfooter', $template))
1821        ? join("\n", $conf->config_orbase('invoice_htmlfooter', $template) )
1822        : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
1823                         $conf->config_orbase('invoice_latexfooter', $template)
1824              );
1825
1826   $invoice_data{'po_line'} =
1827     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1828       ? encode_entities("Purchase Order #". $cust_main->payinfo)
1829       : '';
1830
1831   my $money_char = $conf->config('money_char') || '$';
1832
1833   foreach my $line_item ( $self->_items ) {
1834     my $detail = {
1835       ext_description => [],
1836     };
1837     $detail->{'ref'} = $line_item->{'pkgnum'};
1838     $detail->{'description'} = encode_entities($line_item->{'description'});
1839     if ( exists $line_item->{'ext_description'} ) {
1840       @{$detail->{'ext_description'}} = map {
1841         encode_entities($_);
1842       } @{$line_item->{'ext_description'}};
1843     }
1844     $detail->{'amount'} = $money_char. $line_item->{'amount'};
1845     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1846
1847     push @{$invoice_data{'detail_items'}}, $detail;
1848   }
1849
1850
1851   my $taxtotal = 0;
1852   foreach my $tax ( $self->_items_tax ) {
1853     my $total = {};
1854     $total->{'total_item'} = encode_entities($tax->{'description'});
1855     $taxtotal += $tax->{'amount'};
1856     $total->{'total_amount'} = $money_char. $tax->{'amount'};
1857     push @{$invoice_data{'total_items'}}, $total;
1858   }
1859
1860   if ( $taxtotal ) {
1861     my $total = {};
1862     $total->{'total_item'} = 'Sub-total';
1863     $total->{'total_amount'} =
1864       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1865     unshift @{$invoice_data{'total_items'}}, $total;
1866   }
1867
1868   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1869   {
1870     my $total = {};
1871     $total->{'total_item'} = '<b>Total</b>';
1872     $total->{'total_amount'} =
1873       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1874     push @{$invoice_data{'total_items'}}, $total;
1875   }
1876
1877   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1878
1879   # credits
1880   foreach my $credit ( $self->_items_credits ) {
1881     my $total;
1882     $total->{'total_item'} = encode_entities($credit->{'description'});
1883     #$credittotal
1884     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1885     push @{$invoice_data{'total_items'}}, $total;
1886   }
1887
1888   # payments
1889   foreach my $payment ( $self->_items_payments ) {
1890     my $total = {};
1891     $total->{'total_item'} = encode_entities($payment->{'description'});
1892     #$paymenttotal
1893     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1894     push @{$invoice_data{'total_items'}}, $total;
1895   }
1896
1897   { 
1898     my $total;
1899     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1900     $total->{'total_amount'} =
1901       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1902     push @{$invoice_data{'total_items'}}, $total;
1903   }
1904
1905   $html_template->fill_in( HASH => \%invoice_data);
1906 }
1907
1908 # quick subroutine for print_latex
1909 #
1910 # There are ten characters that LaTeX treats as special characters, which
1911 # means that they do not simply typeset themselves: 
1912 #      # $ % & ~ _ ^ \ { }
1913 #
1914 # TeX ignores blanks following an escaped character; if you want a blank (as
1915 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1916
1917 sub _latex_escape {
1918   my $value = shift;
1919   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1920   $value =~ s/([<>])/\$$1\$/g;
1921   $value;
1922 }
1923
1924 #utility methods for print_*
1925
1926 sub balance_due_msg {
1927   my $self = shift;
1928   my $msg = 'Balance Due';
1929   return $msg unless $conf->exists('invoice_default_terms');
1930   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1931     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1932   } elsif ( $conf->config('invoice_default_terms') ) {
1933     $msg .= ' - '. $conf->config('invoice_default_terms');
1934   }
1935   $msg;
1936 }
1937
1938 sub _items {
1939   my $self = shift;
1940   my @display = scalar(@_)
1941                 ? @_
1942                 : qw( _items_previous _items_pkg );
1943                 #: qw( _items_pkg );
1944                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1945   my @b = ();
1946   foreach my $display ( @display ) {
1947     push @b, $self->$display(@_);
1948   }
1949   @b;
1950 }
1951
1952 sub _items_previous {
1953   my $self = shift;
1954   my $cust_main = $self->cust_main;
1955   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1956   my @b = ();
1957   foreach ( @pr_cust_bill ) {
1958     push @b, {
1959       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1960                        ' ('. time2str('%x',$_->_date). ')',
1961       #'pkgpart'     => 'N/A',
1962       'pkgnum'      => 'N/A',
1963       'amount'      => sprintf("%.2f", $_->owed),
1964     };
1965   }
1966   @b;
1967
1968   #{
1969   #    'description'     => 'Previous Balance',
1970   #    #'pkgpart'         => 'N/A',
1971   #    'pkgnum'          => 'N/A',
1972   #    'amount'          => sprintf("%10.2f", $pr_total ),
1973   #    'ext_description' => [ map {
1974   #                                 "Invoice ". $_->invnum.
1975   #                                 " (". time2str("%x",$_->_date). ") ".
1976   #                                 sprintf("%10.2f", $_->owed)
1977   #                         } @pr_cust_bill ],
1978
1979   #};
1980 }
1981
1982 sub _items_pkg {
1983   my $self = shift;
1984   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1985   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1986 }
1987
1988 sub _items_tax {
1989   my $self = shift;
1990   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1991   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1992 }
1993
1994 sub _items_cust_bill_pkg {
1995   my $self = shift;
1996   my $cust_bill_pkg = shift;
1997
1998   my @b = ();
1999   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2000
2001     my $desc = $cust_bill_pkg->desc;
2002
2003     if ( $cust_bill_pkg->pkgnum > 0 ) {
2004
2005       if ( $cust_bill_pkg->setup != 0 ) {
2006         my $description = $desc;
2007         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2008         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2009         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2010         push @b, {
2011           description     => $description,
2012           #pkgpart         => $part_pkg->pkgpart,
2013           pkgnum          => $cust_bill_pkg->pkgnum,
2014           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2015           ext_description => \@d,
2016         };
2017       }
2018
2019       if ( $cust_bill_pkg->recur != 0 ) {
2020         push @b, {
2021           description     => "$desc (" .
2022                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2023                                time2str('%x', $cust_bill_pkg->edate). ')',
2024           #pkgpart         => $part_pkg->pkgpart,
2025           pkgnum          => $cust_bill_pkg->pkgnum,
2026           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2027           ext_description =>
2028             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2029                                                         $cust_bill_pkg->sdate),
2030               $cust_bill_pkg->details,
2031             ],
2032         };
2033       }
2034
2035     } else { #pkgnum tax or one-shot line item (??)
2036
2037       if ( $cust_bill_pkg->setup != 0 ) {
2038         push @b, {
2039           'description' => $desc,
2040           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2041         };
2042       }
2043       if ( $cust_bill_pkg->recur != 0 ) {
2044         push @b, {
2045           'description' => "$desc (".
2046                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2047                            time2str("%x", $cust_bill_pkg->edate). ')',
2048           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2049         };
2050       }
2051
2052     }
2053
2054   }
2055
2056   @b;
2057
2058 }
2059
2060 sub _items_credits {
2061   my $self = shift;
2062
2063   my @b;
2064   #credits
2065   foreach ( $self->cust_credited ) {
2066
2067     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2068
2069     my $reason = $_->cust_credit->reason;
2070     #my $reason = substr($_->cust_credit->reason,0,32);
2071     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2072     $reason = " ($reason) " if $reason;
2073     push @b, {
2074       #'description' => 'Credit ref\#'. $_->crednum.
2075       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2076       #                 $reason,
2077       'description' => 'Credit applied '.
2078                        time2str("%x",$_->cust_credit->_date). $reason,
2079       'amount'      => sprintf("%.2f",$_->amount),
2080     };
2081   }
2082   #foreach ( @cr_cust_credit ) {
2083   #  push @buf,[
2084   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2085   #    $money_char. sprintf("%10.2f",$_->credited)
2086   #  ];
2087   #}
2088
2089   @b;
2090
2091 }
2092
2093 sub _items_payments {
2094   my $self = shift;
2095
2096   my @b;
2097   #get & print payments
2098   foreach ( $self->cust_bill_pay ) {
2099
2100     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2101
2102     push @b, {
2103       'description' => "Payment received ".
2104                        time2str("%x",$_->cust_pay->_date ),
2105       'amount'      => sprintf("%.2f", $_->amount )
2106     };
2107   }
2108
2109   @b;
2110
2111 }
2112
2113 =back
2114
2115 =head1 SUBROUTINES
2116
2117 =over 4
2118
2119 =item reprint
2120
2121 =cut
2122
2123 sub process_reprint {
2124   process_re_X('print', @_);
2125 }
2126
2127 =item reemail
2128
2129 =cut
2130
2131 sub process_reemail {
2132   process_re_X('email', @_);
2133 }
2134
2135 =item refax
2136
2137 =cut
2138
2139 sub process_refax {
2140   process_re_X('fax', @_);
2141 }
2142
2143 use Storable qw(thaw);
2144 use Data::Dumper;
2145 use MIME::Base64;
2146 sub process_re_X {
2147   my( $method, $job ) = ( shift, shift );
2148
2149   my $param = thaw(decode_base64(shift));
2150   warn Dumper($param) if $DEBUG;
2151
2152   re_X(
2153     $method,
2154     $job,
2155     %$param,
2156   );
2157
2158 }
2159
2160 sub re_X {
2161   my($method, $job, %param ) = @_;
2162 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2163
2164   #some false laziness w/search/cust_bill.html
2165   my $distinct = '';
2166   my $orderby = 'ORDER BY cust_bill._date';
2167
2168   my @where;
2169
2170   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2171     push @where, "cust_bill._date >= $1";
2172   }
2173   if ( $param{'end'} =~ /^(\d+)$/ ) {
2174     push @where, "cust_bill._date < $1";
2175   }
2176   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2177     push @where, "cust_main.agentnum = $1";
2178   }
2179
2180   my $owed =
2181     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2182                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2183              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2184                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2185
2186   push @where, "0 != $owed"
2187     if $param{'open'};
2188
2189   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2190     if $param{'days'};
2191
2192   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2193
2194   my $addl_from = 'left join cust_main using ( custnum )';
2195
2196   if ( $param{'newest_percust'} ) {
2197     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2198     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2199     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2200   }
2201      
2202   my @cust_bill = qsearch( 'cust_bill',
2203                            {},
2204                            "$distinct cust_bill.*",
2205                            $extra_sql,
2206                            '',
2207                            $addl_from
2208                          );
2209
2210   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2211   foreach my $cust_bill ( @cust_bill ) {
2212     $cust_bill->$method();
2213
2214     if ( $job ) { #progressbar foo
2215       $num++;
2216       if ( time - $min_sec > $last ) {
2217         my $error = $job->update_statustext(
2218           int( 100 * $num / scalar(@cust_bill) )
2219         );
2220         die $error if $error;
2221         $last = time;
2222       }
2223     }
2224
2225   }
2226
2227 }
2228
2229 =back
2230
2231 =head1 BUGS
2232
2233 The delete method.
2234
2235 print_text formatting (and some logic :/) is in source, but needs to be
2236 slurped in from a file.  Also number of lines ($=).
2237
2238 =head1 SEE ALSO
2239
2240 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2241 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2242 documentation.
2243
2244 =cut
2245
2246 1;
2247