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