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