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