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