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