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