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