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