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