bulk price plan: label as Name <email>, supress extraneous service list, RT#3519
[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 $subject = $self->email_subject($template);
849
850   my $error = send_email(
851     $self->generate_email(
852       'from'       => $invoice_from,
853       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
854       'subject'    => $subject,
855       'template'   => $template,
856     )
857   );
858   die "can't email invoice: $error\n" if $error;
859   #die "$error\n" if $error;
860
861 }
862
863 sub email_subject {
864   my $self = shift;
865
866   #my $template = scalar(@_) ? shift : '';
867   #per-template?
868
869   my $subject = $conf->config('invoice_subject') || 'Invoice';
870
871   my $cust_main = $self->cust_main;
872   my $name = $cust_main->name;
873   my $name_short = $cust_main->name_short;
874   my $invoice_number = $self->invnum;
875   my $invoice_date = $self->_date_pretty;
876
877   eval qq("$subject");
878 }
879
880 =item lpr_data [ TEMPLATENAME ]
881
882 Returns the postscript or plaintext for this invoice as an arrayref.
883
884 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
885
886 =cut
887
888 sub lpr_data {
889   my( $self, $template) = @_;
890   $conf->exists('invoice_latex')
891     ? [ $self->print_ps('', $template) ]
892     : [ $self->print_text('', $template) ];
893 }
894
895 =item print [ TEMPLATENAME ]
896
897 Prints this invoice.
898
899 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
900
901 =cut
902
903 #sub print_invoice {
904 sub print {
905   my $self = shift;
906   my $template = scalar(@_) ? shift : '';
907
908   do_print $self->lpr_data($template);
909 }
910
911 =item fax_invoice [ TEMPLATENAME ] 
912
913 Faxes this invoice.
914
915 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
916
917 =cut
918
919 sub fax_invoice {
920   my $self = shift;
921   my $template = scalar(@_) ? shift : '';
922
923   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
924     unless $conf->exists('invoice_latex');
925
926   my $dialstring = $self->cust_main->getfield('fax');
927   #Check $dialstring?
928
929   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
930                         'dialstring' => $dialstring,
931                       );
932   die $error if $error;
933
934 }
935
936 =item ftp_invoice [ TEMPLATENAME ] 
937
938 Sends this invoice data via FTP.
939
940 TEMPLATENAME is unused?
941
942 =cut
943
944 sub ftp_invoice {
945   my $self = shift;
946   my $template = scalar(@_) ? shift : '';
947
948   $self->send_csv(
949     'protocol'   => 'ftp',
950     'server'     => $conf->config('cust_bill-ftpserver'),
951     'username'   => $conf->config('cust_bill-ftpusername'),
952     'password'   => $conf->config('cust_bill-ftppassword'),
953     'dir'        => $conf->config('cust_bill-ftpdir'),
954     'format'     => $conf->config('cust_bill-ftpformat'),
955   );
956 }
957
958 =item spool_invoice [ TEMPLATENAME ] 
959
960 Spools this invoice data (see L<FS::spool_csv>)
961
962 TEMPLATENAME is unused?
963
964 =cut
965
966 sub spool_invoice {
967   my $self = shift;
968   my $template = scalar(@_) ? shift : '';
969
970   $self->spool_csv(
971     'format'       => $conf->config('cust_bill-spoolformat'),
972     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
973   );
974 }
975
976 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
977
978 Like B<send>, but only sends the invoice if it is the newest open invoice for
979 this customer.
980
981 =cut
982
983 sub send_if_newest {
984   my $self = shift;
985
986   return ''
987     if scalar(
988                grep { $_->owed > 0 } 
989                     qsearch('cust_bill', {
990                       'custnum' => $self->custnum,
991                       #'_date'   => { op=>'>', value=>$self->_date },
992                       'invnum'  => { op=>'>', value=>$self->invnum },
993                     } )
994              );
995     
996   $self->send(@_);
997 }
998
999 =item send_csv OPTION => VALUE, ...
1000
1001 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1002
1003 Options are:
1004
1005 protocol - currently only "ftp"
1006 server
1007 username
1008 password
1009 dir
1010
1011 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1012 and YYMMDDHHMMSS is a timestamp.
1013
1014 See L</print_csv> for a description of the output format.
1015
1016 =cut
1017
1018 sub send_csv {
1019   my($self, %opt) = @_;
1020
1021   #create file(s)
1022
1023   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1024   mkdir $spooldir, 0700 unless -d $spooldir;
1025
1026   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1027   my $file = "$spooldir/$tracctnum.csv";
1028   
1029   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1030
1031   open(CSV, ">$file") or die "can't open $file: $!";
1032   print CSV $header;
1033
1034   print CSV $detail;
1035
1036   close CSV;
1037
1038   my $net;
1039   if ( $opt{protocol} eq 'ftp' ) {
1040     eval "use Net::FTP;";
1041     die $@ if $@;
1042     $net = Net::FTP->new($opt{server}) or die @$;
1043   } else {
1044     die "unknown protocol: $opt{protocol}";
1045   }
1046
1047   $net->login( $opt{username}, $opt{password} )
1048     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1049
1050   $net->binary or die "can't set binary mode";
1051
1052   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1053
1054   $net->put($file) or die "can't put $file: $!";
1055
1056   $net->quit;
1057
1058   unlink $file;
1059
1060 }
1061
1062 =item spool_csv
1063
1064 Spools CSV invoice data.
1065
1066 Options are:
1067
1068 =over 4
1069
1070 =item format - 'default' or 'billco'
1071
1072 =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>).
1073
1074 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1075
1076 =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.
1077
1078 =back
1079
1080 =cut
1081
1082 sub spool_csv {
1083   my($self, %opt) = @_;
1084
1085   my $cust_main = $self->cust_main;
1086
1087   if ( $opt{'dest'} ) {
1088     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1089                              $cust_main->invoicing_list;
1090     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1091                      || ! keys %invoicing_list;
1092   }
1093
1094   if ( $opt{'balanceover'} ) {
1095     return 'N/A'
1096       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1097   }
1098
1099   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1100   mkdir $spooldir, 0700 unless -d $spooldir;
1101
1102   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1103
1104   my $file =
1105     "$spooldir/".
1106     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1107     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1108     '.csv';
1109   
1110   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1111
1112   open(CSV, ">>$file") or die "can't open $file: $!";
1113   flock(CSV, LOCK_EX);
1114   seek(CSV, 0, 2);
1115
1116   print CSV $header;
1117
1118   if ( lc($opt{'format'}) eq 'billco' ) {
1119
1120     flock(CSV, LOCK_UN);
1121     close CSV;
1122
1123     $file =
1124       "$spooldir/".
1125       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1126       '-detail.csv';
1127
1128     open(CSV,">>$file") or die "can't open $file: $!";
1129     flock(CSV, LOCK_EX);
1130     seek(CSV, 0, 2);
1131   }
1132
1133   print CSV $detail;
1134
1135   flock(CSV, LOCK_UN);
1136   close CSV;
1137
1138   return '';
1139
1140 }
1141
1142 =item print_csv OPTION => VALUE, ...
1143
1144 Returns CSV data for this invoice.
1145
1146 Options are:
1147
1148 format - 'default' or 'billco'
1149
1150 Returns a list consisting of two scalars.  The first is a single line of CSV
1151 header information for this invoice.  The second is one or more lines of CSV
1152 detail information for this invoice.
1153
1154 If I<format> is not specified or "default", the fields of the CSV file are as
1155 follows:
1156
1157 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1158
1159 =over 4
1160
1161 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1162
1163 B<record_type> is C<cust_bill> for the initial header line only.  The
1164 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1165 fields are filled in.
1166
1167 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1168 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1169 are filled in.
1170
1171 =item invnum - invoice number
1172
1173 =item custnum - customer number
1174
1175 =item _date - invoice date
1176
1177 =item charged - total invoice amount
1178
1179 =item first - customer first name
1180
1181 =item last - customer first name
1182
1183 =item company - company name
1184
1185 =item address1 - address line 1
1186
1187 =item address2 - address line 1
1188
1189 =item city
1190
1191 =item state
1192
1193 =item zip
1194
1195 =item country
1196
1197 =item pkg - line item description
1198
1199 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1200
1201 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1202
1203 =item sdate - start date for recurring fee
1204
1205 =item edate - end date for recurring fee
1206
1207 =back
1208
1209 If I<format> is "billco", the fields of the header CSV file are as follows:
1210
1211   +-------------------------------------------------------------------+
1212   |                        FORMAT HEADER FILE                         |
1213   |-------------------------------------------------------------------|
1214   | Field | Description                   | Name       | Type | Width |
1215   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1216   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1217   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1218   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1219   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1220   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1221   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1222   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1223   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1224   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1225   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1226   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1227   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1228   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1229   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1230   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1231   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1232   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1233   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1234   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1235   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1236   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1237   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1238   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1239   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1240   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1241   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1242   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1243   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1244   +-------+-------------------------------+------------+------+-------+
1245
1246 If I<format> is "billco", the fields of the detail CSV file are as follows:
1247
1248                                   FORMAT FOR DETAIL FILE
1249         |                            |           |      |
1250   Field | Description                | Name      | Type | Width
1251   1     | N/A-Leave Empty            | RC        | CHAR |     2
1252   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1253   3     | Account Number             | TRACCTNUM | CHAR |    15
1254   4     | Invoice Number             | TRINVOICE | CHAR |    15
1255   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1256   6     | Transaction Detail         | DETAILS   | CHAR |   100
1257   7     | Amount                     | AMT       | NUM* |     9
1258   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1259   9     | Grouping Code              | GROUP     | CHAR |     2
1260   10    | User Defined               | ACCT CODE | CHAR |    15
1261
1262 =cut
1263
1264 sub print_csv {
1265   my($self, %opt) = @_;
1266   
1267   eval "use Text::CSV_XS";
1268   die $@ if $@;
1269
1270   my $cust_main = $self->cust_main;
1271
1272   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1273
1274   if ( lc($opt{'format'}) eq 'billco' ) {
1275
1276     my $taxtotal = 0;
1277     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1278
1279     my $duedate = $self->balance_due_date;
1280
1281     my( $previous_balance, @unused ) = $self->previous; #previous balance
1282
1283     my $pmt_cr_applied = 0;
1284     $pmt_cr_applied += $_->{'amount'}
1285       foreach ( $self->_items_payments, $self->_items_credits ) ;
1286
1287     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1288
1289     $csv->combine(
1290       '',                         #  1 | N/A-Leave Empty               CHAR   2
1291       '',                         #  2 | N/A-Leave Empty               CHAR  15
1292       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1293       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1294       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1295       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1296       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1297       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1298       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1299       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1300       '',                         # 10 | Ancillary Billing Information CHAR  30
1301       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1302       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1303
1304       # XXX ?
1305       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1306
1307       # XXX ?
1308       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1309
1310       $previous_balance,          # 15 | Previous Balance              NUM*   9
1311       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1312       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1313       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1314       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1315       '',                         # 20 | 30 Day Aging                  NUM*   9
1316       '',                         # 21 | 60 Day Aging                  NUM*   9
1317       '',                         # 22 | 90 Day Aging                  NUM*   9
1318       'N',                        # 23 | Y/N                           CHAR   1
1319       '',                         # 24 | Remittance automation         CHAR 100
1320       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1321       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1322       '0',                        # 27 | Federal Tax***                NUM*   9
1323       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1324       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1325     );
1326
1327   } else {
1328   
1329     $csv->combine(
1330       'cust_bill',
1331       $self->invnum,
1332       $self->custnum,
1333       time2str("%x", $self->_date),
1334       sprintf("%.2f", $self->charged),
1335       ( map { $cust_main->getfield($_) }
1336           qw( first last company address1 address2 city state zip country ) ),
1337       map { '' } (1..5),
1338     ) or die "can't create csv";
1339   }
1340
1341   my $header = $csv->string. "\n";
1342
1343   my $detail = '';
1344   if ( lc($opt{'format'}) eq 'billco' ) {
1345
1346     my $lineseq = 0;
1347     foreach my $item ( $self->_items_pkg ) {
1348
1349       $csv->combine(
1350         '',                     #  1 | N/A-Leave Empty            CHAR   2
1351         '',                     #  2 | N/A-Leave Empty            CHAR  15
1352         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1353         $self->invnum,          #  4 | Invoice Number             CHAR  15
1354         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1355         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1356         $item->{'amount'},      #  7 | Amount                     NUM*   9
1357         '',                     #  8 | Line Format Control**      CHAR   2
1358         '',                     #  9 | Grouping Code              CHAR   2
1359         '',                     # 10 | User Defined               CHAR  15
1360       );
1361
1362       $detail .= $csv->string. "\n";
1363
1364     }
1365
1366   } else {
1367
1368     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1369
1370       my($pkg, $setup, $recur, $sdate, $edate);
1371       if ( $cust_bill_pkg->pkgnum ) {
1372       
1373         ($pkg, $setup, $recur, $sdate, $edate) = (
1374           $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1375           ( $cust_bill_pkg->setup != 0
1376             ? sprintf("%.2f", $cust_bill_pkg->setup )
1377             : '' ),
1378           ( $cust_bill_pkg->recur != 0
1379             ? sprintf("%.2f", $cust_bill_pkg->recur )
1380             : '' ),
1381           ( $cust_bill_pkg->sdate 
1382             ? time2str("%x", $cust_bill_pkg->sdate)
1383             : '' ),
1384           ($cust_bill_pkg->edate 
1385             ?time2str("%x", $cust_bill_pkg->edate)
1386             : '' ),
1387         );
1388   
1389       } else { #pkgnum tax
1390         next unless $cust_bill_pkg->setup != 0;
1391         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1392                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1393                          : 'Tax';
1394         ($pkg, $setup, $recur, $sdate, $edate) =
1395           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1396       }
1397   
1398       $csv->combine(
1399         'cust_bill_pkg',
1400         $self->invnum,
1401         ( map { '' } (1..11) ),
1402         ($pkg, $setup, $recur, $sdate, $edate)
1403       ) or die "can't create csv";
1404
1405       $detail .= $csv->string. "\n";
1406
1407     }
1408
1409   }
1410
1411   ( $header, $detail );
1412
1413 }
1414
1415 =item comp
1416
1417 Pays this invoice with a compliemntary payment.  If there is an error,
1418 returns the error, otherwise returns false.
1419
1420 =cut
1421
1422 sub comp {
1423   my $self = shift;
1424   my $cust_pay = new FS::cust_pay ( {
1425     'invnum'   => $self->invnum,
1426     'paid'     => $self->owed,
1427     '_date'    => '',
1428     'payby'    => 'COMP',
1429     'payinfo'  => $self->cust_main->payinfo,
1430     'paybatch' => '',
1431   } );
1432   $cust_pay->insert;
1433 }
1434
1435 =item realtime_card
1436
1437 Attempts to pay this invoice with a credit card payment via a
1438 Business::OnlinePayment realtime gateway.  See
1439 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1440 for supported processors.
1441
1442 =cut
1443
1444 sub realtime_card {
1445   my $self = shift;
1446   $self->realtime_bop( 'CC', @_ );
1447 }
1448
1449 =item realtime_ach
1450
1451 Attempts to pay this invoice with an electronic check (ACH) payment via a
1452 Business::OnlinePayment realtime gateway.  See
1453 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1454 for supported processors.
1455
1456 =cut
1457
1458 sub realtime_ach {
1459   my $self = shift;
1460   $self->realtime_bop( 'ECHECK', @_ );
1461 }
1462
1463 =item realtime_lec
1464
1465 Attempts to pay this invoice with phone bill (LEC) payment via a
1466 Business::OnlinePayment realtime gateway.  See
1467 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1468 for supported processors.
1469
1470 =cut
1471
1472 sub realtime_lec {
1473   my $self = shift;
1474   $self->realtime_bop( 'LEC', @_ );
1475 }
1476
1477 sub realtime_bop {
1478   my( $self, $method ) = @_;
1479
1480   my $cust_main = $self->cust_main;
1481   my $balance = $cust_main->balance;
1482   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1483   $amount = sprintf("%.2f", $amount);
1484   return "not run (balance $balance)" unless $amount > 0;
1485
1486   my $description = 'Internet Services';
1487   if ( $conf->exists('business-onlinepayment-description') ) {
1488     my $dtempl = $conf->config('business-onlinepayment-description');
1489
1490     my $agent_obj = $cust_main->agent
1491       or die "can't retreive agent for $cust_main (agentnum ".
1492              $cust_main->agentnum. ")";
1493     my $agent = $agent_obj->agent;
1494     my $pkgs = join(', ',
1495       map { $_->cust_pkg->part_pkg->pkg }
1496         grep { $_->pkgnum } $self->cust_bill_pkg
1497     );
1498     $description = eval qq("$dtempl");
1499   }
1500
1501   $cust_main->realtime_bop($method, $amount,
1502     'description' => $description,
1503     'invnum'      => $self->invnum,
1504   );
1505
1506 }
1507
1508 =item batch_card OPTION => VALUE...
1509
1510 Adds a payment for this invoice to the pending credit card batch (see
1511 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1512 runs the payment using a realtime gateway.
1513
1514 =cut
1515
1516 sub batch_card {
1517   my ($self, %options) = @_;
1518   my $cust_main = $self->cust_main;
1519
1520   $options{invnum} = $self->invnum;
1521   
1522   $cust_main->batch_card(%options);
1523 }
1524
1525 sub _agent_template {
1526   my $self = shift;
1527   $self->cust_main->agent_template;
1528 }
1529
1530 sub _agent_invoice_from {
1531   my $self = shift;
1532   $self->cust_main->agent_invoice_from;
1533 }
1534
1535 =item print_text [ TIME [ , TEMPLATE ] ]
1536
1537 Returns an text invoice, as a list of lines.
1538
1539 TIME an optional value used to control the printing of overdue messages.  The
1540 default is now.  It isn't the date of the invoice; that's the `_date' field.
1541 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1542 L<Time::Local> and L<Date::Parse> for conversion functions.
1543
1544 =cut
1545
1546 #still some false laziness w/_items stuff (and send_csv)
1547 sub print_text {
1548
1549   my( $self, $today, $template ) = @_;
1550   $today ||= time;
1551
1552 #  my $invnum = $self->invnum;
1553   my $cust_main = $self->cust_main;
1554   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1555     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1556
1557   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1558 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1559   #my $balance_due = $self->owed + $pr_total - $cr_total;
1560   my $balance_due = $self->owed + $pr_total;
1561
1562   #my @collect = ();
1563   #my($description,$amount);
1564   @buf = ();
1565
1566   #previous balance
1567   unless ($conf->exists('disable_previous_balance')) {
1568     foreach ( @pr_cust_bill ) {
1569       push @buf, [
1570         "Previous Balance, Invoice #". $_->invnum. 
1571                    " (". time2str("%x",$_->_date). ")",
1572         $money_char. sprintf("%10.2f",$_->owed)
1573       ];
1574     }
1575     if (@pr_cust_bill) {
1576       push @buf,['','-----------'];
1577       push @buf,[ 'Total Previous Balance',
1578                   $money_char. sprintf("%10.2f",$pr_total ) ];
1579       push @buf,['',''];
1580     }
1581   }
1582
1583   #new charges
1584   foreach my $cust_bill_pkg (
1585     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1586     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1587   ) {
1588
1589     my $desc = $cust_bill_pkg->desc;
1590
1591     if ( $cust_bill_pkg->pkgnum > 0 ) {
1592
1593       if ( $cust_bill_pkg->setup != 0 ) {
1594         my $description = $desc;
1595         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1596         push @buf, [ $description,
1597                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1598         push @buf,
1599           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1600               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1601       }
1602
1603       if ( $cust_bill_pkg->recur != 0 ) {
1604         push @buf, [
1605           $desc .
1606             ( $conf->exists('disable_line_item_date_ranges')
1607               ? ''
1608               : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1609                        time2str("%x", $cust_bill_pkg->edate) . ")"
1610             ),
1611           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1612         ];
1613         push @buf,
1614           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1615               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1616                                                   $cust_bill_pkg->sdate );
1617       }
1618
1619       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1620
1621     } else { #pkgnum tax or one-shot line item
1622
1623       if ( $cust_bill_pkg->setup != 0 ) {
1624         push @buf, [ $desc,
1625                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1626       }
1627       if ( $cust_bill_pkg->recur != 0 ) {
1628         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1629                               . time2str("%x", $cust_bill_pkg->edate). ")",
1630                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1631                    ];
1632       }
1633
1634     }
1635
1636   }
1637
1638   push @buf,['','-----------'];
1639   push @buf,[ ( $conf->exists('disable_previous_balance')
1640                 ? 'Total Charges'
1641                 : 'Total New Charges'),
1642              $money_char. sprintf("%10.2f",$self->charged) ];
1643   push @buf,['',''];
1644
1645   unless ($conf->exists('disable_previous_balance')) {
1646     push @buf,['','-----------'];
1647     push @buf,['Total Charges',
1648                $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1649     push @buf,['',''];
1650
1651     #credits
1652     foreach ( $self->cust_credited ) {
1653
1654       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1655
1656       my $reason = substr($_->cust_credit->reason,0,32);
1657       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1658       $reason = " ($reason) " if $reason;
1659       push @buf,[
1660         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1661           $reason,
1662         $money_char. sprintf("%10.2f",$_->amount)
1663       ];
1664     }
1665     #foreach ( @cr_cust_credit ) {
1666     #  push @buf,[
1667     #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1668     #    $money_char. sprintf("%10.2f",$_->credited)
1669     #  ];
1670     #}
1671
1672     #get & print payments
1673     foreach ( $self->cust_bill_pay ) {
1674
1675       #something more elaborate if $_->amount ne ->cust_pay->paid ?
1676
1677       push @buf,[
1678         "Payment received ". time2str("%x",$_->cust_pay->_date ),
1679         $money_char. sprintf("%10.2f",$_->amount )
1680       ];
1681     }
1682
1683     #balance due
1684     my $balance_due_msg = $self->balance_due_msg;
1685
1686     push @buf,['','-----------'];
1687     push @buf,[$balance_due_msg, $money_char. 
1688       sprintf("%10.2f", $balance_due ) ];
1689   }
1690
1691   #create the template
1692   $template ||= $self->_agent_template;
1693   my $templatefile = 'invoice_template';
1694   $templatefile .= "_$template" if length($template);
1695   my @invoice_template = $conf->config($templatefile)
1696     or die "cannot load config file $templatefile";
1697   $invoice_lines = 0;
1698   my $wasfunc = 0;
1699   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1700     /invoice_lines\((\d*)\)/;
1701     $invoice_lines += $1 || scalar(@buf);
1702     $wasfunc=1;
1703   }
1704   die "no invoice_lines() functions in template?" unless $wasfunc;
1705   my $invoice_template = new Text::Template (
1706     TYPE   => 'ARRAY',
1707     SOURCE => [ map "$_\n", @invoice_template ],
1708   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1709   $invoice_template->compile()
1710     or die "can't compile template: $Text::Template::ERROR";
1711
1712   #setup template variables
1713   package FS::cust_bill::_template; #!
1714   use vars qw( $custnum $invnum $date $agent @address $overdue
1715                $page $total_pages @buf );
1716
1717   $custnum = $self->custnum;
1718   $invnum = $self->invnum;
1719   $date = $self->_date;
1720   $agent = $self->cust_main->agent->agent;
1721   $page = 1;
1722
1723   if ( $FS::cust_bill::invoice_lines ) {
1724     $total_pages =
1725       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1726     $total_pages++
1727       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1728   } else {
1729     $total_pages = 1;
1730   }
1731
1732   #format address (variable for the template)
1733   my $l = 0;
1734   @address = ( '', '', '', '', '', '' );
1735   package FS::cust_bill; #!
1736   $FS::cust_bill::_template::address[$l++] =
1737     $cust_main->payname.
1738       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1739         ? " (P.O. #". $cust_main->payinfo. ")"
1740         : ''
1741       )
1742   ;
1743   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1744     if $cust_main->company;
1745   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1746   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1747     if $cust_main->address2;
1748   $FS::cust_bill::_template::address[$l++] =
1749     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1750
1751   my $countrydefault = $conf->config('countrydefault') || 'US';
1752   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1753     unless $cust_main->country eq $countrydefault;
1754
1755         #  #overdue? (variable for the template)
1756         #  $FS::cust_bill::_template::overdue = ( 
1757         #    $balance_due > 0
1758         #    && $today > $self->_date 
1759         ##    && $self->printed > 1
1760         #    && $self->printed > 0
1761         #  );
1762
1763   #and subroutine for the template
1764   sub FS::cust_bill::_template::invoice_lines {
1765     my $lines = shift || scalar(@buf);
1766     map { 
1767       scalar(@buf) ? shift @buf : [ '', '' ];
1768     }
1769     ( 1 .. $lines );
1770   }
1771
1772   #and fill it in
1773   $FS::cust_bill::_template::page = 1;
1774   my $lines;
1775   my @collect;
1776   while (@buf) {
1777     push @collect, split("\n",
1778       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1779     );
1780     $FS::cust_bill::_template::page++;
1781   }
1782
1783   map "$_\n", @collect;
1784
1785 }
1786
1787 =item print_latex [ TIME [ , TEMPLATE ] ]
1788
1789 Internal method - returns a filename of a filled-in LaTeX template for this
1790 invoice (Note: add ".tex" to get the actual filename).
1791
1792 See print_ps and print_pdf for methods that return PostScript and PDF output.
1793
1794 TIME an optional value used to control the printing of overdue messages.  The
1795 default is now.  It isn't the date of the invoice; that's the `_date' field.
1796 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1797 L<Time::Local> and L<Date::Parse> for conversion functions.
1798
1799 =cut
1800
1801 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1802 sub print_latex {
1803
1804   my( $self, $today, $template ) = @_;
1805   $today ||= time;
1806   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1807     if $DEBUG;
1808
1809   my $cust_main = $self->cust_main;
1810   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1811     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1812
1813   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1814 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1815   #my $balance_due = $self->owed + $pr_total - $cr_total;
1816   my $balance_due = $self->owed + $pr_total;
1817
1818   #create the template
1819   $template ||= $self->_agent_template;
1820   my $templatefile = 'invoice_latex';
1821   my $suffix = length($template) ? "_$template" : '';
1822   $templatefile .= $suffix;
1823   my @invoice_template = map "$_\n", $conf->config($templatefile)
1824     or die "cannot load config file $templatefile";
1825
1826   my($format, $text_template);
1827   if ( grep { /^%%Detail/ } @invoice_template ) {
1828     #change this to a die when the old code is removed
1829     warn "old-style invoice template $templatefile; ".
1830          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1831     $format = 'old';
1832   } else {
1833     $format = 'Text::Template';
1834     $text_template = new Text::Template(
1835       TYPE => 'ARRAY',
1836       SOURCE => \@invoice_template,
1837       DELIMITERS => [ '[@--', '--@]' ],
1838     );
1839
1840     $text_template->compile()
1841       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1842   }
1843
1844   my $returnaddress;
1845   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1846     $returnaddress = join("\n",
1847       $conf->config_orbase('invoice_latexreturnaddress', $template)
1848     );
1849   } else {
1850     $returnaddress = '~';
1851   }
1852
1853   my %invoice_data = (
1854     'custnum'      => $self->custnum,
1855     'invnum'       => $self->invnum,
1856     'date'         => time2str('%b %o, %Y', $self->_date),
1857     'today'        => time2str('%b %o, %Y', $today),
1858     'agent'        => _latex_escape($cust_main->agent->agent),
1859     'agent_custid' => _latex_escape($cust_main->agent_custid),
1860     'payname'      => _latex_escape($cust_main->payname),
1861     'company'      => _latex_escape($cust_main->company),
1862     'address1'     => _latex_escape($cust_main->address1),
1863     'address2'     => _latex_escape($cust_main->address2),
1864     'city'         => _latex_escape($cust_main->city),
1865     'state'        => _latex_escape($cust_main->state),
1866     #'quantity'     => 1,
1867     'zip'          => _latex_escape($cust_main->zip),
1868     'fax'          => _latex_escape($cust_main->fax),
1869     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1870     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1871     'returnaddress' => $returnaddress,
1872     'quantity'     => 1,
1873     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1874     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1875     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1876     'current_charges'  => sprintf('%.2f', $self->charged ),
1877     'previous_balance' => sprintf("%.2f", $pr_total),
1878     'balance'      => sprintf("%.2f", $balance_due),
1879     'duedate'      => $self->balance_due_date,
1880     'ship_enable'  => $conf->exists('invoice-ship_address'),
1881     'unitprices'   => $conf->exists('invoice-unitprice'),
1882   );
1883
1884   my $countrydefault = $conf->config('countrydefault') || 'US';
1885   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1886   foreach ( qw( contact company address1 address2 city state zip country fax) ){
1887     my $method = $prefix.$_;
1888     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1889   }
1890   $invoice_data{'ship_country'} = ''
1891     if ( $invoice_data{'ship_country'} eq $countrydefault );
1892
1893   if ( $cust_main->country eq $countrydefault ) {
1894     $invoice_data{'country'} = '';
1895   } else {
1896     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1897   }
1898
1899   $invoice_data{'notes'} =
1900     join("\n",
1901 #  #do variable substitutions in notes
1902 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1903         $conf->config_orbase('invoice_latexnotes', $template)
1904     );
1905   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1906     if $DEBUG;
1907
1908   #do variable substitution in coupon
1909   foreach my $include (qw( coupon )) {
1910
1911     my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1912
1913     my $inc_tt = new Text::Template (
1914       TYPE       => 'ARRAY',
1915       SOURCE     => [ map "$_\n", @inc_src ],
1916       DELIMITERS => [ '[@--', '--@]' ],
1917     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1918
1919     unless ( $inc_tt->compile() ) {
1920       my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1921       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1922       die $error;
1923     }
1924
1925     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1926
1927     $invoice_data{$include} =~ s/\n+$//
1928   }
1929
1930   $invoice_data{'footer'} =~ s/\n+$//;
1931   $invoice_data{'smallfooter'} =~ s/\n+$//;
1932   $invoice_data{'notes'} =~ s/\n+$//;
1933
1934   $invoice_data{'po_line'} =
1935     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1936       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1937       : '~';
1938
1939   my @filled_in = ();
1940   if ( $format eq 'old' ) {
1941   
1942     my @line_item = ();
1943     my @total_item = ();
1944     while ( @invoice_template ) {
1945       my $line = shift @invoice_template;
1946   
1947       if ( $line =~ /^%%Detail\s*$/ ) {
1948   
1949         while ( ( my $line_item_line = shift @invoice_template )
1950                 !~ /^%%EndDetail\s*$/                            ) {
1951           push @line_item, $line_item_line;
1952         }
1953         foreach my $line_item ( $self->_items ) { #( 'format'=>'latex' ) ) {
1954         #foreach my $line_item ( $self->_items_pkg ) {
1955           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1956           $invoice_data{'description'} =
1957             _latex_escape($line_item->{'description'});
1958           if ( exists $line_item->{'ext_description'} ) {
1959             $invoice_data{'description'} .=
1960               "\\tabularnewline\n~~".
1961               join( "\\tabularnewline\n~~",
1962                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1963                   );
1964           }
1965           $invoice_data{'amount'}       = $line_item->{'amount'};
1966           $invoice_data{'unit_amount'}  = $line_item->{'unit_amount'};
1967           $invoice_data{'quantity'}     = $line_item->{'quantity'};
1968           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1969           push @filled_in,
1970             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1971         }
1972   
1973       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1974   
1975         while ( ( my $total_item_line = shift @invoice_template )
1976                 !~ /^%%EndTotalDetails\s*$/                      ) {
1977           push @total_item, $total_item_line;
1978         }
1979   
1980         my @total_fill = ();
1981   
1982         my $taxtotal = 0;
1983         foreach my $tax ( $self->_items_tax ) {
1984           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1985           $taxtotal += $tax->{'amount'};
1986           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1987           push @total_fill,
1988             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1989                 @total_item;
1990         }
1991
1992         if ( $taxtotal ) {
1993           $invoice_data{'total_item'} = 'Sub-total';
1994           $invoice_data{'total_amount'} =
1995             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1996           unshift @total_fill,
1997             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1998                 @total_item;
1999         }
2000   
2001         $invoice_data{'total_item'} = '\textbf{Total}';
2002         $invoice_data{'total_amount'} =
2003           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2004         push @total_fill,
2005           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2006               @total_item;
2007   
2008         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2009   
2010         # credits
2011         foreach my $credit ( $self->_items_credits ) {
2012           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
2013           #$credittotal
2014           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
2015           push @total_fill, 
2016             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2017                 @total_item;
2018         }
2019   
2020         # payments
2021         foreach my $payment ( $self->_items_payments ) {
2022           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2023           #$paymenttotal
2024           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2025           push @total_fill, 
2026             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2027                 @total_item;
2028         }
2029   
2030         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2031         $invoice_data{'total_amount'} =
2032           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2033         push @total_fill,
2034           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2035               @total_item;
2036   
2037         push @filled_in, @total_fill;
2038   
2039       } else {
2040         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2041         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2042         push @filled_in, $line;
2043       }
2044   
2045     }
2046
2047     sub nounder {
2048       my $var = $1;
2049       $var =~ s/_/\-/g;
2050       $var;
2051     }
2052
2053   } elsif ( $format eq 'Text::Template' ) {
2054
2055     my @detail_items = ();
2056     my @total_items = ();
2057
2058     $invoice_data{'detail_items'} = \@detail_items;
2059     $invoice_data{'total_items'} = \@total_items;
2060   
2061     my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
2062     foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2063       my $detail = {
2064         ext_description => [],
2065       };
2066       $detail->{'ref'} = $line_item->{'pkgnum'};
2067       $detail->{'quantity'} = 1;
2068       $detail->{'description'} = _latex_escape($line_item->{'description'});
2069       if ( exists $line_item->{'ext_description'} ) {
2070         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2071       }
2072       $detail->{'amount'} = $line_item->{'amount'};
2073       $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2074       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2075   
2076       push @detail_items, $detail;
2077     }
2078   
2079   
2080     my $taxtotal = 0;
2081     foreach my $tax ( $self->_items_tax ) {
2082       my $total = {};
2083       $total->{'total_item'} = _latex_escape($tax->{'description'});
2084       $taxtotal += $tax->{'amount'};
2085       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2086       push @total_items, $total;
2087     }
2088   
2089     if ( $taxtotal ) {
2090       $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2091       my $total = {};
2092       $total->{'total_item'} = 'Sub-total';
2093       $total->{'total_amount'} =
2094         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2095       unshift @total_items, $total;
2096     }else{
2097       $invoice_data{'taxtotal'} = '0.00';
2098     }
2099   
2100     {
2101       my $total = {};
2102       $total->{'total_item'} = '\textbf{Total}';
2103       $total->{'total_amount'} =
2104         '\textbf{\dollar '.
2105         sprintf( '%.2f',
2106                  $self->charged + ( $conf->exists('disable_previous_balance')
2107                                     ? 0
2108                                     : $pr_total
2109                                   )
2110                ).
2111       '}';
2112       push @total_items, $total;
2113     }
2114   
2115     unless ($conf->exists('disable_previous_balance')) {
2116       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2117   
2118       # credits
2119       my $credittotal = 0;
2120       foreach my $credit ( $self->_items_credits ) {
2121         my $total;
2122         $total->{'total_item'} = _latex_escape($credit->{'description'});
2123         $credittotal += $credit->{'amount'};
2124         $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2125         push @total_items, $total;
2126       }
2127       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2128   
2129       # payments
2130       my $paymenttotal = 0;
2131       foreach my $payment ( $self->_items_payments ) {
2132         my $total = {};
2133         $total->{'total_item'} = _latex_escape($payment->{'description'});
2134         $paymenttotal += $payment->{'amount'};
2135         $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2136         push @total_items, $total;
2137       }
2138       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2139   
2140       { 
2141         my $total;
2142         $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2143         $total->{'total_amount'} =
2144           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2145         push @total_items, $total;
2146       }
2147     }
2148
2149   } else {
2150     die "guru meditation #54";
2151   }
2152
2153   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2154   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2155                            DIR      => $dir,
2156                            SUFFIX   => '.tex',
2157                            UNLINK   => 0,
2158                          ) or die "can't open temp file: $!\n";
2159   if ( $format eq 'old' ) {
2160     print $fh join('', @filled_in );
2161   } elsif ( $format eq 'Text::Template' ) {
2162     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2163   } else {
2164     die "guru meditation #32";
2165   }
2166   close $fh;
2167
2168   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2169   return $1;
2170
2171 }
2172
2173 =item print_ps [ TIME [ , TEMPLATE ] ]
2174
2175 Returns an postscript invoice, as a scalar.
2176
2177 TIME an optional value used to control the printing of overdue messages.  The
2178 default is now.  It isn't the date of the invoice; that's the `_date' field.
2179 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2180 L<Time::Local> and L<Date::Parse> for conversion functions.
2181
2182 =cut
2183
2184 sub print_ps {
2185   my $self = shift;
2186
2187   my $file = $self->print_latex(@_);
2188   my $ps = generate_ps($file);
2189   
2190   $ps;
2191 }
2192
2193 =item print_pdf [ TIME [ , TEMPLATE ] ]
2194
2195 Returns an PDF invoice, as a scalar.
2196
2197 TIME an optional value used to control the printing of overdue messages.  The
2198 default is now.  It isn't the date of the invoice; that's the `_date' field.
2199 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2200 L<Time::Local> and L<Date::Parse> for conversion functions.
2201
2202 =cut
2203
2204 sub print_pdf {
2205   my $self = shift;
2206
2207   my $file = $self->print_latex(@_);
2208   my $pdf = generate_pdf($file);
2209   
2210   $pdf;
2211 }
2212
2213 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2214
2215 Returns an HTML invoice, as a scalar.
2216
2217 TIME an optional value used to control the printing of overdue messages.  The
2218 default is now.  It isn't the date of the invoice; that's the `_date' field.
2219 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2220 L<Time::Local> and L<Date::Parse> for conversion functions.
2221
2222 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2223 when emailing the invoice as part of a multipart/related MIME email.
2224
2225 =cut
2226
2227 #some falze laziness w/print_text and print_latex (and send_csv)
2228 sub print_html {
2229   my( $self, $today, $template, $cid ) = @_;
2230   $today ||= time;
2231
2232   my $cust_main = $self->cust_main;
2233   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2234     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2235
2236   $template ||= $self->_agent_template;
2237   my $templatefile = 'invoice_html';
2238   my $suffix = length($template) ? "_$template" : '';
2239   $templatefile .= $suffix;
2240   my @html_template = map "$_\n", $conf->config($templatefile)
2241     or die "cannot load config file $templatefile";
2242
2243   my $html_template = new Text::Template(
2244     TYPE   => 'ARRAY',
2245     SOURCE => \@html_template,
2246     DELIMITERS => [ '<%=', '%>' ],
2247   );
2248
2249   $html_template->compile()
2250     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2251
2252   my %invoice_data = (
2253     'custnum'      => $self->custnum,
2254     'invnum'       => $self->invnum,
2255     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2256     'today'        => time2str('%b %o, %Y', $today),
2257     'agent'        => encode_entities($cust_main->agent->agent),
2258     'agent_custid' => encode_entities($cust_main->agent_custid),
2259     'payname'      => encode_entities($cust_main->payname),
2260     'company'      => encode_entities($cust_main->company),
2261     'address1'     => encode_entities($cust_main->address1),
2262     'address2'     => encode_entities($cust_main->address2),
2263     'city'         => encode_entities($cust_main->city),
2264     'state'        => encode_entities($cust_main->state),
2265     'zip'          => encode_entities($cust_main->zip),
2266     'fax'          => encode_entities($cust_main->fax),
2267     'terms'        => $conf->config('invoice_default_terms')
2268                       || 'Payable upon receipt',
2269     'cid'          => $cid,
2270     'template'     => $template,
2271     'ship_enable'  => $conf->exists('invoice-ship_address'),
2272     'unitprices'   => $conf->exists('invoice-unitprice'),
2273 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2274   );
2275
2276   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2277   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2278     my $method = $prefix.$_;
2279     $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2280   }
2281
2282   if (
2283          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2284       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2285   ) {
2286     $invoice_data{'returnaddress'} =
2287       join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2288   } else {
2289     $invoice_data{'returnaddress'} =
2290       join("\n", map { 
2291                        s/~/&nbsp;/g;
2292                        s/\\\\\*?\s*$/<BR>/;
2293                        s/\\hyphenation\{[\w\s\-]+\}//;
2294                        s/\\([&])/$1/g;
2295                        $_;
2296                      }
2297                      $conf->config_orbase( 'invoice_latexreturnaddress',
2298                                            $template
2299                                          )
2300           );
2301   }
2302
2303   my $countrydefault = $conf->config('countrydefault') || 'US';
2304   if ( $cust_main->country eq $countrydefault ) {
2305     $invoice_data{'country'} = '';
2306   } else {
2307     $invoice_data{'country'} =
2308       encode_entities(code2country($cust_main->country));
2309   }
2310
2311   if (
2312          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2313       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2314   ) {
2315     $invoice_data{'notes'} =
2316       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2317   } else {
2318     $invoice_data{'notes'} = 
2319       join("\n", map { 
2320                        s/%%(.*)$/<!-- $1 -->/g;
2321                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2322                        s/\\begin\{enumerate\}/<ol>/g;
2323                        s/\\item /  <li>/g;
2324                        s/\\end\{enumerate\}/<\/ol>/g;
2325                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2326                        s/\\\\\*/<br>/g;
2327                        s/\\dollar ?/\$/g;
2328                        s/\\#/#/g;
2329                        s/~/&nbsp;/g;
2330                        $_;
2331                      } 
2332                      $conf->config_orbase('invoice_latexnotes', $template)
2333           );
2334   }
2335
2336 #  #do variable substitutions in notes
2337 #  $invoice_data{'notes'} =
2338 #    join("\n",
2339 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2340 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2341 #    );
2342
2343   if (
2344          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2345       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2346   ) {
2347    $invoice_data{'footer'} =
2348      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2349   } else {
2350    $invoice_data{'footer'} =
2351        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2352                       $conf->config_orbase('invoice_latexfooter', $template)
2353            );
2354   }
2355
2356   $invoice_data{'po_line'} =
2357     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2358       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2359       : '';
2360
2361   my $money_char = $conf->config('money_char') || '$';
2362
2363   my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2364   foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2365     my $detail = {
2366       ext_description => [],
2367     };
2368     $detail->{'ref'} = $line_item->{'pkgnum'};
2369     $detail->{'description'} = encode_entities($line_item->{'description'});
2370     if ( exists $line_item->{'ext_description'} ) {
2371       @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2372     }
2373     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2374     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2375
2376     push @{$invoice_data{'detail_items'}}, $detail;
2377   }
2378
2379
2380   my $taxtotal = 0;
2381   foreach my $tax ( $self->_items_tax ) {
2382     my $total = {};
2383     $total->{'total_item'} = encode_entities($tax->{'description'});
2384     $taxtotal += $tax->{'amount'};
2385     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2386     push @{$invoice_data{'total_items'}}, $total;
2387   }
2388
2389   if ( $taxtotal ) {
2390     my $total = {};
2391     $total->{'total_item'} = 'Sub-total';
2392     $total->{'total_amount'} =
2393       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2394     unshift @{$invoice_data{'total_items'}}, $total;
2395   }
2396
2397   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2398   {
2399     my $total = {};
2400     $total->{'total_item'} = '<b>Total</b>';
2401     $total->{'total_amount'} =
2402       "<b>$money_char".
2403       sprintf( '%.2f',
2404                $self->charged + ( $conf->exists('disable_previous_balance')
2405                                   ? 0
2406                                   : $pr_total
2407                                 )
2408              ).
2409       '</b>';
2410     push @{$invoice_data{'total_items'}}, $total;
2411   }
2412
2413   unless ($conf->exists('disable_previous_balance')) {
2414     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2415
2416     # credits
2417     foreach my $credit ( $self->_items_credits ) {
2418       my $total;
2419       $total->{'total_item'} = encode_entities($credit->{'description'});
2420       #$credittotal
2421       $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2422       push @{$invoice_data{'total_items'}}, $total;
2423     }
2424
2425     # payments
2426     foreach my $payment ( $self->_items_payments ) {
2427       my $total = {};
2428       $total->{'total_item'} = encode_entities($payment->{'description'});
2429       #$paymenttotal
2430       $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2431       push @{$invoice_data{'total_items'}}, $total;
2432     }
2433
2434     { 
2435       my $total;
2436       $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2437       $total->{'total_amount'} =
2438         "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2439       push @{$invoice_data{'total_items'}}, $total;
2440     }
2441   }
2442
2443   $html_template->fill_in( HASH => \%invoice_data);
2444 }
2445
2446 # quick subroutine for print_latex
2447 #
2448 # There are ten characters that LaTeX treats as special characters, which
2449 # means that they do not simply typeset themselves: 
2450 #      # $ % & ~ _ ^ \ { }
2451 #
2452 # TeX ignores blanks following an escaped character; if you want a blank (as
2453 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2454
2455 sub _latex_escape {
2456   my $value = shift;
2457   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2458   $value =~ s/([<>])/\$$1\$/g;
2459   $value;
2460 }
2461
2462 #utility methods for print_*
2463
2464 sub balance_due_msg {
2465   my $self = shift;
2466   my $msg = 'Balance Due';
2467   return $msg unless $conf->exists('invoice_default_terms');
2468   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2469     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2470   } elsif ( $conf->config('invoice_default_terms') ) {
2471     $msg .= ' - '. $conf->config('invoice_default_terms');
2472   }
2473   $msg;
2474 }
2475
2476 sub balance_due_date {
2477   my $self = shift;
2478   my $duedate = '';
2479   if (    $conf->exists('invoice_default_terms') 
2480        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2481     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2482   }
2483   $duedate;
2484 }
2485
2486 =item invnum_date_pretty
2487
2488 Returns a string with the invoice number and date, for example:
2489 "Invoice #54 (3/20/2008)"
2490
2491 =cut
2492
2493 sub invnum_date_pretty {
2494   my $self = shift;
2495   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2496 }
2497
2498 =item _date_pretty
2499
2500 Returns a string with the date, for example: "3/20/2008"
2501
2502 =cut
2503
2504 sub _date_pretty {
2505   my $self = shift;
2506   time2str('%x', $self->_date);
2507 }
2508
2509 sub _items {
2510   my $self = shift;
2511
2512   #my @display = scalar(@_)
2513   #              ? @_
2514   #              : qw( _items_previous _items_pkg );
2515   #              #: qw( _items_pkg );
2516   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2517   my @display = qw( _items_previous _items_pkg );
2518
2519   my @b = ();
2520   foreach my $display ( @display ) {
2521     push @b, $self->$display(@_);
2522   }
2523   @b;
2524 }
2525
2526 sub _items_previous {
2527   my $self = shift;
2528   my $cust_main = $self->cust_main;
2529   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2530   my @b = ();
2531   foreach ( @pr_cust_bill ) {
2532     push @b, {
2533       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2534                        ' ('. time2str('%x',$_->_date). ')',
2535       #'pkgpart'     => 'N/A',
2536       'pkgnum'      => 'N/A',
2537       'amount'      => sprintf("%.2f", $_->owed),
2538     };
2539   }
2540   @b;
2541
2542   #{
2543   #    'description'     => 'Previous Balance',
2544   #    #'pkgpart'         => 'N/A',
2545   #    'pkgnum'          => 'N/A',
2546   #    'amount'          => sprintf("%10.2f", $pr_total ),
2547   #    'ext_description' => [ map {
2548   #                                 "Invoice ". $_->invnum.
2549   #                                 " (". time2str("%x",$_->_date). ") ".
2550   #                                 sprintf("%10.2f", $_->owed)
2551   #                         } @pr_cust_bill ],
2552
2553   #};
2554 }
2555
2556 sub _items_pkg {
2557   my $self = shift;
2558   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2559   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2560 }
2561
2562 sub _items_tax {
2563   my $self = shift;
2564   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2565   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2566 }
2567
2568 sub _items_cust_bill_pkg {
2569   my $self = shift;
2570   my $cust_bill_pkg = shift;
2571   my %opt = @_;
2572
2573   my $format = $opt{format} || '';
2574   my $escape_function = $opt{escape_function} || sub { shift };
2575
2576   my @b = ();
2577   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2578
2579     my $cust_pkg = $cust_bill_pkg->cust_pkg;
2580
2581     my $desc = $cust_bill_pkg->desc;
2582     $desc = substr($desc, 0, 50). '...'
2583       if $format eq 'latex' && length($desc) > 50;
2584
2585     my %details_opt = ( 'format'          => $format,
2586                         'escape_function' => $escape_function,
2587                       );
2588
2589     if ( $cust_bill_pkg->pkgnum > 0 ) {
2590
2591       if ( $cust_bill_pkg->setup != 0 ) {
2592
2593         my $description = $desc;
2594         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2595
2596         my @d = ();
2597         push @d, map &{$escape_function}($_),
2598                      $cust_pkg->h_labels_short($self->_date)
2599           unless $cust_pkg->part_pkg->hide_svc_detail;
2600
2601         push @d, $cust_bill_pkg->details(%details_opt)
2602           if $cust_bill_pkg->recur == 0;
2603
2604         push @b, {
2605           description     => $description,
2606           #pkgpart         => $part_pkg->pkgpart,
2607           pkgnum          => $cust_bill_pkg->pkgnum,
2608           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2609           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2610           quantity        => $cust_bill_pkg->quantity,
2611           ext_description => \@d,
2612         };
2613       }
2614
2615       if ( $cust_bill_pkg->recur != 0 ) {
2616
2617         my $description = $desc;
2618         unless ( $conf->exists('disable_line_item_date_ranges') ) {
2619           $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2620                           " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2621         }
2622
2623         my @d = ();
2624
2625         #at least until cust_bill_pkg has "past" ranges in addition to
2626         #the "future" sdate/edate ones... see #3032
2627         push @d, map &{$escape_function}($_),
2628                      $cust_pkg->h_labels_short($self->_date)
2629                                                #$cust_bill_pkg->edate,
2630                                                #$cust_bill_pkg->sdate),
2631           unless $cust_pkg->part_pkg->hide_svc_detail
2632               || $cust_bill_pkg->itemdesc;
2633
2634         push @d, $cust_bill_pkg->details(%details_opt);
2635
2636         push @b, {
2637           description     => $description,
2638           #pkgpart         => $part_pkg->pkgpart,
2639           pkgnum          => $cust_bill_pkg->pkgnum,
2640           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2641           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2642           quantity        => $cust_bill_pkg->quantity,
2643           ext_description => \@d,
2644         };
2645
2646       }
2647
2648     } else { #pkgnum tax or one-shot line item (??)
2649
2650       if ( $cust_bill_pkg->setup != 0 ) {
2651         push @b, {
2652           'description' => $desc,
2653           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2654         };
2655       }
2656       if ( $cust_bill_pkg->recur != 0 ) {
2657         push @b, {
2658           'description' => "$desc (".
2659                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2660                            time2str("%x", $cust_bill_pkg->edate). ')',
2661           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2662         };
2663       }
2664
2665     }
2666
2667   }
2668
2669   @b;
2670
2671 }
2672
2673 sub _items_credits {
2674   my $self = shift;
2675
2676   my @b;
2677   #credits
2678   foreach ( $self->cust_credited ) {
2679
2680     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2681
2682     my $reason = $_->cust_credit->reason;
2683     #my $reason = substr($_->cust_credit->reason,0,32);
2684     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2685     $reason = " ($reason) " if $reason;
2686     push @b, {
2687       #'description' => 'Credit ref\#'. $_->crednum.
2688       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2689       #                 $reason,
2690       'description' => 'Credit applied '.
2691                        time2str("%x",$_->cust_credit->_date). $reason,
2692       'amount'      => sprintf("%.2f",$_->amount),
2693     };
2694   }
2695   #foreach ( @cr_cust_credit ) {
2696   #  push @buf,[
2697   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2698   #    $money_char. sprintf("%10.2f",$_->credited)
2699   #  ];
2700   #}
2701
2702   @b;
2703
2704 }
2705
2706 sub _items_payments {
2707   my $self = shift;
2708
2709   my @b;
2710   #get & print payments
2711   foreach ( $self->cust_bill_pay ) {
2712
2713     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2714
2715     push @b, {
2716       'description' => "Payment received ".
2717                        time2str("%x",$_->cust_pay->_date ),
2718       'amount'      => sprintf("%.2f", $_->amount )
2719     };
2720   }
2721
2722   @b;
2723
2724 }
2725
2726
2727 =back
2728
2729 =head1 SUBROUTINES
2730
2731 =over 4
2732
2733 =item process_reprint
2734
2735 =cut
2736
2737 sub process_reprint {
2738   process_re_X('print', @_);
2739 }
2740
2741 =item process_reemail
2742
2743 =cut
2744
2745 sub process_reemail {
2746   process_re_X('email', @_);
2747 }
2748
2749 =item process_refax
2750
2751 =cut
2752
2753 sub process_refax {
2754   process_re_X('fax', @_);
2755 }
2756
2757 =item process_reftp
2758
2759 =cut
2760
2761 sub process_reftp {
2762   process_re_X('ftp', @_);
2763 }
2764
2765 =item respool
2766
2767 =cut
2768
2769 sub process_respool {
2770   process_re_X('spool', @_);
2771 }
2772
2773 use Storable qw(thaw);
2774 use Data::Dumper;
2775 use MIME::Base64;
2776 sub process_re_X {
2777   my( $method, $job ) = ( shift, shift );
2778   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2779
2780   my $param = thaw(decode_base64(shift));
2781   warn Dumper($param) if $DEBUG;
2782
2783   re_X(
2784     $method,
2785     $job,
2786     %$param,
2787   );
2788
2789 }
2790
2791 sub re_X {
2792   my($method, $job, %param ) = @_;
2793   if ( $DEBUG ) {
2794     warn "re_X $method for job $job with param:\n".
2795          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2796   }
2797
2798   #some false laziness w/search/cust_bill.html
2799   my $distinct = '';
2800   my $orderby = 'ORDER BY cust_bill._date';
2801
2802   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2803
2804   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2805      
2806   my @cust_bill = qsearch( {
2807     #'select'    => "cust_bill.*",
2808     'table'     => 'cust_bill',
2809     'addl_from' => $addl_from,
2810     'hashref'   => {},
2811     'extra_sql' => $extra_sql,
2812     'order_by'  => $orderby,
2813     'debug' => 1,
2814   } );
2815
2816   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2817
2818   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2819     if $DEBUG;
2820
2821   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2822   foreach my $cust_bill ( @cust_bill ) {
2823     $cust_bill->$method();
2824
2825     if ( $job ) { #progressbar foo
2826       $num++;
2827       if ( time - $min_sec > $last ) {
2828         my $error = $job->update_statustext(
2829           int( 100 * $num / scalar(@cust_bill) )
2830         );
2831         die $error if $error;
2832         $last = time;
2833       }
2834     }
2835
2836   }
2837
2838 }
2839
2840 =back
2841
2842 =head1 CLASS METHODS
2843
2844 =over 4
2845
2846 =item owed_sql
2847
2848 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2849
2850 =cut
2851
2852 sub owed_sql {
2853   my $class = shift;
2854   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2855 }
2856
2857 =item net_sql
2858
2859 Returns an SQL fragment to retreive the net amount (charged minus credited).
2860
2861 =cut
2862
2863 sub net_sql {
2864   my $class = shift;
2865   'charged - '. $class->credited_sql;
2866 }
2867
2868 =item paid_sql
2869
2870 Returns an SQL fragment to retreive the amount paid against this invoice.
2871
2872 =cut
2873
2874 sub paid_sql {
2875   #my $class = shift;
2876   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2877        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
2878 }
2879
2880 =item credited_sql
2881
2882 Returns an SQL fragment to retreive the amount credited against this invoice.
2883
2884 =cut
2885
2886 sub credited_sql {
2887   #my $class = shift;
2888   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2889        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
2890 }
2891
2892 =item search_sql HASHREF
2893
2894 Class method which returns an SQL WHERE fragment to search for parameters
2895 specified in HASHREF.  Valid parameters are
2896
2897 =over 4
2898
2899 =item begin
2900
2901 Epoch date (UNIX timestamp) setting a lower bound for _date values
2902
2903 =item end
2904
2905 Epoch date (UNIX timestamp) setting an upper bound for _date values
2906
2907 =item invnum_min
2908
2909 =item invnum_max
2910
2911 =item agentnum
2912
2913 =item owed
2914
2915 =item net
2916
2917 =item days
2918
2919 =item newest_percust
2920
2921 =back
2922
2923 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2924
2925 =cut
2926
2927 sub search_sql {
2928   my($class, $param) = @_;
2929   if ( $DEBUG ) {
2930     warn "$me search_sql called with params: \n".
2931          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
2932   }
2933
2934   my @search = ();
2935
2936   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2937     push @search, "cust_bill._date >= $1";
2938   }
2939   if ( $param->{'end'} =~ /^(\d+)$/ ) {
2940     push @search, "cust_bill._date < $1";
2941   }
2942   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2943     push @search, "cust_bill.invnum >= $1";
2944   }
2945   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2946     push @search, "cust_bill.invnum <= $1";
2947   }
2948   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2949     push @search, "cust_main.agentnum = $1";
2950   }
2951
2952   push @search, '0 != '. FS::cust_bill->owed_sql
2953     if $param->{'open'};
2954
2955   push @search, '0 != '. FS::cust_bill->net_sql
2956     if $param->{'net'};
2957
2958   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2959     if $param->{'days'};
2960
2961   if ( $param->{'newest_percust'} ) {
2962
2963     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2964     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2965
2966     my @newest_where = map { my $x = $_;
2967                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
2968                              $x;
2969                            }
2970                            grep ! /^cust_main./, @search;
2971     my $newest_where = scalar(@newest_where)
2972                          ? ' AND '. join(' AND ', @newest_where)
2973                          : '';
2974
2975
2976     push @search, "cust_bill._date = (
2977       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2978         WHERE newest_cust_bill.custnum = cust_bill.custnum
2979           $newest_where
2980     )";
2981
2982   }
2983
2984   my $curuser = $FS::CurrentUser::CurrentUser;
2985   if ( $curuser->username eq 'fs_queue'
2986        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2987     my $username = $1;
2988     my $newuser = qsearchs('access_user', {
2989       'username' => $username,
2990       'disabled' => '',
2991     } );
2992     if ( $newuser ) {
2993       $curuser = $newuser;
2994     } else {
2995       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2996     }
2997   }
2998
2999   push @search, $curuser->agentnums_sql;
3000
3001   join(' AND ', @search );
3002
3003 }
3004
3005 =back
3006
3007 =head1 BUGS
3008
3009 The delete method.
3010
3011 =head1 SEE ALSO
3012
3013 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3014 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3015 documentation.
3016
3017 =cut
3018
3019 1;
3020