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