add billco respooling, not re-FTPing, RT#3971
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
8 use Date::Format;
9 use Text::Template 1.20;
10 use File::Temp 0.14;
11 use String::ShellQuote;
12 use HTML::Entities;
13 use Locale::Country;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
18 use FS::cust_main;
19 use FS::cust_bill_pkg;
20 use FS::cust_credit;
21 use FS::cust_pay;
22 use FS::cust_pkg;
23 use FS::cust_credit_bill;
24 use FS::pay_batch;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
27 use FS::part_pkg;
28 use FS::cust_bill_pay;
29 use FS::cust_bill_pay_batch;
30 use FS::part_bill_event;
31 use FS::payby;
32
33 @ISA = qw( FS::cust_main_Mixin FS::Record );
34
35 $DEBUG = 0;
36 $me = '[FS::cust_bill]';
37
38 #ask FS::UID to run this stuff for us later
39 FS::UID->install_callback( sub { 
40   $conf = new FS::Conf;
41   $money_char = $conf->config('money_char') || '$';  
42 } );
43
44 =head1 NAME
45
46 FS::cust_bill - Object methods for cust_bill records
47
48 =head1 SYNOPSIS
49
50   use FS::cust_bill;
51
52   $record = new FS::cust_bill \%hash;
53   $record = new FS::cust_bill { 'column' => 'value' };
54
55   $error = $record->insert;
56
57   $error = $new_record->replace($old_record);
58
59   $error = $record->delete;
60
61   $error = $record->check;
62
63   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
64
65   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
66
67   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
68
69   @cust_pay_objects = $cust_bill->cust_pay;
70
71   $tax_amount = $record->tax;
72
73   @lines = $cust_bill->print_text;
74   @lines = $cust_bill->print_text $time;
75
76 =head1 DESCRIPTION
77
78 An FS::cust_bill object represents an invoice; a declaration that a customer
79 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
80 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
81 following fields are currently supported:
82
83 =over 4
84
85 =item invnum - primary key (assigned automatically for new invoices)
86
87 =item custnum - customer (see L<FS::cust_main>)
88
89 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
90 L<Time::Local> and L<Date::Parse> for conversion functions.
91
92 =item charged - amount of this invoice
93
94 =item printed - deprecated
95
96 =item closed - books closed flag, empty or `Y'
97
98 =back
99
100 =head1 METHODS
101
102 =over 4
103
104 =item new HASHREF
105
106 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
107 Invoices are normally created by calling the bill method of a customer object
108 (see L<FS::cust_main>).
109
110 =cut
111
112 sub table { 'cust_bill'; }
113
114 sub cust_linked { $_[0]->cust_main_custnum; } 
115 sub cust_unlinked_msg {
116   my $self = shift;
117   "WARNING: can't find cust_main.custnum ". $self->custnum.
118   ' (cust_bill.invnum '. $self->invnum. ')';
119 }
120
121 =item insert
122
123 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
124 returns the error, otherwise returns false.
125
126 =item delete
127
128 This method now works but you probably shouldn't use it.  Instead, apply a
129 credit against the invoice.
130
131 Using this method to delete invoices outright is really, really bad.  There
132 would be no record you ever posted this invoice, and there are no check to
133 make sure charged = 0 or that there are no associated cust_bill_pkg records.
134
135 Really, don't use it.
136
137 =cut
138
139 sub delete {
140   my $self = shift;
141   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
142   $self->SUPER::delete(@_);
143 }
144
145 =item replace OLD_RECORD
146
147 Replaces the OLD_RECORD with this one in the database.  If there is an error,
148 returns the error, otherwise returns false.
149
150 Only printed may be changed.  printed is normally updated by calling the
151 collect method of a customer object (see L<FS::cust_main>).
152
153 =cut
154
155 #replace can be inherited from Record.pm
156
157 # replace_check is now the preferred way to #implement replace data checks
158 # (so $object->replace() works without an argument)
159
160 sub replace_check {
161   my( $new, $old ) = ( shift, shift );
162   return "Can't change custnum!" unless $old->custnum == $new->custnum;
163   #return "Can't change _date!" unless $old->_date eq $new->_date;
164   return "Can't change _date!" unless $old->_date == $new->_date;
165   return "Can't change charged!" unless $old->charged == $new->charged
166                                      || $old->charged == 0;
167
168   '';
169 }
170
171 =item check
172
173 Checks all fields to make sure this is a valid invoice.  If there is an error,
174 returns the error, otherwise returns false.  Called by the insert and replace
175 methods.
176
177 =cut
178
179 sub check {
180   my $self = shift;
181
182   my $error =
183     $self->ut_numbern('invnum')
184     || $self->ut_number('custnum')
185     || $self->ut_numbern('_date')
186     || $self->ut_money('charged')
187     || $self->ut_numbern('printed')
188     || $self->ut_enum('closed', [ '', 'Y' ])
189   ;
190   return $error if $error;
191
192   return "Unknown customer"
193     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
194
195   $self->_date(time) unless $self->_date;
196
197   $self->printed(0) if $self->printed eq '';
198
199   $self->SUPER::check;
200 }
201
202 =item previous
203
204 Returns a list consisting of the total previous balance for this customer, 
205 followed by the previous outstanding invoices (as FS::cust_bill objects also).
206
207 =cut
208
209 sub previous {
210   my $self = shift;
211   my $total = 0;
212   my @cust_bill = sort { $a->_date <=> $b->_date }
213     grep { $_->owed != 0 && $_->_date < $self->_date }
214       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
215   ;
216   foreach ( @cust_bill ) { $total += $_->owed; }
217   $total, @cust_bill;
218 }
219
220 =item cust_bill_pkg
221
222 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
223
224 =cut
225
226 sub cust_bill_pkg {
227   my $self = shift;
228   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
229 }
230
231 =item cust_pkg
232
233 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
234 this invoice.
235
236 =cut
237
238 sub cust_pkg {
239   my $self = shift;
240   my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
241   my %saw = ();
242   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
243 }
244
245 =item open_cust_bill_pkg
246
247 Returns the open line items for this invoice.
248
249 Note that cust_bill_pkg with both setup and recur fees are returned as two
250 separate line items, each with only one fee.
251
252 =cut
253
254 # modeled after cust_main::open_cust_bill
255 sub open_cust_bill_pkg {
256   my $self = shift;
257
258   # grep { $_->owed > 0 } $self->cust_bill_pkg
259
260   my %other = ( 'recur' => 'setup',
261                 'setup' => 'recur', );
262   my @open = ();
263   foreach my $field ( qw( recur setup )) {
264     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
265                 grep { $_->owed($field) > 0 }
266                 $self->cust_bill_pkg;
267   }
268
269   @open;
270 }
271
272 =item cust_bill_event
273
274 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
275 invoice.
276
277 =cut
278
279 sub cust_bill_event {
280   my $self = shift;
281   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
282 }
283
284
285 =item cust_main
286
287 Returns the customer (see L<FS::cust_main>) for this invoice.
288
289 =cut
290
291 sub cust_main {
292   my $self = shift;
293   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
294 }
295
296 =item cust_suspend_if_balance_over AMOUNT
297
298 Suspends the customer associated with this invoice if the total amount owed on
299 this invoice and all older invoices is greater than the specified amount.
300
301 Returns a list: an empty list on success or a list of errors.
302
303 =cut
304
305 sub cust_suspend_if_balance_over {
306   my( $self, $amount ) = ( shift, shift );
307   my $cust_main = $self->cust_main;
308   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
309     return ();
310   } else {
311     $cust_main->suspend(@_);
312   }
313 }
314
315 =item cust_credit
316
317 Depreciated.  See the cust_credited method.
318
319  #Returns a list consisting of the total previous credited (see
320  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
321  #outstanding credits (FS::cust_credit objects).
322
323 =cut
324
325 sub cust_credit {
326   use Carp;
327   croak "FS::cust_bill->cust_credit depreciated; see ".
328         "FS::cust_bill->cust_credit_bill";
329   #my $self = shift;
330   #my $total = 0;
331   #my @cust_credit = sort { $a->_date <=> $b->_date }
332   #  grep { $_->credited != 0 && $_->_date < $self->_date }
333   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
334   #;
335   #foreach (@cust_credit) { $total += $_->credited; }
336   #$total, @cust_credit;
337 }
338
339 =item cust_pay
340
341 Depreciated.  See the cust_bill_pay method.
342
343 #Returns all payments (see L<FS::cust_pay>) for this invoice.
344
345 =cut
346
347 sub cust_pay {
348   use Carp;
349   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
350   #my $self = shift;
351   #sort { $a->_date <=> $b->_date }
352   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
353   #;
354 }
355
356 =item cust_bill_pay
357
358 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
359
360 =cut
361
362 sub cust_bill_pay {
363   my $self = shift;
364   sort { $a->_date <=> $b->_date }
365     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
366 }
367
368 =item cust_credited
369
370 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
371
372 =cut
373
374 sub cust_credited {
375   my $self = shift;
376   sort { $a->_date <=> $b->_date }
377     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
378   ;
379 }
380
381 =item tax
382
383 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
384
385 =cut
386
387 sub tax {
388   my $self = shift;
389   my $total = 0;
390   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
391                                              'pkgnum' => 0 } );
392   foreach (@taxlines) { $total += $_->setup; }
393   $total;
394 }
395
396 =item owed
397
398 Returns the amount owed (still outstanding) on this invoice, which is charged
399 minus all payment applications (see L<FS::cust_bill_pay>) and credit
400 applications (see L<FS::cust_credit_bill>).
401
402 =cut
403
404 sub owed {
405   my $self = shift;
406   my $balance = $self->charged;
407   $balance -= $_->amount foreach ( $self->cust_bill_pay );
408   $balance -= $_->amount foreach ( $self->cust_credited );
409   $balance = sprintf( "%.2f", $balance);
410   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
411   $balance;
412 }
413
414 =item apply_payments_and_credits
415
416 =cut
417
418 sub apply_payments_and_credits {
419   my $self = shift;
420
421   local $SIG{HUP} = 'IGNORE';
422   local $SIG{INT} = 'IGNORE';
423   local $SIG{QUIT} = 'IGNORE';
424   local $SIG{TERM} = 'IGNORE';
425   local $SIG{TSTP} = 'IGNORE';
426   local $SIG{PIPE} = 'IGNORE';
427
428   my $oldAutoCommit = $FS::UID::AutoCommit;
429   local $FS::UID::AutoCommit = 0;
430   my $dbh = dbh;
431
432   $self->select_for_update; #mutex
433
434   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
435   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
436
437   while ( $self->owed > 0 and ( @payments || @credits ) ) {
438
439     my $app = '';
440     if ( @payments && @credits ) {
441
442       #decide which goes first by weight of top (unapplied) line item
443
444       my @open_lineitems = $self->open_cust_bill_pkg;
445
446       my $max_pay_weight =
447         max( map  { $_->part_pkg->pay_weight || 0 }
448              grep { $_ }
449              map  { $_->cust_pkg }
450                   @open_lineitems
451            );
452       my $max_credit_weight =
453         max( map  { $_->part_pkg->credit_weight || 0 }
454              grep { $_ } 
455              map  { $_->cust_pkg }
456                   @open_lineitems
457            );
458
459       #if both are the same... payments first?  it has to be something
460       if ( $max_pay_weight >= $max_credit_weight ) {
461         $app = 'pay';
462       } else {
463         $app = 'credit';
464       }
465     
466     } elsif ( @payments ) {
467       $app = 'pay';
468     } elsif ( @credits ) {
469       $app = 'credit';
470     } else {
471       die "guru meditation #12 and 35";
472     }
473
474     if ( $app eq 'pay' ) {
475
476       my $payment = shift @payments;
477
478       $app = new FS::cust_bill_pay {
479         'paynum'  => $payment->paynum,
480         'amount'  => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
481       };
482
483     } elsif ( $app eq 'credit' ) {
484
485       my $credit = shift @credits;
486
487       $app = new FS::cust_credit_bill {
488         'crednum' => $credit->crednum,
489         'amount'  => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
490       };
491
492     } else {
493       die "guru meditation #12 and 35";
494     }
495
496     $app->invnum( $self->invnum );
497
498     my $error = $app->insert;
499     if ( $error ) {
500       $dbh->rollback if $oldAutoCommit;
501       return "Error inserting ". $app->table. " record: $error";
502     }
503     die $error if $error;
504
505   }
506
507   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
508   ''; #no error
509
510 }
511
512 =item generate_email PARAMHASH
513
514 PARAMHASH can contain the following:
515
516 =over 4
517
518 =item from       => sender address, required
519
520 =item tempate    => alternate template name, optional
521
522 =item print_text => text attachment arrayref, optional
523
524 =item subject    => email subject, optional
525
526 =back
527
528 Returns an argument list to be passed to L<FS::Misc::send_email>.
529
530 =cut
531
532 use MIME::Entity;
533
534 sub generate_email {
535
536   my $self = shift;
537   my %args = @_;
538
539   my $me = '[FS::cust_bill::generate_email]';
540
541   my %return = (
542     'from'      => $args{'from'},
543     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
544   );
545
546   if (ref($args{'to'}) eq 'ARRAY') {
547     $return{'to'} = $args{'to'};
548   } else {
549     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
550                            $self->cust_main->invoicing_list
551                     ];
552   }
553
554   if ( $conf->exists('invoice_html') ) {
555
556     warn "$me creating HTML/text multipart message"
557       if $DEBUG;
558
559     $return{'nobody'} = 1;
560
561     my $alternative = build MIME::Entity
562       'Type'        => 'multipart/alternative',
563       'Encoding'    => '7bit',
564       'Disposition' => 'inline'
565     ;
566
567     my $data;
568     if ( $conf->exists('invoice_email_pdf')
569          and scalar($conf->config('invoice_email_pdf_note')) ) {
570
571       warn "$me using 'invoice_email_pdf_note' in multipart message"
572         if $DEBUG;
573       $data = [ map { $_ . "\n" }
574                     $conf->config('invoice_email_pdf_note')
575               ];
576
577     } else {
578
579       warn "$me not using 'invoice_email_pdf_note' in multipart message"
580         if $DEBUG;
581       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
582         $data = $args{'print_text'};
583       } else {
584         $data = [ $self->print_text('', $args{'template'}) ];
585       }
586
587     }
588
589     $alternative->attach(
590       'Type'        => 'text/plain',
591       #'Encoding'    => 'quoted-printable',
592       'Encoding'    => '7bit',
593       'Data'        => $data,
594       'Disposition' => 'inline',
595     );
596
597     $args{'from'} =~ /\@([\w\.\-]+)/;
598     my $from = $1 || 'example.com';
599     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
600
601     my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
602     my $file;
603     if ( defined($args{'template'}) && length($args{'template'})
604          && -e "$path/logo_". $args{'template'}. ".png"
605        )
606     {
607       $file = "$path/logo_". $args{'template'}. ".png";
608     } else {
609       $file = "$path/logo.png";
610     }
611
612     my $image = build MIME::Entity
613       'Type'       => 'image/png',
614       'Encoding'   => 'base64',
615       'Path'       => $file,
616       'Filename'   => 'logo.png',
617       'Content-ID' => "<$content_id>",
618     ;
619
620     $alternative->attach(
621       'Type'        => 'text/html',
622       'Encoding'    => 'quoted-printable',
623       'Data'        => [ '<html>',
624                          '  <head>',
625                          '    <title>',
626                          '      '. encode_entities($return{'subject'}), 
627                          '    </title>',
628                          '  </head>',
629                          '  <body bgcolor="#e8e8e8">',
630                          $self->print_html('', $args{'template'}, $content_id),
631                          '  </body>',
632                          '</html>',
633                        ],
634       'Disposition' => 'inline',
635       #'Filename'    => 'invoice.pdf',
636     );
637
638     if ( $conf->exists('invoice_email_pdf') ) {
639
640       #attaching pdf too:
641       # multipart/mixed
642       #   multipart/related
643       #     multipart/alternative
644       #       text/plain
645       #       text/html
646       #     image/png
647       #   application/pdf
648
649       my $related = build MIME::Entity 'Type'     => 'multipart/related',
650                                        'Encoding' => '7bit';
651
652       #false laziness w/Misc::send_email
653       $related->head->replace('Content-type',
654         $related->mime_type.
655         '; boundary="'. $related->head->multipart_boundary. '"'.
656         '; type=multipart/alternative'
657       );
658
659       $related->add_part($alternative);
660
661       $related->add_part($image);
662
663       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
664
665       $return{'mimeparts'} = [ $related, $pdf ];
666
667     } else {
668
669       #no other attachment:
670       # multipart/related
671       #   multipart/alternative
672       #     text/plain
673       #     text/html
674       #   image/png
675
676       $return{'content-type'} = 'multipart/related';
677       $return{'mimeparts'} = [ $alternative, $image ];
678       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
679       #$return{'disposition'} = 'inline';
680
681     }
682   
683   } else {
684
685     if ( $conf->exists('invoice_email_pdf') ) {
686       warn "$me creating PDF attachment"
687         if $DEBUG;
688
689       #mime parts arguments a la MIME::Entity->build().
690       $return{'mimeparts'} = [
691         { $self->mimebuild_pdf('', $args{'template'}) }
692       ];
693     }
694   
695     if ( $conf->exists('invoice_email_pdf')
696          and scalar($conf->config('invoice_email_pdf_note')) ) {
697
698       warn "$me using 'invoice_email_pdf_note'"
699         if $DEBUG;
700       $return{'body'} = [ map { $_ . "\n" }
701                               $conf->config('invoice_email_pdf_note')
702                         ];
703
704     } else {
705
706       warn "$me not using 'invoice_email_pdf_note'"
707         if $DEBUG;
708       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
709         $return{'body'} = $args{'print_text'};
710       } else {
711         $return{'body'} = [ $self->print_text('', $args{'template'}) ];
712       }
713
714     }
715
716   }
717
718   %return;
719
720 }
721
722 =item mimebuild_pdf
723
724 Returns a list suitable for passing to MIME::Entity->build(), representing
725 this invoice as PDF attachment.
726
727 =cut
728
729 sub mimebuild_pdf {
730   my $self = shift;
731   (
732     'Type'        => 'application/pdf',
733     'Encoding'    => 'base64',
734     'Data'        => [ $self->print_pdf(@_) ],
735     'Disposition' => 'attachment',
736     'Filename'    => 'invoice.pdf',
737   );
738 }
739
740 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
741
742 Sends this invoice to the destinations configured for this customer: sends
743 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
744
745 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
746
747 AGENTNUM, if specified, means that this invoice will only be sent for customers
748 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
749 single agent) or an arrayref of agentnums.
750
751 INVOICE_FROM, if specified, overrides the default email invoice From: address.
752
753 AMOUNT, if specified, only sends the invoice if the total amount owed on this
754 invoice and all older invoices is greater than the specified amount.
755
756 =cut
757
758 sub queueable_send {
759   my %opt = @_;
760
761   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
762     or die "invalid invoice number: " . $opt{invnum};
763
764   my @args = ( $opt{template}, $opt{agentnum} );
765   push @args, $opt{invoice_from}
766     if exists($opt{invoice_from}) && $opt{invoice_from};
767
768   my $error = $self->send( @args );
769   die $error if $error;
770
771 }
772
773 sub send {
774   my $self = shift;
775   my $template = scalar(@_) ? shift : '';
776   if ( scalar(@_) && $_[0]  ) {
777     my $agentnums = ref($_[0]) ? shift : [ shift ];
778     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
779   }
780
781   my $invoice_from =
782     scalar(@_)
783       ? shift
784       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
785
786   my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
787
788   return ''
789     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
790
791   my @invoicing_list = $self->cust_main->invoicing_list;
792
793   #$self->email_invoice($template, $invoice_from)
794   $self->email($template, $invoice_from)
795     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
796
797   #$self->print_invoice($template)
798   $self->print($template)
799     if grep { $_ eq 'POST' } @invoicing_list; #postal
800
801   $self->fax_invoice($template)
802     if grep { $_ eq 'FAX' } @invoicing_list; #fax
803
804   '';
805
806 }
807
808 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
809
810 Emails this invoice.
811
812 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
813
814 INVOICE_FROM, if specified, overrides the default email invoice From: address.
815
816 =cut
817
818 sub queueable_email {
819   my %opt = @_;
820
821   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
822     or die "invalid invoice number: " . $opt{invnum};
823
824   my @args = ( $opt{template} );
825   push @args, $opt{invoice_from}
826     if exists($opt{invoice_from}) && $opt{invoice_from};
827
828   my $error = $self->email( @args );
829   die $error if $error;
830
831 }
832
833 #sub email_invoice {
834 sub email {
835   my $self = shift;
836   my $template = scalar(@_) ? shift : '';
837   my $invoice_from =
838     scalar(@_)
839       ? shift
840       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
841
842   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
843                             $self->cust_main->invoicing_list;
844
845   #better to notify this person than silence
846   @invoicing_list = ($invoice_from) unless @invoicing_list;
847
848   my $error = send_email(
849     $self->generate_email(
850       'from'       => $invoice_from,
851       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
852       'template'   => $template,
853     )
854   );
855   die "can't email invoice: $error\n" if $error;
856   #die "$error\n" if $error;
857
858 }
859
860 =item lpr_data [ TEMPLATENAME ]
861
862 Returns the postscript or plaintext for this invoice as an arrayref.
863
864 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
865
866 =cut
867
868 sub lpr_data {
869   my( $self, $template) = @_;
870   $conf->exists('invoice_latex')
871     ? [ $self->print_ps('', $template) ]
872     : [ $self->print_text('', $template) ];
873 }
874
875 =item print [ TEMPLATENAME ]
876
877 Prints this invoice.
878
879 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
880
881 =cut
882
883 #sub print_invoice {
884 sub print {
885   my $self = shift;
886   my $template = scalar(@_) ? shift : '';
887
888   do_print $self->lpr_data($template);
889 }
890
891 =item fax_invoice [ TEMPLATENAME ] 
892
893 Faxes this invoice.
894
895 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
896
897 =cut
898
899 sub fax_invoice {
900   my $self = shift;
901   my $template = scalar(@_) ? shift : '';
902
903   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
904     unless $conf->exists('invoice_latex');
905
906   my $dialstring = $self->cust_main->getfield('fax');
907   #Check $dialstring?
908
909   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
910                         'dialstring' => $dialstring,
911                       );
912   die $error if $error;
913
914 }
915
916 =item ftp_invoice [ TEMPLATENAME ] 
917
918 Sends this invoice data via FTP.
919
920 TEMPLATENAME is unused?
921
922 =cut
923
924 sub ftp_invoice {
925   my $self = shift;
926   my $template = scalar(@_) ? shift : '';
927
928   $self->send_csv(
929     'protocol'   => 'ftp',
930     'server'     => $conf->config('cust_bill-ftpserver'),
931     'username'   => $conf->config('cust_bill-ftpusername'),
932     'password'   => $conf->config('cust_bill-ftppassword'),
933     'dir'        => $conf->config('cust_bill-ftpdir'),
934     'format'     => $conf->config('cust_bill-ftpformat'),
935   );
936 }
937
938 =item spool_invoice [ TEMPLATENAME ] 
939
940 Spools this invoice data (see L<FS::spool_csv>)
941
942 TEMPLATENAME is unused?
943
944 =cut
945
946 sub spool_invoice {
947   my $self = shift;
948   my $template = scalar(@_) ? shift : '';
949
950   $self->spool_csv(
951     'format'       => $conf->config('cust_bill-spoolformat'),
952     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
953   );
954 }
955
956 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
957
958 Like B<send>, but only sends the invoice if it is the newest open invoice for
959 this customer.
960
961 =cut
962
963 sub send_if_newest {
964   my $self = shift;
965
966   return ''
967     if scalar(
968                grep { $_->owed > 0 } 
969                     qsearch('cust_bill', {
970                       'custnum' => $self->custnum,
971                       #'_date'   => { op=>'>', value=>$self->_date },
972                       'invnum'  => { op=>'>', value=>$self->invnum },
973                     } )
974              );
975     
976   $self->send(@_);
977 }
978
979 =item send_csv OPTION => VALUE, ...
980
981 Sends invoice as a CSV data-file to a remote host with the specified protocol.
982
983 Options are:
984
985 protocol - currently only "ftp"
986 server
987 username
988 password
989 dir
990
991 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
992 and YYMMDDHHMMSS is a timestamp.
993
994 See L</print_csv> for a description of the output format.
995
996 =cut
997
998 sub send_csv {
999   my($self, %opt) = @_;
1000
1001   #create file(s)
1002
1003   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1004   mkdir $spooldir, 0700 unless -d $spooldir;
1005
1006   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1007   my $file = "$spooldir/$tracctnum.csv";
1008   
1009   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1010
1011   open(CSV, ">$file") or die "can't open $file: $!";
1012   print CSV $header;
1013
1014   print CSV $detail;
1015
1016   close CSV;
1017
1018   my $net;
1019   if ( $opt{protocol} eq 'ftp' ) {
1020     eval "use Net::FTP;";
1021     die $@ if $@;
1022     $net = Net::FTP->new($opt{server}) or die @$;
1023   } else {
1024     die "unknown protocol: $opt{protocol}";
1025   }
1026
1027   $net->login( $opt{username}, $opt{password} )
1028     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1029
1030   $net->binary or die "can't set binary mode";
1031
1032   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1033
1034   $net->put($file) or die "can't put $file: $!";
1035
1036   $net->quit;
1037
1038   unlink $file;
1039
1040 }
1041
1042 =item spool_csv
1043
1044 Spools CSV invoice data.
1045
1046 Options are:
1047
1048 =over 4
1049
1050 =item format - 'default' or 'billco'
1051
1052 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1053
1054 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1055
1056 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1057
1058 =back
1059
1060 =cut
1061
1062 sub spool_csv {
1063   my($self, %opt) = @_;
1064
1065   my $cust_main = $self->cust_main;
1066
1067   if ( $opt{'dest'} ) {
1068     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1069                              $cust_main->invoicing_list;
1070     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1071                      || ! keys %invoicing_list;
1072   }
1073
1074   if ( $opt{'balanceover'} ) {
1075     return 'N/A'
1076       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1077   }
1078
1079   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1080   mkdir $spooldir, 0700 unless -d $spooldir;
1081
1082   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1083
1084   my $file =
1085     "$spooldir/".
1086     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1087     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1088     '.csv';
1089   
1090   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1091
1092   open(CSV, ">>$file") or die "can't open $file: $!";
1093   flock(CSV, LOCK_EX);
1094   seek(CSV, 0, 2);
1095
1096   print CSV $header;
1097
1098   if ( lc($opt{'format'}) eq 'billco' ) {
1099
1100     flock(CSV, LOCK_UN);
1101     close CSV;
1102
1103     $file =
1104       "$spooldir/".
1105       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1106       '-detail.csv';
1107
1108     open(CSV,">>$file") or die "can't open $file: $!";
1109     flock(CSV, LOCK_EX);
1110     seek(CSV, 0, 2);
1111   }
1112
1113   print CSV $detail;
1114
1115   flock(CSV, LOCK_UN);
1116   close CSV;
1117
1118   return '';
1119
1120 }
1121
1122 =item print_csv OPTION => VALUE, ...
1123
1124 Returns CSV data for this invoice.
1125
1126 Options are:
1127
1128 format - 'default' or 'billco'
1129
1130 Returns a list consisting of two scalars.  The first is a single line of CSV
1131 header information for this invoice.  The second is one or more lines of CSV
1132 detail information for this invoice.
1133
1134 If I<format> is not specified or "default", the fields of the CSV file are as
1135 follows:
1136
1137 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1138
1139 =over 4
1140
1141 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1142
1143 B<record_type> is C<cust_bill> for the initial header line only.  The
1144 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1145 fields are filled in.
1146
1147 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1148 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1149 are filled in.
1150
1151 =item invnum - invoice number
1152
1153 =item custnum - customer number
1154
1155 =item _date - invoice date
1156
1157 =item charged - total invoice amount
1158
1159 =item first - customer first name
1160
1161 =item last - customer first name
1162
1163 =item company - company name
1164
1165 =item address1 - address line 1
1166
1167 =item address2 - address line 1
1168
1169 =item city
1170
1171 =item state
1172
1173 =item zip
1174
1175 =item country
1176
1177 =item pkg - line item description
1178
1179 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1180
1181 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1182
1183 =item sdate - start date for recurring fee
1184
1185 =item edate - end date for recurring fee
1186
1187 =back
1188
1189 If I<format> is "billco", the fields of the header CSV file are as follows:
1190
1191   +-------------------------------------------------------------------+
1192   |                        FORMAT HEADER FILE                         |
1193   |-------------------------------------------------------------------|
1194   | Field | Description                   | Name       | Type | Width |
1195   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1196   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1197   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1198   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1199   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1200   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1201   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1202   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1203   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1204   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1205   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1206   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1207   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1208   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1209   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1210   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1211   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1212   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1213   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1214   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1215   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1216   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1217   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1218   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1219   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1220   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1221   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1222   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1223   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1224   +-------+-------------------------------+------------+------+-------+
1225
1226 If I<format> is "billco", the fields of the detail CSV file are as follows:
1227
1228                                   FORMAT FOR DETAIL FILE
1229         |                            |           |      |
1230   Field | Description                | Name      | Type | Width
1231   1     | N/A-Leave Empty            | RC        | CHAR |     2
1232   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1233   3     | Account Number             | TRACCTNUM | CHAR |    15
1234   4     | Invoice Number             | TRINVOICE | CHAR |    15
1235   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1236   6     | Transaction Detail         | DETAILS   | CHAR |   100
1237   7     | Amount                     | AMT       | NUM* |     9
1238   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1239   9     | Grouping Code              | GROUP     | CHAR |     2
1240   10    | User Defined               | ACCT CODE | CHAR |    15
1241
1242 =cut
1243
1244 sub print_csv {
1245   my($self, %opt) = @_;
1246   
1247   eval "use Text::CSV_XS";
1248   die $@ if $@;
1249
1250   my $cust_main = $self->cust_main;
1251
1252   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1253
1254   if ( lc($opt{'format'}) eq 'billco' ) {
1255
1256     my $taxtotal = 0;
1257     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1258
1259     my $duedate = $self->balance_due_date;
1260
1261     my( $previous_balance, @unused ) = $self->previous; #previous balance
1262
1263     my $pmt_cr_applied = 0;
1264     $pmt_cr_applied += $_->{'amount'}
1265       foreach ( $self->_items_payments, $self->_items_credits ) ;
1266
1267     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1268
1269     $csv->combine(
1270       '',                         #  1 | N/A-Leave Empty               CHAR   2
1271       '',                         #  2 | N/A-Leave Empty               CHAR  15
1272       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1273       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1274       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1275       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1276       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1277       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1278       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1279       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1280       '',                         # 10 | Ancillary Billing Information CHAR  30
1281       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1282       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1283
1284       # XXX ?
1285       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1286
1287       # XXX ?
1288       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1289
1290       $previous_balance,          # 15 | Previous Balance              NUM*   9
1291       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1292       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1293       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1294       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1295       '',                         # 20 | 30 Day Aging                  NUM*   9
1296       '',                         # 21 | 60 Day Aging                  NUM*   9
1297       '',                         # 22 | 90 Day Aging                  NUM*   9
1298       'N',                        # 23 | Y/N                           CHAR   1
1299       '',                         # 24 | Remittance automation         CHAR 100
1300       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1301       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1302       '0',                        # 27 | Federal Tax***                NUM*   9
1303       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1304       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1305     );
1306
1307   } else {
1308   
1309     $csv->combine(
1310       'cust_bill',
1311       $self->invnum,
1312       $self->custnum,
1313       time2str("%x", $self->_date),
1314       sprintf("%.2f", $self->charged),
1315       ( map { $cust_main->getfield($_) }
1316           qw( first last company address1 address2 city state zip country ) ),
1317       map { '' } (1..5),
1318     ) or die "can't create csv";
1319   }
1320
1321   my $header = $csv->string. "\n";
1322
1323   my $detail = '';
1324   if ( lc($opt{'format'}) eq 'billco' ) {
1325
1326     my $lineseq = 0;
1327     foreach my $item ( $self->_items_pkg ) {
1328
1329       $csv->combine(
1330         '',                     #  1 | N/A-Leave Empty            CHAR   2
1331         '',                     #  2 | N/A-Leave Empty            CHAR  15
1332         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1333         $self->invnum,          #  4 | Invoice Number             CHAR  15
1334         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1335         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1336         $item->{'amount'},      #  7 | Amount                     NUM*   9
1337         '',                     #  8 | Line Format Control**      CHAR   2
1338         '',                     #  9 | Grouping Code              CHAR   2
1339         '',                     # 10 | User Defined               CHAR  15
1340       );
1341
1342       $detail .= $csv->string. "\n";
1343
1344     }
1345
1346   } else {
1347
1348     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1349
1350       my($pkg, $setup, $recur, $sdate, $edate);
1351       if ( $cust_bill_pkg->pkgnum ) {
1352       
1353         ($pkg, $setup, $recur, $sdate, $edate) = (
1354           $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1355           ( $cust_bill_pkg->setup != 0
1356             ? sprintf("%.2f", $cust_bill_pkg->setup )
1357             : '' ),
1358           ( $cust_bill_pkg->recur != 0
1359             ? sprintf("%.2f", $cust_bill_pkg->recur )
1360             : '' ),
1361           ( $cust_bill_pkg->sdate 
1362             ? time2str("%x", $cust_bill_pkg->sdate)
1363             : '' ),
1364           ($cust_bill_pkg->edate 
1365             ?time2str("%x", $cust_bill_pkg->edate)
1366             : '' ),
1367         );
1368   
1369       } else { #pkgnum tax
1370         next unless $cust_bill_pkg->setup != 0;
1371         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1372                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1373                          : 'Tax';
1374         ($pkg, $setup, $recur, $sdate, $edate) =
1375           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1376       }
1377   
1378       $csv->combine(
1379         'cust_bill_pkg',
1380         $self->invnum,
1381         ( map { '' } (1..11) ),
1382         ($pkg, $setup, $recur, $sdate, $edate)
1383       ) or die "can't create csv";
1384
1385       $detail .= $csv->string. "\n";
1386
1387     }
1388
1389   }
1390
1391   ( $header, $detail );
1392
1393 }
1394
1395 =item comp
1396
1397 Pays this invoice with a compliemntary payment.  If there is an error,
1398 returns the error, otherwise returns false.
1399
1400 =cut
1401
1402 sub comp {
1403   my $self = shift;
1404   my $cust_pay = new FS::cust_pay ( {
1405     'invnum'   => $self->invnum,
1406     'paid'     => $self->owed,
1407     '_date'    => '',
1408     'payby'    => 'COMP',
1409     'payinfo'  => $self->cust_main->payinfo,
1410     'paybatch' => '',
1411   } );
1412   $cust_pay->insert;
1413 }
1414
1415 =item realtime_card
1416
1417 Attempts to pay this invoice with a credit card payment via a
1418 Business::OnlinePayment realtime gateway.  See
1419 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1420 for supported processors.
1421
1422 =cut
1423
1424 sub realtime_card {
1425   my $self = shift;
1426   $self->realtime_bop( 'CC', @_ );
1427 }
1428
1429 =item realtime_ach
1430
1431 Attempts to pay this invoice with an electronic check (ACH) payment via a
1432 Business::OnlinePayment realtime gateway.  See
1433 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1434 for supported processors.
1435
1436 =cut
1437
1438 sub realtime_ach {
1439   my $self = shift;
1440   $self->realtime_bop( 'ECHECK', @_ );
1441 }
1442
1443 =item realtime_lec
1444
1445 Attempts to pay this invoice with phone bill (LEC) payment via a
1446 Business::OnlinePayment realtime gateway.  See
1447 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1448 for supported processors.
1449
1450 =cut
1451
1452 sub realtime_lec {
1453   my $self = shift;
1454   $self->realtime_bop( 'LEC', @_ );
1455 }
1456
1457 sub realtime_bop {
1458   my( $self, $method ) = @_;
1459
1460   my $cust_main = $self->cust_main;
1461   my $balance = $cust_main->balance;
1462   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1463   $amount = sprintf("%.2f", $amount);
1464   return "not run (balance $balance)" unless $amount > 0;
1465
1466   my $description = 'Internet Services';
1467   if ( $conf->exists('business-onlinepayment-description') ) {
1468     my $dtempl = $conf->config('business-onlinepayment-description');
1469
1470     my $agent_obj = $cust_main->agent
1471       or die "can't retreive agent for $cust_main (agentnum ".
1472              $cust_main->agentnum. ")";
1473     my $agent = $agent_obj->agent;
1474     my $pkgs = join(', ',
1475       map { $_->cust_pkg->part_pkg->pkg }
1476         grep { $_->pkgnum } $self->cust_bill_pkg
1477     );
1478     $description = eval qq("$dtempl");
1479   }
1480
1481   $cust_main->realtime_bop($method, $amount,
1482     'description' => $description,
1483     'invnum'      => $self->invnum,
1484   );
1485
1486 }
1487
1488 =item batch_card OPTION => VALUE...
1489
1490 Adds a payment for this invoice to the pending credit card batch (see
1491 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1492 runs the payment using a realtime gateway.
1493
1494 =cut
1495
1496 sub batch_card {
1497   my ($self, %options) = @_;
1498   my $cust_main = $self->cust_main;
1499
1500   $options{invnum} = $self->invnum;
1501   
1502   $cust_main->batch_card(%options);
1503 }
1504
1505 sub _agent_template {
1506   my $self = shift;
1507   $self->cust_main->agent_template;
1508 }
1509
1510 sub _agent_invoice_from {
1511   my $self = shift;
1512   $self->cust_main->agent_invoice_from;
1513 }
1514
1515 =item print_text [ TIME [ , TEMPLATE ] ]
1516
1517 Returns an text invoice, as a list of lines.
1518
1519 TIME an optional value used to control the printing of overdue messages.  The
1520 default is now.  It isn't the date of the invoice; that's the `_date' field.
1521 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1522 L<Time::Local> and L<Date::Parse> for conversion functions.
1523
1524 =cut
1525
1526 #still some false laziness w/_items stuff (and send_csv)
1527 sub print_text {
1528
1529   my( $self, $today, $template ) = @_;
1530   $today ||= time;
1531
1532 #  my $invnum = $self->invnum;
1533   my $cust_main = $self->cust_main;
1534   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1535     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1536
1537   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1538 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1539   #my $balance_due = $self->owed + $pr_total - $cr_total;
1540   my $balance_due = $self->owed + $pr_total;
1541
1542   #my @collect = ();
1543   #my($description,$amount);
1544   @buf = ();
1545
1546   #previous balance
1547   unless ($conf->exists('disable_previous_balance')) {
1548     foreach ( @pr_cust_bill ) {
1549       push @buf, [
1550         "Previous Balance, Invoice #". $_->invnum. 
1551                    " (". time2str("%x",$_->_date). ")",
1552         $money_char. sprintf("%10.2f",$_->owed)
1553       ];
1554     }
1555     if (@pr_cust_bill) {
1556       push @buf,['','-----------'];
1557       push @buf,[ 'Total Previous Balance',
1558                   $money_char. sprintf("%10.2f",$pr_total ) ];
1559       push @buf,['',''];
1560     }
1561   }
1562
1563   #new charges
1564   foreach my $cust_bill_pkg (
1565     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1566     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1567   ) {
1568
1569     my $desc = $cust_bill_pkg->desc;
1570
1571     if ( $cust_bill_pkg->pkgnum > 0 ) {
1572
1573       if ( $cust_bill_pkg->setup != 0 ) {
1574         my $description = $desc;
1575         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1576         push @buf, [ $description,
1577                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1578         push @buf,
1579           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1580               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1581       }
1582
1583       if ( $cust_bill_pkg->recur != 0 ) {
1584         push @buf, [
1585           $desc .
1586             ( $conf->exists('disable_line_item_date_ranges')
1587               ? ''
1588               : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1589                        time2str("%x", $cust_bill_pkg->edate) . ")"
1590             ),
1591           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1592         ];
1593         push @buf,
1594           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1595               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1596                                                   $cust_bill_pkg->sdate );
1597       }
1598
1599       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1600
1601     } else { #pkgnum tax or one-shot line item
1602
1603       if ( $cust_bill_pkg->setup != 0 ) {
1604         push @buf, [ $desc,
1605                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1606       }
1607       if ( $cust_bill_pkg->recur != 0 ) {
1608         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1609                               . time2str("%x", $cust_bill_pkg->edate). ")",
1610                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1611                    ];
1612       }
1613
1614     }
1615
1616   }
1617
1618   push @buf,['','-----------'];
1619   push @buf,[ ( $conf->exists('disable_previous_balance')
1620                 ? 'Total Charges'
1621                 : 'Total New Charges'),
1622              $money_char. sprintf("%10.2f",$self->charged) ];
1623   push @buf,['',''];
1624
1625   unless ($conf->exists('disable_previous_balance')) {
1626     push @buf,['','-----------'];
1627     push @buf,['Total Charges',
1628                $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1629     push @buf,['',''];
1630
1631     #credits
1632     foreach ( $self->cust_credited ) {
1633
1634       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1635
1636       my $reason = substr($_->cust_credit->reason,0,32);
1637       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1638       $reason = " ($reason) " if $reason;
1639       push @buf,[
1640         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1641           $reason,
1642         $money_char. sprintf("%10.2f",$_->amount)
1643       ];
1644     }
1645     #foreach ( @cr_cust_credit ) {
1646     #  push @buf,[
1647     #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1648     #    $money_char. sprintf("%10.2f",$_->credited)
1649     #  ];
1650     #}
1651
1652     #get & print payments
1653     foreach ( $self->cust_bill_pay ) {
1654
1655       #something more elaborate if $_->amount ne ->cust_pay->paid ?
1656
1657       push @buf,[
1658         "Payment received ". time2str("%x",$_->cust_pay->_date ),
1659         $money_char. sprintf("%10.2f",$_->amount )
1660       ];
1661     }
1662
1663     #balance due
1664     my $balance_due_msg = $self->balance_due_msg;
1665
1666     push @buf,['','-----------'];
1667     push @buf,[$balance_due_msg, $money_char. 
1668       sprintf("%10.2f", $balance_due ) ];
1669   }
1670
1671   #create the template
1672   $template ||= $self->_agent_template;
1673   my $templatefile = 'invoice_template';
1674   $templatefile .= "_$template" if length($template);
1675   my @invoice_template = $conf->config($templatefile)
1676     or die "cannot load config file $templatefile";
1677   $invoice_lines = 0;
1678   my $wasfunc = 0;
1679   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1680     /invoice_lines\((\d*)\)/;
1681     $invoice_lines += $1 || scalar(@buf);
1682     $wasfunc=1;
1683   }
1684   die "no invoice_lines() functions in template?" unless $wasfunc;
1685   my $invoice_template = new Text::Template (
1686     TYPE   => 'ARRAY',
1687     SOURCE => [ map "$_\n", @invoice_template ],
1688   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1689   $invoice_template->compile()
1690     or die "can't compile template: $Text::Template::ERROR";
1691
1692   #setup template variables
1693   package FS::cust_bill::_template; #!
1694   use vars qw( $custnum $invnum $date $agent @address $overdue
1695                $page $total_pages @buf );
1696
1697   $custnum = $self->custnum;
1698   $invnum = $self->invnum;
1699   $date = $self->_date;
1700   $agent = $self->cust_main->agent->agent;
1701   $page = 1;
1702
1703   if ( $FS::cust_bill::invoice_lines ) {
1704     $total_pages =
1705       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1706     $total_pages++
1707       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1708   } else {
1709     $total_pages = 1;
1710   }
1711
1712   #format address (variable for the template)
1713   my $l = 0;
1714   @address = ( '', '', '', '', '', '' );
1715   package FS::cust_bill; #!
1716   $FS::cust_bill::_template::address[$l++] =
1717     $cust_main->payname.
1718       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1719         ? " (P.O. #". $cust_main->payinfo. ")"
1720         : ''
1721       )
1722   ;
1723   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1724     if $cust_main->company;
1725   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1726   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1727     if $cust_main->address2;
1728   $FS::cust_bill::_template::address[$l++] =
1729     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1730
1731   my $countrydefault = $conf->config('countrydefault') || 'US';
1732   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1733     unless $cust_main->country eq $countrydefault;
1734
1735         #  #overdue? (variable for the template)
1736         #  $FS::cust_bill::_template::overdue = ( 
1737         #    $balance_due > 0
1738         #    && $today > $self->_date 
1739         ##    && $self->printed > 1
1740         #    && $self->printed > 0
1741         #  );
1742
1743   #and subroutine for the template
1744   sub FS::cust_bill::_template::invoice_lines {
1745     my $lines = shift || scalar(@buf);
1746     map { 
1747       scalar(@buf) ? shift @buf : [ '', '' ];
1748     }
1749     ( 1 .. $lines );
1750   }
1751
1752   #and fill it in
1753   $FS::cust_bill::_template::page = 1;
1754   my $lines;
1755   my @collect;
1756   while (@buf) {
1757     push @collect, split("\n",
1758       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1759     );
1760     $FS::cust_bill::_template::page++;
1761   }
1762
1763   map "$_\n", @collect;
1764
1765 }
1766
1767 =item print_latex [ TIME [ , TEMPLATE ] ]
1768
1769 Internal method - returns a filename of a filled-in LaTeX template for this
1770 invoice (Note: add ".tex" to get the actual filename).
1771
1772 See print_ps and print_pdf for methods that return PostScript and PDF output.
1773
1774 TIME an optional value used to control the printing of overdue messages.  The
1775 default is now.  It isn't the date of the invoice; that's the `_date' field.
1776 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1777 L<Time::Local> and L<Date::Parse> for conversion functions.
1778
1779 =cut
1780
1781 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1782 sub print_latex {
1783
1784   my( $self, $today, $template ) = @_;
1785   $today ||= time;
1786   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1787     if $DEBUG;
1788
1789   my $cust_main = $self->cust_main;
1790   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1791     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1792
1793   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1794 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1795   #my $balance_due = $self->owed + $pr_total - $cr_total;
1796   my $balance_due = $self->owed + $pr_total;
1797
1798   #create the template
1799   $template ||= $self->_agent_template;
1800   my $templatefile = 'invoice_latex';
1801   my $suffix = length($template) ? "_$template" : '';
1802   $templatefile .= $suffix;
1803   my @invoice_template = map "$_\n", $conf->config($templatefile)
1804     or die "cannot load config file $templatefile";
1805
1806   my($format, $text_template);
1807   if ( grep { /^%%Detail/ } @invoice_template ) {
1808     #change this to a die when the old code is removed
1809     warn "old-style invoice template $templatefile; ".
1810          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1811     $format = 'old';
1812   } else {
1813     $format = 'Text::Template';
1814     $text_template = new Text::Template(
1815       TYPE => 'ARRAY',
1816       SOURCE => \@invoice_template,
1817       DELIMITERS => [ '[@--', '--@]' ],
1818     );
1819
1820     $text_template->compile()
1821       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1822   }
1823
1824   my $returnaddress;
1825   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1826     $returnaddress = join("\n",
1827       $conf->config_orbase('invoice_latexreturnaddress', $template)
1828     );
1829   } else {
1830     $returnaddress = '~';
1831   }
1832
1833   my %invoice_data = (
1834     'custnum'      => $self->custnum,
1835     'invnum'       => $self->invnum,
1836     'date'         => time2str('%b %o, %Y', $self->_date),
1837     'today'        => time2str('%b %o, %Y', $today),
1838     'agent'        => _latex_escape($cust_main->agent->agent),
1839     'agent_custid' => _latex_escape($cust_main->agent_custid),
1840     'payname'      => _latex_escape($cust_main->payname),
1841     'company'      => _latex_escape($cust_main->company),
1842     'address1'     => _latex_escape($cust_main->address1),
1843     'address2'     => _latex_escape($cust_main->address2),
1844     'city'         => _latex_escape($cust_main->city),
1845     'state'        => _latex_escape($cust_main->state),
1846     #'quantity'     => 1,
1847     'zip'          => _latex_escape($cust_main->zip),
1848     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1849     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1850     'returnaddress' => $returnaddress,
1851     'quantity'     => 1,
1852     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1853     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1854     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1855     'current_charges'  => sprintf('%.2f', $self->charged ),
1856     'previous_balance' => sprintf("%.2f", $pr_total),
1857     'balance'      => sprintf("%.2f", $balance_due),
1858     'duedate'      => $self->balance_due_date,
1859     'ship_enable'  => $conf->exists('invoice-ship_address'),
1860     'unitprices'   => $conf->exists('invoice-unitprice'),
1861   );
1862
1863   my $countrydefault = $conf->config('countrydefault') || 'US';
1864   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1865   foreach ( qw( contact company address1 address2 city state zip country fax) ){
1866     my $method = $prefix.$_;
1867     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1868   }
1869   $invoice_data{'ship_country'} = ''
1870     if ( $invoice_data{'ship_country'} eq $countrydefault );
1871
1872   if ( $cust_main->country eq $countrydefault ) {
1873     $invoice_data{'country'} = '';
1874   } else {
1875     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1876   }
1877
1878   $invoice_data{'notes'} =
1879     join("\n",
1880 #  #do variable substitutions in notes
1881 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1882         $conf->config_orbase('invoice_latexnotes', $template)
1883     );
1884   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1885     if $DEBUG;
1886
1887   #do variable substitution in coupon
1888   foreach my $include (qw( coupon )) {
1889
1890     my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1891
1892     my $inc_tt = new Text::Template (
1893       TYPE       => 'ARRAY',
1894       SOURCE     => [ map "$_\n", @inc_src ],
1895       DELIMITERS => [ '[@--', '--@]' ],
1896     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1897
1898     unless ( $inc_tt->compile() ) {
1899       my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1900       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1901       die $error;
1902     }
1903
1904     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1905
1906     $invoice_data{$include} =~ s/\n+$//
1907   }
1908
1909   $invoice_data{'footer'} =~ s/\n+$//;
1910   $invoice_data{'smallfooter'} =~ s/\n+$//;
1911   $invoice_data{'notes'} =~ s/\n+$//;
1912
1913   $invoice_data{'po_line'} =
1914     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1915       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1916       : '~';
1917
1918   my @filled_in = ();
1919   if ( $format eq 'old' ) {
1920   
1921     my @line_item = ();
1922     my @total_item = ();
1923     while ( @invoice_template ) {
1924       my $line = shift @invoice_template;
1925   
1926       if ( $line =~ /^%%Detail\s*$/ ) {
1927   
1928         while ( ( my $line_item_line = shift @invoice_template )
1929                 !~ /^%%EndDetail\s*$/                            ) {
1930           push @line_item, $line_item_line;
1931         }
1932         foreach my $line_item ( $self->_items ) {
1933         #foreach my $line_item ( $self->_items_pkg ) {
1934           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1935           $invoice_data{'description'} =
1936             _latex_escape($line_item->{'description'});
1937           if ( exists $line_item->{'ext_description'} ) {
1938             $invoice_data{'description'} .=
1939               "\\tabularnewline\n~~".
1940               join( "\\tabularnewline\n~~",
1941                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1942                   );
1943           }
1944           $invoice_data{'amount'}       = $line_item->{'amount'};
1945           $invoice_data{'unit_amount'}  = $line_item->{'unit_amount'};
1946           $invoice_data{'quantity'}     = $line_item->{'quantity'};
1947           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1948           push @filled_in,
1949             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1950         }
1951   
1952       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1953   
1954         while ( ( my $total_item_line = shift @invoice_template )
1955                 !~ /^%%EndTotalDetails\s*$/                      ) {
1956           push @total_item, $total_item_line;
1957         }
1958   
1959         my @total_fill = ();
1960   
1961         my $taxtotal = 0;
1962         foreach my $tax ( $self->_items_tax ) {
1963           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1964           $taxtotal += $tax->{'amount'};
1965           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1966           push @total_fill,
1967             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1968                 @total_item;
1969         }
1970
1971         if ( $taxtotal ) {
1972           $invoice_data{'total_item'} = 'Sub-total';
1973           $invoice_data{'total_amount'} =
1974             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1975           unshift @total_fill,
1976             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1977                 @total_item;
1978         }
1979   
1980         $invoice_data{'total_item'} = '\textbf{Total}';
1981         $invoice_data{'total_amount'} =
1982           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1983         push @total_fill,
1984           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1985               @total_item;
1986   
1987         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1988   
1989         # credits
1990         foreach my $credit ( $self->_items_credits ) {
1991           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1992           #$credittotal
1993           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1994           push @total_fill, 
1995             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1996                 @total_item;
1997         }
1998   
1999         # payments
2000         foreach my $payment ( $self->_items_payments ) {
2001           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2002           #$paymenttotal
2003           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2004           push @total_fill, 
2005             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2006                 @total_item;
2007         }
2008   
2009         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2010         $invoice_data{'total_amount'} =
2011           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2012         push @total_fill,
2013           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2014               @total_item;
2015   
2016         push @filled_in, @total_fill;
2017   
2018       } else {
2019         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2020         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2021         push @filled_in, $line;
2022       }
2023   
2024     }
2025
2026     sub nounder {
2027       my $var = $1;
2028       $var =~ s/_/\-/g;
2029       $var;
2030     }
2031
2032   } elsif ( $format eq 'Text::Template' ) {
2033
2034     my @detail_items = ();
2035     my @total_items = ();
2036
2037     $invoice_data{'detail_items'} = \@detail_items;
2038     $invoice_data{'total_items'} = \@total_items;
2039   
2040     my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
2041     foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2042       my $detail = {
2043         ext_description => [],
2044       };
2045       $detail->{'ref'} = $line_item->{'pkgnum'};
2046       $detail->{'quantity'} = 1;
2047       $detail->{'description'} = _latex_escape($line_item->{'description'});
2048       if ( exists $line_item->{'ext_description'} ) {
2049         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2050       }
2051       $detail->{'amount'} = $line_item->{'amount'};
2052       $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2053       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2054   
2055       push @detail_items, $detail;
2056     }
2057   
2058   
2059     my $taxtotal = 0;
2060     foreach my $tax ( $self->_items_tax ) {
2061       my $total = {};
2062       $total->{'total_item'} = _latex_escape($tax->{'description'});
2063       $taxtotal += $tax->{'amount'};
2064       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2065       push @total_items, $total;
2066     }
2067   
2068     if ( $taxtotal ) {
2069       $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2070       my $total = {};
2071       $total->{'total_item'} = 'Sub-total';
2072       $total->{'total_amount'} =
2073         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2074       unshift @total_items, $total;
2075     }else{
2076       $invoice_data{'taxtotal'} = '0.00';
2077     }
2078   
2079     {
2080       my $total = {};
2081       $total->{'total_item'} = '\textbf{Total}';
2082       $total->{'total_amount'} =
2083         '\textbf{\dollar '.
2084         sprintf( '%.2f',
2085                  $self->charged + ( $conf->exists('disable_previous_balance')
2086                                     ? 0
2087                                     : $pr_total
2088                                   )
2089                ).
2090       '}';
2091       push @total_items, $total;
2092     }
2093   
2094     unless ($conf->exists('disable_previous_balance')) {
2095       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2096   
2097       # credits
2098       my $credittotal = 0;
2099       foreach my $credit ( $self->_items_credits ) {
2100         my $total;
2101         $total->{'total_item'} = _latex_escape($credit->{'description'});
2102         $credittotal += $credit->{'amount'};
2103         $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2104         push @total_items, $total;
2105       }
2106       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2107   
2108       # payments
2109       my $paymenttotal = 0;
2110       foreach my $payment ( $self->_items_payments ) {
2111         my $total = {};
2112         $total->{'total_item'} = _latex_escape($payment->{'description'});
2113         $paymenttotal += $payment->{'amount'};
2114         $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2115         push @total_items, $total;
2116       }
2117       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2118   
2119       { 
2120         my $total;
2121         $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2122         $total->{'total_amount'} =
2123           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2124         push @total_items, $total;
2125       }
2126     }
2127
2128   } else {
2129     die "guru meditation #54";
2130   }
2131
2132   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2133   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2134                            DIR      => $dir,
2135                            SUFFIX   => '.tex',
2136                            UNLINK   => 0,
2137                          ) or die "can't open temp file: $!\n";
2138   if ( $format eq 'old' ) {
2139     print $fh join('', @filled_in );
2140   } elsif ( $format eq 'Text::Template' ) {
2141     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2142   } else {
2143     die "guru meditation #32";
2144   }
2145   close $fh;
2146
2147   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2148   return $1;
2149
2150 }
2151
2152 =item print_ps [ TIME [ , TEMPLATE ] ]
2153
2154 Returns an postscript invoice, as a scalar.
2155
2156 TIME an optional value used to control the printing of overdue messages.  The
2157 default is now.  It isn't the date of the invoice; that's the `_date' field.
2158 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2159 L<Time::Local> and L<Date::Parse> for conversion functions.
2160
2161 =cut
2162
2163 sub print_ps {
2164   my $self = shift;
2165
2166   my $file = $self->print_latex(@_);
2167   my $ps = generate_ps($file);
2168   
2169   $ps;
2170 }
2171
2172 =item print_pdf [ TIME [ , TEMPLATE ] ]
2173
2174 Returns an PDF invoice, as a scalar.
2175
2176 TIME an optional value used to control the printing of overdue messages.  The
2177 default is now.  It isn't the date of the invoice; that's the `_date' field.
2178 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2179 L<Time::Local> and L<Date::Parse> for conversion functions.
2180
2181 =cut
2182
2183 sub print_pdf {
2184   my $self = shift;
2185
2186   my $file = $self->print_latex(@_);
2187   my $pdf = generate_pdf($file);
2188   
2189   $pdf;
2190 }
2191
2192 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2193
2194 Returns an HTML invoice, as a scalar.
2195
2196 TIME an optional value used to control the printing of overdue messages.  The
2197 default is now.  It isn't the date of the invoice; that's the `_date' field.
2198 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2199 L<Time::Local> and L<Date::Parse> for conversion functions.
2200
2201 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2202 when emailing the invoice as part of a multipart/related MIME email.
2203
2204 =cut
2205
2206 #some falze laziness w/print_text and print_latex (and send_csv)
2207 sub print_html {
2208   my( $self, $today, $template, $cid ) = @_;
2209   $today ||= time;
2210
2211   my $cust_main = $self->cust_main;
2212   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2213     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2214
2215   $template ||= $self->_agent_template;
2216   my $templatefile = 'invoice_html';
2217   my $suffix = length($template) ? "_$template" : '';
2218   $templatefile .= $suffix;
2219   my @html_template = map "$_\n", $conf->config($templatefile)
2220     or die "cannot load config file $templatefile";
2221
2222   my $html_template = new Text::Template(
2223     TYPE   => 'ARRAY',
2224     SOURCE => \@html_template,
2225     DELIMITERS => [ '<%=', '%>' ],
2226   );
2227
2228   $html_template->compile()
2229     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2230
2231   my %invoice_data = (
2232     'custnum'      => $self->custnum,
2233     'invnum'       => $self->invnum,
2234     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2235     'today'        => time2str('%b %o, %Y', $today),
2236     'agent'        => encode_entities($cust_main->agent->agent),
2237     'agent_custid' => encode_entities($cust_main->agent_custid),
2238     'payname'      => encode_entities($cust_main->payname),
2239     'company'      => encode_entities($cust_main->company),
2240     'address1'     => encode_entities($cust_main->address1),
2241     'address2'     => encode_entities($cust_main->address2),
2242     'city'         => encode_entities($cust_main->city),
2243     'state'        => encode_entities($cust_main->state),
2244     'zip'          => encode_entities($cust_main->zip),
2245     'terms'        => $conf->config('invoice_default_terms')
2246                       || 'Payable upon receipt',
2247     'cid'          => $cid,
2248     'template'     => $template,
2249     'ship_enable'  => $conf->exists('invoice-ship_address'),
2250     'unitprices'   => $conf->exists('invoice-unitprice'),
2251 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2252   );
2253
2254   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2255   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2256     my $method = $prefix.$_;
2257     $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2258   }
2259
2260   if (
2261          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2262       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2263   ) {
2264     $invoice_data{'returnaddress'} =
2265       join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2266   } else {
2267     $invoice_data{'returnaddress'} =
2268       join("\n", map { 
2269                        s/~/&nbsp;/g;
2270                        s/\\\\\*?\s*$/<BR>/;
2271                        s/\\hyphenation\{[\w\s\-]+\}//;
2272                        $_;
2273                      }
2274                      $conf->config_orbase( 'invoice_latexreturnaddress',
2275                                            $template
2276                                          )
2277           );
2278   }
2279
2280   my $countrydefault = $conf->config('countrydefault') || 'US';
2281   if ( $cust_main->country eq $countrydefault ) {
2282     $invoice_data{'country'} = '';
2283   } else {
2284     $invoice_data{'country'} =
2285       encode_entities(code2country($cust_main->country));
2286   }
2287
2288   if (
2289          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2290       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2291   ) {
2292     $invoice_data{'notes'} =
2293       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2294   } else {
2295     $invoice_data{'notes'} = 
2296       join("\n", map { 
2297                        s/%%(.*)$/<!-- $1 -->/g;
2298                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2299                        s/\\begin\{enumerate\}/<ol>/g;
2300                        s/\\item /  <li>/g;
2301                        s/\\end\{enumerate\}/<\/ol>/g;
2302                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2303                        s/\\\\\*/<br>/g;
2304                        s/\\dollar ?/\$/g;
2305                        s/\\#/#/g;
2306                        s/~/&nbsp;/g;
2307                        $_;
2308                      } 
2309                      $conf->config_orbase('invoice_latexnotes', $template)
2310           );
2311   }
2312
2313 #  #do variable substitutions in notes
2314 #  $invoice_data{'notes'} =
2315 #    join("\n",
2316 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2317 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2318 #    );
2319
2320   if (
2321          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2322       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2323   ) {
2324    $invoice_data{'footer'} =
2325      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2326   } else {
2327    $invoice_data{'footer'} =
2328        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2329                       $conf->config_orbase('invoice_latexfooter', $template)
2330            );
2331   }
2332
2333   $invoice_data{'po_line'} =
2334     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2335       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2336       : '';
2337
2338   my $money_char = $conf->config('money_char') || '$';
2339
2340   my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2341   foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2342     my $detail = {
2343       ext_description => [],
2344     };
2345     $detail->{'ref'} = $line_item->{'pkgnum'};
2346     $detail->{'description'} = encode_entities($line_item->{'description'});
2347     if ( exists $line_item->{'ext_description'} ) {
2348       @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2349     }
2350     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2351     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2352
2353     push @{$invoice_data{'detail_items'}}, $detail;
2354   }
2355
2356
2357   my $taxtotal = 0;
2358   foreach my $tax ( $self->_items_tax ) {
2359     my $total = {};
2360     $total->{'total_item'} = encode_entities($tax->{'description'});
2361     $taxtotal += $tax->{'amount'};
2362     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2363     push @{$invoice_data{'total_items'}}, $total;
2364   }
2365
2366   if ( $taxtotal ) {
2367     my $total = {};
2368     $total->{'total_item'} = 'Sub-total';
2369     $total->{'total_amount'} =
2370       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2371     unshift @{$invoice_data{'total_items'}}, $total;
2372   }
2373
2374   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2375   {
2376     my $total = {};
2377     $total->{'total_item'} = '<b>Total</b>';
2378     $total->{'total_amount'} =
2379       "<b>$money_char".
2380       sprintf( '%.2f',
2381                $self->charged + ( $conf->exists('disable_previous_balance')
2382                                   ? 0
2383                                   : $pr_total
2384                                 )
2385              ).
2386       '</b>';
2387     push @{$invoice_data{'total_items'}}, $total;
2388   }
2389
2390   unless ($conf->exists('disable_previous_balance')) {
2391     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2392
2393     # credits
2394     foreach my $credit ( $self->_items_credits ) {
2395       my $total;
2396       $total->{'total_item'} = encode_entities($credit->{'description'});
2397       #$credittotal
2398       $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2399       push @{$invoice_data{'total_items'}}, $total;
2400     }
2401
2402     # payments
2403     foreach my $payment ( $self->_items_payments ) {
2404       my $total = {};
2405       $total->{'total_item'} = encode_entities($payment->{'description'});
2406       #$paymenttotal
2407       $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2408       push @{$invoice_data{'total_items'}}, $total;
2409     }
2410
2411     { 
2412       my $total;
2413       $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2414       $total->{'total_amount'} =
2415         "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2416       push @{$invoice_data{'total_items'}}, $total;
2417     }
2418   }
2419
2420   $html_template->fill_in( HASH => \%invoice_data);
2421 }
2422
2423 # quick subroutine for print_latex
2424 #
2425 # There are ten characters that LaTeX treats as special characters, which
2426 # means that they do not simply typeset themselves: 
2427 #      # $ % & ~ _ ^ \ { }
2428 #
2429 # TeX ignores blanks following an escaped character; if you want a blank (as
2430 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2431
2432 sub _latex_escape {
2433   my $value = shift;
2434   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2435   $value =~ s/([<>])/\$$1\$/g;
2436   $value;
2437 }
2438
2439 #utility methods for print_*
2440
2441 sub balance_due_msg {
2442   my $self = shift;
2443   my $msg = 'Balance Due';
2444   return $msg unless $conf->exists('invoice_default_terms');
2445   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2446     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2447   } elsif ( $conf->config('invoice_default_terms') ) {
2448     $msg .= ' - '. $conf->config('invoice_default_terms');
2449   }
2450   $msg;
2451 }
2452
2453 sub balance_due_date {
2454   my $self = shift;
2455   my $duedate = '';
2456   if (    $conf->exists('invoice_default_terms') 
2457        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2458     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2459   }
2460   $duedate;
2461 }
2462
2463 =item invnum_date_pretty
2464
2465 Returns a string with the invoice number and date, for example:
2466 "Invoice #54 (3/20/2008)"
2467
2468 =cut
2469
2470 sub invnum_date_pretty {
2471   my $self = shift;
2472   'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2473 }
2474
2475 sub _items {
2476   my $self = shift;
2477
2478   #my @display = scalar(@_)
2479   #              ? @_
2480   #              : qw( _items_previous _items_pkg );
2481   #              #: qw( _items_pkg );
2482   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2483   my @display = qw( _items_previous _items_pkg );
2484
2485   my @b = ();
2486   foreach my $display ( @display ) {
2487     push @b, $self->$display(@_);
2488   }
2489   @b;
2490 }
2491
2492 sub _items_previous {
2493   my $self = shift;
2494   my $cust_main = $self->cust_main;
2495   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2496   my @b = ();
2497   foreach ( @pr_cust_bill ) {
2498     push @b, {
2499       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2500                        ' ('. time2str('%x',$_->_date). ')',
2501       #'pkgpart'     => 'N/A',
2502       'pkgnum'      => 'N/A',
2503       'amount'      => sprintf("%.2f", $_->owed),
2504     };
2505   }
2506   @b;
2507
2508   #{
2509   #    'description'     => 'Previous Balance',
2510   #    #'pkgpart'         => 'N/A',
2511   #    'pkgnum'          => 'N/A',
2512   #    'amount'          => sprintf("%10.2f", $pr_total ),
2513   #    'ext_description' => [ map {
2514   #                                 "Invoice ". $_->invnum.
2515   #                                 " (". time2str("%x",$_->_date). ") ".
2516   #                                 sprintf("%10.2f", $_->owed)
2517   #                         } @pr_cust_bill ],
2518
2519   #};
2520 }
2521
2522 sub _items_pkg {
2523   my $self = shift;
2524   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2525   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2526 }
2527
2528 sub _items_tax {
2529   my $self = shift;
2530   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2531   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2532 }
2533
2534 sub _items_cust_bill_pkg {
2535   my $self = shift;
2536   my $cust_bill_pkg = shift;
2537   my %opt = @_;
2538
2539   my $format = $opt{format} || '';
2540   my $escape_function = $opt{escape_function} || sub { shift };
2541
2542   my @b = ();
2543   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2544
2545     my $cust_pkg = $cust_bill_pkg->cust_pkg;
2546
2547     my $desc = $cust_bill_pkg->desc;
2548
2549     my %details_opt = ( 'format'          => $format,
2550                         'escape_function' => $escape_function,
2551                       );
2552
2553     if ( $cust_bill_pkg->pkgnum > 0 ) {
2554
2555       if ( $cust_bill_pkg->setup != 0 ) {
2556
2557         my $description = $desc;
2558         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2559
2560         my @d = map &{$escape_function}($_),
2561                        $cust_pkg->h_labels_short($self->_date);
2562         push @d, $cust_bill_pkg->details(%details_opt)
2563           if $cust_bill_pkg->recur == 0;
2564
2565         push @b, {
2566           description     => $description,
2567           #pkgpart         => $part_pkg->pkgpart,
2568           pkgnum          => $cust_bill_pkg->pkgnum,
2569           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2570           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2571           quantity        => $cust_bill_pkg->quantity,
2572           ext_description => \@d,
2573         };
2574       }
2575
2576       if ( $cust_bill_pkg->recur != 0 ) {
2577
2578         my $description = $desc;
2579         unless ( $conf->exists('disable_line_item_date_ranges') ) {
2580           $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2581                           " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2582         }
2583
2584         #at least until cust_bill_pkg has "past" ranges in addition to
2585         #the "future" sdate/edate ones... see #3032
2586         my @d = map &{$escape_function}($_),
2587                     $cust_pkg->h_labels_short($self->_date);
2588                                               #$cust_bill_pkg->edate,
2589                                               #$cust_bill_pkg->sdate),
2590         push @d, $cust_bill_pkg->details(%details_opt);
2591
2592         push @b, {
2593           description     => $description,
2594           #pkgpart         => $part_pkg->pkgpart,
2595           pkgnum          => $cust_bill_pkg->pkgnum,
2596           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2597           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2598           quantity        => $cust_bill_pkg->quantity,
2599           ext_description => \@d,
2600         };
2601
2602       }
2603
2604     } else { #pkgnum tax or one-shot line item (??)
2605
2606       if ( $cust_bill_pkg->setup != 0 ) {
2607         push @b, {
2608           'description' => $desc,
2609           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2610         };
2611       }
2612       if ( $cust_bill_pkg->recur != 0 ) {
2613         push @b, {
2614           'description' => "$desc (".
2615                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2616                            time2str("%x", $cust_bill_pkg->edate). ')',
2617           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2618         };
2619       }
2620
2621     }
2622
2623   }
2624
2625   @b;
2626
2627 }
2628
2629 sub _items_credits {
2630   my $self = shift;
2631
2632   my @b;
2633   #credits
2634   foreach ( $self->cust_credited ) {
2635
2636     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2637
2638     my $reason = $_->cust_credit->reason;
2639     #my $reason = substr($_->cust_credit->reason,0,32);
2640     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2641     $reason = " ($reason) " if $reason;
2642     push @b, {
2643       #'description' => 'Credit ref\#'. $_->crednum.
2644       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2645       #                 $reason,
2646       'description' => 'Credit applied '.
2647                        time2str("%x",$_->cust_credit->_date). $reason,
2648       'amount'      => sprintf("%.2f",$_->amount),
2649     };
2650   }
2651   #foreach ( @cr_cust_credit ) {
2652   #  push @buf,[
2653   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2654   #    $money_char. sprintf("%10.2f",$_->credited)
2655   #  ];
2656   #}
2657
2658   @b;
2659
2660 }
2661
2662 sub _items_payments {
2663   my $self = shift;
2664
2665   my @b;
2666   #get & print payments
2667   foreach ( $self->cust_bill_pay ) {
2668
2669     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2670
2671     push @b, {
2672       'description' => "Payment received ".
2673                        time2str("%x",$_->cust_pay->_date ),
2674       'amount'      => sprintf("%.2f", $_->amount )
2675     };
2676   }
2677
2678   @b;
2679
2680 }
2681
2682
2683 =back
2684
2685 =head1 SUBROUTINES
2686
2687 =over 4
2688
2689 =item process_reprint
2690
2691 =cut
2692
2693 sub process_reprint {
2694   process_re_X('print', @_);
2695 }
2696
2697 =item process_reemail
2698
2699 =cut
2700
2701 sub process_reemail {
2702   process_re_X('email', @_);
2703 }
2704
2705 =item process_refax
2706
2707 =cut
2708
2709 sub process_refax {
2710   process_re_X('fax', @_);
2711 }
2712
2713 =item process_reftp
2714
2715 =cut
2716
2717 sub process_reftp {
2718   process_re_X('ftp', @_);
2719 }
2720
2721 =item respool
2722
2723 =cut
2724
2725 sub process_respool {
2726   process_re_X('spool', @_);
2727 }
2728
2729 use Storable qw(thaw);
2730 use Data::Dumper;
2731 use MIME::Base64;
2732 sub process_re_X {
2733   my( $method, $job ) = ( shift, shift );
2734   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2735
2736   my $param = thaw(decode_base64(shift));
2737   warn Dumper($param) if $DEBUG;
2738
2739   re_X(
2740     $method,
2741     $job,
2742     %$param,
2743   );
2744
2745 }
2746
2747 sub re_X {
2748   my($method, $job, %param ) = @_;
2749   if ( $DEBUG ) {
2750     warn "re_X $method for job $job with param:\n".
2751          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2752   }
2753
2754   #some false laziness w/search/cust_bill.html
2755   my $distinct = '';
2756   my $orderby = 'ORDER BY cust_bill._date';
2757
2758   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2759
2760   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2761      
2762   my @cust_bill = qsearch( {
2763     #'select'    => "cust_bill.*",
2764     'table'     => 'cust_bill',
2765     'addl_from' => $addl_from,
2766     'hashref'   => {},
2767     'extra_sql' => $extra_sql,
2768     'order_by'  => $orderby,
2769     'debug' => 1,
2770   } );
2771
2772   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2773
2774   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2775     if $DEBUG;
2776
2777   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2778   foreach my $cust_bill ( @cust_bill ) {
2779     $cust_bill->$method();
2780
2781     if ( $job ) { #progressbar foo
2782       $num++;
2783       if ( time - $min_sec > $last ) {
2784         my $error = $job->update_statustext(
2785           int( 100 * $num / scalar(@cust_bill) )
2786         );
2787         die $error if $error;
2788         $last = time;
2789       }
2790     }
2791
2792   }
2793
2794 }
2795
2796 =back
2797
2798 =head1 CLASS METHODS
2799
2800 =over 4
2801
2802 =item owed_sql
2803
2804 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2805
2806 =cut
2807
2808 sub owed_sql {
2809   my $class = shift;
2810   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2811 }
2812
2813 =item net_sql
2814
2815 Returns an SQL fragment to retreive the net amount (charged minus credited).
2816
2817 =cut
2818
2819 sub net_sql {
2820   my $class = shift;
2821   'charged - '. $class->credited_sql;
2822 }
2823
2824 =item paid_sql
2825
2826 Returns an SQL fragment to retreive the amount paid against this invoice.
2827
2828 =cut
2829
2830 sub paid_sql {
2831   #my $class = shift;
2832   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2833        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
2834 }
2835
2836 =item credited_sql
2837
2838 Returns an SQL fragment to retreive the amount credited against this invoice.
2839
2840 =cut
2841
2842 sub credited_sql {
2843   #my $class = shift;
2844   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2845        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
2846 }
2847
2848 =item search_sql HASHREF
2849
2850 Class method which returns an SQL WHERE fragment to search for parameters
2851 specified in HASHREF.  Valid parameters are
2852
2853 =over 4
2854
2855 =item begin
2856
2857 Epoch date (UNIX timestamp) setting a lower bound for _date values
2858
2859 =item end
2860
2861 Epoch date (UNIX timestamp) setting an upper bound for _date values
2862
2863 =item invnum_min
2864
2865 =item invnum_max
2866
2867 =item agentnum
2868
2869 =item owed
2870
2871 =item net
2872
2873 =item days
2874
2875 =item newest_percust
2876
2877 =back
2878
2879 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2880
2881 =cut
2882
2883 sub search_sql {
2884   my($class, $param) = @_;
2885   if ( $DEBUG ) {
2886     warn "$me search_sql called with params: \n".
2887          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
2888   }
2889
2890   my @search = ();
2891
2892   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2893     push @search, "cust_bill._date >= $1";
2894   }
2895   if ( $param->{'end'} =~ /^(\d+)$/ ) {
2896     push @search, "cust_bill._date < $1";
2897   }
2898   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2899     push @search, "cust_bill.invnum >= $1";
2900   }
2901   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2902     push @search, "cust_bill.invnum <= $1";
2903   }
2904   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2905     push @search, "cust_main.agentnum = $1";
2906   }
2907
2908   push @search, '0 != '. FS::cust_bill->owed_sql
2909     if $param->{'open'};
2910
2911   push @search, '0 != '. FS::cust_bill->net_sql
2912     if $param->{'net'};
2913
2914   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2915     if $param->{'days'};
2916
2917   if ( $param->{'newest_percust'} ) {
2918
2919     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2920     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2921
2922     my @newest_where = map { my $x = $_;
2923                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
2924                              $x;
2925                            }
2926                            grep ! /^cust_main./, @search;
2927     my $newest_where = scalar(@newest_where)
2928                          ? ' AND '. join(' AND ', @newest_where)
2929                          : '';
2930
2931
2932     push @search, "cust_bill._date = (
2933       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2934         WHERE newest_cust_bill.custnum = cust_bill.custnum
2935           $newest_where
2936     )";
2937
2938   }
2939
2940   my $curuser = $FS::CurrentUser::CurrentUser;
2941   if ( $curuser->username eq 'fs_queue'
2942        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2943     my $username = $1;
2944     my $newuser = qsearchs('access_user', {
2945       'username' => $username,
2946       'disabled' => '',
2947     } );
2948     if ( $newuser ) {
2949       $curuser = $newuser;
2950     } else {
2951       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2952     }
2953   }
2954
2955   push @search, $curuser->agentnums_sql;
2956
2957   join(' AND ', @search );
2958
2959 }
2960
2961 =back
2962
2963 =head1 BUGS
2964
2965 The delete method.
2966
2967 =head1 SEE ALSO
2968
2969 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2970 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2971 documentation.
2972
2973 =cut
2974
2975 1;
2976