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