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