add previous_balance-summary_only config, RT#4404
[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   my( $self, $today, $template ) = @_;
1641
1642   my %params = ( 'format' => 'latex' );
1643   $params{'time'} = $today if $today;
1644   $params{'template'} = $template if $template;
1645
1646   $template ||= $self->_agent_template;
1647
1648   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1649   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1650                            DIR      => $dir,
1651                            SUFFIX   => '.eps',
1652                            UNLINK   => 0,
1653                          ) or die "can't open temp file: $!\n";
1654
1655   my $agentnum = $self->cust_main->agentnum;
1656
1657   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1658     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1659       or die "can't write temp file: $!\n";
1660   } else {
1661     print $lh $conf->config_binary('logo.eps', $agentnum)
1662       or die "can't write temp file: $!\n";
1663   }
1664   close $lh;
1665   $params{'logo_file'} = $lh->filename;
1666
1667   my @filled_in = $self->print_generic( %params );
1668   
1669   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1670                            DIR      => $dir,
1671                            SUFFIX   => '.tex',
1672                            UNLINK   => 0,
1673                          ) or die "can't open temp file: $!\n";
1674   print $fh join('', @filled_in );
1675   close $fh;
1676
1677   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1678   return ($1, $params{'logo_file'});
1679
1680 }
1681
1682 =item print_generic OPTIONS_HASH
1683
1684 Internal method - returns a filled-in template for this invoice as a scalar.
1685
1686 See print_ps and print_pdf for methods that return PostScript and PDF output.
1687
1688 Non optional options include 
1689   format - latex, html, template
1690
1691 Optional options include
1692
1693 template - a value used as a suffix for a configuration template
1694
1695 time - a value used to control the printing of overdue messages.  The
1696 default is now.  It isn't the date of the invoice; that's the `_date' field.
1697 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1698 L<Time::Local> and L<Date::Parse> for conversion functions.
1699
1700 cid - 
1701
1702 unsquelch_cdr - overrides any per customer cdr squelching when true
1703
1704 =cut
1705
1706 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
1707 # (alignment?) problems to change them all to '%.2f' ?
1708 sub print_generic {
1709
1710   my( $self, %params ) = @_;
1711   my $today = $params{today} ? $params{today} : time;
1712   warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1713     if $DEBUG;
1714
1715   my $format = $params{format};
1716   die "Unknown format: $format"
1717     unless $format =~ /^(latex|html|template)$/;
1718
1719   my $cust_main = $self->cust_main;
1720   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1721     unless $cust_main->payname
1722         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1723
1724   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
1725                      'html'     => [ '<%=', '%>' ],
1726                      'template' => [ '{', '}' ],
1727                    );
1728
1729   #create the template
1730   my $template = $params{template} ? $params{template} : $self->_agent_template;
1731   my $templatefile = "invoice_$format";
1732   $templatefile .= "_$template"
1733     if length($template);
1734   my @invoice_template = map "$_\n", $conf->config($templatefile)
1735     or die "cannot load config data $templatefile";
1736
1737   my $old_latex = '';
1738   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1739     #change this to a die when the old code is removed
1740     warn "old-style invoice template $templatefile; ".
1741          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1742     $old_latex = 'true';
1743     @invoice_template = _translate_old_latex_format(@invoice_template);
1744   } 
1745
1746   my $text_template = new Text::Template(
1747     TYPE => 'ARRAY',
1748     SOURCE => \@invoice_template,
1749     DELIMITERS => $delimiters{$format},
1750   );
1751
1752   $text_template->compile()
1753     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1754
1755
1756   # additional substitution could possibly cause breakage in existing templates
1757   my %convert_maps = ( 
1758     'latex' => {
1759                  'notes'         => sub { map "$_", @_ },
1760                  'footer'        => sub { map "$_", @_ },
1761                  'smallfooter'   => sub { map "$_", @_ },
1762                  'returnaddress' => sub { map "$_", @_ },
1763                  'coupon'        => sub { map "$_", @_ },
1764                },
1765     'html'  => {
1766                  'notes' =>
1767                    sub {
1768                      map { 
1769                        s/%%(.*)$/<!-- $1 -->/g;
1770                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1771                        s/\\begin\{enumerate\}/<ol>/g;
1772                        s/\\item /  <li>/g;
1773                        s/\\end\{enumerate\}/<\/ol>/g;
1774                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1775                        s/\\\\\*/<br>/g;
1776                        s/\\dollar ?/\$/g;
1777                        s/\\#/#/g;
1778                        s/~/&nbsp;/g;
1779                        $_;
1780                      }  @_
1781                    },
1782                  'footer' =>
1783                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1784                  'smallfooter' =>
1785                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1786                  'returnaddress' =>
1787                    sub {
1788                      map { 
1789                        s/~/&nbsp;/g;
1790                        s/\\\\\*?\s*$/<BR>/;
1791                        s/\\hyphenation\{[\w\s\-]+}//;
1792                        s/\\([&])/$1/g;
1793                        $_;
1794                      }  @_
1795                    },
1796                  'coupon'        => sub { "" },
1797                },
1798     'template' => {
1799                  'notes' =>
1800                    sub {
1801                      map { 
1802                        s/%%.*$//g;
1803                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1804                        s/\\begin\{enumerate\}//g;
1805                        s/\\item /  * /g;
1806                        s/\\end\{enumerate\}//g;
1807                        s/\\textbf\{(.*)\}/$1/g;
1808                        s/\\\\\*/ /;
1809                        s/\\dollar ?/\$/g;
1810                        $_;
1811                      }  @_
1812                    },
1813                  'footer' =>
1814                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1815                  'smallfooter' =>
1816                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1817                  'returnaddress' =>
1818                    sub {
1819                      map { 
1820                        s/~/ /g;
1821                        s/\\\\\*?\s*$/\n/;             # dubious
1822                        s/\\hyphenation\{[\w\s\-]+}//;
1823                        $_;
1824                      }  @_
1825                    },
1826                  'coupon'        => sub { "" },
1827                },
1828   );
1829
1830
1831   # hashes for differing output formats
1832   my %nbsps = ( 'latex'    => '~',
1833                 'html'     => '',    # '&nbps;' would be nice
1834                 'template' => '',    # not used
1835               );
1836   my $nbsp = $nbsps{$format};
1837
1838   my %escape_functions = ( 'latex'    => \&_latex_escape,
1839                            'html'     => \&encode_entities,
1840                            'template' => sub { shift },
1841                          );
1842   my $escape_function = $escape_functions{$format};
1843
1844   my %date_formats = ( 'latex'    => '%b %o, %Y',
1845                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
1846                        'template' => '%s',
1847                      );
1848   my $date_format = $date_formats{$format};
1849
1850   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
1851                                                },
1852                              'html'     => sub { return '<b>'. shift(). '</b>'
1853                                                },
1854                              'template' => sub { shift },
1855                            );
1856   my $embolden_function = $embolden_functions{$format};
1857
1858
1859   # generate template variables
1860   my $returnaddress;
1861   if (
1862          defined( $conf->config_orbase( "invoice_${format}returnaddress",
1863                                         $template
1864                                       )
1865                 )
1866        && length( $conf->config_orbase( "invoice_${format}returnaddress",
1867                                         $template
1868                                       )
1869                 )
1870   ) {
1871
1872     $returnaddress = join("\n",
1873       $conf->config_orbase("invoice_${format}returnaddress", $template)
1874     );
1875
1876   } elsif ( grep /\S/,
1877             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1878
1879     my $convert_map = $convert_maps{$format}{'returnaddress'};
1880     $returnaddress =
1881       join( "\n",
1882             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1883                                                  $template
1884                                                )
1885                          )
1886           );
1887   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1888
1889     my $convert_map = $convert_maps{$format}{'returnaddress'};
1890     $returnaddress = join( "\n", &$convert_map(
1891                                    map { s/( {2,})/'~' x length($1)/eg;
1892                                          s/$/\\\\\*/;
1893                                          $_
1894                                        }
1895                                      ( $conf->config('company_name', $self->cust_main->agentnum),
1896                                        $conf->config('company_address', $self->cust_main->agentnum),
1897                                      )
1898                                  )
1899                      );
1900
1901   } else {
1902
1903     my $warning = "Couldn't find a return address; ".
1904                   "do you need to set the company_address configuration value?";
1905     warn "$warning\n";
1906     $returnaddress = $nbsp;
1907     #$returnaddress = $warning;
1908
1909   }
1910
1911   my %invoice_data = (
1912     'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
1913     'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
1914     'custnum'         => $cust_main->display_custnum,
1915     'invnum'          => $self->invnum,
1916     'date'            => time2str($date_format, $self->_date),
1917     'today'           => time2str('%b %o, %Y', $today),
1918     'agent'           => &$escape_function($cust_main->agent->agent),
1919     'agent_custid'    => &$escape_function($cust_main->agent_custid),
1920     'payname'         => &$escape_function($cust_main->payname),
1921     'company'         => &$escape_function($cust_main->company),
1922     'address1'        => &$escape_function($cust_main->address1),
1923     'address2'        => &$escape_function($cust_main->address2),
1924     'city'            => &$escape_function($cust_main->city),
1925     'state'           => &$escape_function($cust_main->state),
1926     'zip'             => &$escape_function($cust_main->zip),
1927     'fax'             => &$escape_function($cust_main->fax),
1928     'returnaddress'   => $returnaddress,
1929     #'quantity'        => 1,
1930     'terms'           => $self->terms,
1931     'template'        => $template, #params{'template'},
1932     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
1933     # better hang on to conf_dir for a while
1934     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1935     'page'            => 1,
1936     'total_pages'     => 1,
1937     'current_charges' => sprintf("%.2f", $self->charged),
1938     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
1939     'ship_enable'     => $conf->exists('invoice-ship_address'),
1940     'unitprices'      => $conf->exists('invoice-unitprice'),
1941   );
1942
1943   my $countrydefault = $conf->config('countrydefault') || 'US';
1944   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1945   foreach ( qw( contact company address1 address2 city state zip country fax) ){
1946     my $method = $prefix.$_;
1947     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1948   }
1949   $invoice_data{'ship_country'} = ''
1950     if ( $invoice_data{'ship_country'} eq $countrydefault );
1951   
1952   $invoice_data{'cid'} = $params{'cid'}
1953     if $params{'cid'};
1954
1955   if ( $cust_main->country eq $countrydefault ) {
1956     $invoice_data{'country'} = '';
1957   } else {
1958     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1959   }
1960
1961   my @address = ();
1962   $invoice_data{'address'} = \@address;
1963   push @address,
1964     $cust_main->payname.
1965       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1966         ? " (P.O. #". $cust_main->payinfo. ")"
1967         : ''
1968       )
1969   ;
1970   push @address, $cust_main->company
1971     if $cust_main->company;
1972   push @address, $cust_main->address1;
1973   push @address, $cust_main->address2
1974     if $cust_main->address2;
1975   push @address,
1976     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1977   push @address, $invoice_data{'country'}
1978     if $invoice_data{'country'};
1979   push @address, ''
1980     while (scalar(@address) < 5);
1981
1982   $invoice_data{'logo_file'} = $params{'logo_file'}
1983     if $params{'logo_file'};
1984
1985   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1986 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1987   #my $balance_due = $self->owed + $pr_total - $cr_total;
1988   my $balance_due = $self->owed + $pr_total;
1989   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1990   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1991
1992   #do variable substitution in notes, footer, smallfooter
1993   foreach my $include (qw( notes footer smallfooter coupon )) {
1994
1995     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1996     my @inc_src;
1997
1998     if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1999
2000       @inc_src = $conf->config($inc_file);
2001
2002     } else {
2003
2004       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2005
2006       my $convert_map = $convert_maps{$format}{$include};
2007
2008       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2009                        s/--\@\]/$delimiters{$format}[1]/g;
2010                        $_;
2011                      } 
2012                  &$convert_map( $conf->config($inc_file) );
2013
2014     }
2015
2016     my $inc_tt = new Text::Template (
2017       TYPE       => 'ARRAY',
2018       SOURCE     => [ map "$_\n", @inc_src ],
2019       DELIMITERS => $delimiters{$format},
2020     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2021
2022     unless ( $inc_tt->compile() ) {
2023       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2024       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2025       die $error;
2026     }
2027
2028     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2029
2030     $invoice_data{$include} =~ s/\n+$//
2031       if ($format eq 'latex');
2032   }
2033
2034   $invoice_data{'po_line'} =
2035     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2036       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2037       : $nbsp;
2038
2039   my %money_chars = ( 'latex'    => '',
2040                       'html'     => $conf->config('money_char') || '$',
2041                       'template' => '',
2042                     );
2043   my $money_char = $money_chars{$format};
2044
2045   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2046                             'html'     => $conf->config('money_char') || '$',
2047                             'template' => '',
2048                           );
2049   my $other_money_char = $other_money_chars{$format};
2050
2051   my @detail_items = ();
2052   my @total_items = ();
2053   my @buf = ();
2054   my @sections = ();
2055
2056   $invoice_data{'detail_items'} = \@detail_items;
2057   $invoice_data{'total_items'} = \@total_items;
2058   $invoice_data{'buf'} = \@buf;
2059   $invoice_data{'sections'} = \@sections;
2060   
2061   my $previous_section = { 'description' => 'Previous Charges',
2062                            'subtotal'    => $other_money_char.
2063                                             sprintf('%.2f', $pr_total),
2064                          };
2065
2066   my $taxtotal = 0;
2067   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2068                       'subtotal'    => $taxtotal }; # adjusted below
2069
2070   my $adjusttotal = 0;
2071   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2072                          'subtotal'    => 0 }; # adjusted below
2073
2074   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2075   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2076   my $late_sections = [];
2077   if ( $multisection ) {
2078     push @sections, $self->_items_sections( $late_sections );
2079   }else{
2080     push @sections, { 'description' => '', 'subtotal' => '' };
2081   }
2082
2083   unless (    $conf->exists('disable_previous_balance')
2084            || $conf->exists('previous_balance-summary_only')
2085          )
2086   {
2087
2088     foreach my $line_item ( $self->_items_previous ) {
2089
2090       my $detail = {
2091         ext_description => [],
2092       };
2093       $detail->{'ref'} = $line_item->{'pkgnum'};
2094       $detail->{'quantity'} = 1;
2095       $detail->{'section'} = $previous_section;
2096       $detail->{'description'} = &$escape_function($line_item->{'description'});
2097       if ( exists $line_item->{'ext_description'} ) {
2098         @{$detail->{'ext_description'}} = map {
2099           &$escape_function($_);
2100         } @{$line_item->{'ext_description'}};
2101       }
2102       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2103                             $line_item->{'amount'};
2104       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2105
2106       push @detail_items, $detail;
2107       push @buf, [ $detail->{'description'},
2108                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2109                  ];
2110     }
2111
2112   }
2113
2114   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2115     push @buf, ['','-----------'];
2116     push @buf, [ 'Total Previous Balance',
2117                  $money_char. sprintf("%10.2f", $pr_total) ];
2118     push @buf, ['',''];
2119   }
2120
2121   foreach my $section (@sections, @$late_sections) {
2122
2123     $section->{'subtotal'} = $other_money_char.
2124                              sprintf('%.2f', $section->{'subtotal'})
2125       if $multisection;
2126
2127     if ( $section->{'description'} ) {
2128       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2129                    [ '', '' ],
2130                  );
2131     }
2132
2133     my %options = ();
2134     $options{'section'} = $section if $multisection;
2135     $options{'format'} = $format;
2136     $options{'escape_function'} = $escape_function;
2137     $options{'format_function'} = sub { () } unless $unsquelched;
2138     $options{'unsquelched'} = $unsquelched;
2139
2140     foreach my $line_item ( $self->_items_pkg(%options) ) {
2141       my $detail = {
2142         ext_description => [],
2143       };
2144       $detail->{'ref'} = $line_item->{'pkgnum'};
2145       $detail->{'quantity'} = $line_item->{'quantity'};
2146       $detail->{'section'} = $section;
2147       $detail->{'description'} = &$escape_function($line_item->{'description'});
2148       if ( exists $line_item->{'ext_description'} ) {
2149         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2150       }
2151       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2152                               $line_item->{'amount'};
2153       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2154                                  $line_item->{'unit_amount'};
2155       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2156   
2157       push @detail_items, $detail;
2158       push @buf, ( [ $detail->{'description'},
2159                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2160                    ],
2161                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2162                  );
2163     }
2164
2165     if ( $section->{'description'} ) {
2166       push @buf, ( ['','-----------'],
2167                    [ $section->{'description'}. ' sub-total',
2168                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2169                    ],
2170                    [ '', '' ],
2171                    [ '', '' ],
2172                  );
2173     }
2174   
2175   }
2176   
2177   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2178     unshift @sections, $previous_section if $pr_total;
2179   }
2180
2181   foreach my $tax ( $self->_items_tax ) {
2182
2183     $taxtotal += $tax->{'amount'};
2184
2185     my $description = &$escape_function( $tax->{'description'} );
2186     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2187
2188     if ( $multisection ) {
2189
2190       my $money = $old_latex ? '' : $money_char;
2191       push @detail_items, {
2192         ext_description => [],
2193         ref          => '',
2194         quantity     => '',
2195         description  => $description,
2196         amount       => $money. $amount,
2197         product_code => '',
2198         section      => $tax_section,
2199       };
2200
2201     } else {
2202
2203       push @total_items, {
2204         'total_item'   => $description,
2205         'total_amount' => $other_money_char. $amount,
2206       };
2207
2208     }
2209
2210     push @buf,[ $description,
2211                 $money_char. $amount,
2212               ];
2213
2214   }
2215   
2216   if ( $taxtotal ) {
2217     my $total = {};
2218     $total->{'total_item'} = 'Sub-total';
2219     $total->{'total_amount'} =
2220       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2221
2222     if ( $multisection ) {
2223       $tax_section->{'subtotal'} = $other_money_char.
2224                                    sprintf('%.2f', $taxtotal);
2225       $tax_section->{'pretotal'} = 'New charges sub-total '.
2226                                    $total->{'total_amount'};
2227       push @sections, $tax_section if $taxtotal;
2228     }else{
2229       unshift @total_items, $total;
2230     }
2231   }
2232   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2233   
2234   push @buf,['','-----------'];
2235   push @buf,[( $conf->exists('disable_previous_balance') 
2236                ? 'Total Charges'
2237                : 'Total New Charges'
2238              ),
2239              $money_char. sprintf("%10.2f",$self->charged) ];
2240   push @buf,['',''];
2241
2242   {
2243     my $total = {};
2244     $total->{'total_item'} = &$embolden_function('Total');
2245     $total->{'total_amount'} =
2246       &$embolden_function(
2247         $other_money_char.
2248         sprintf( '%.2f',
2249                  $self->charged + ( $conf->exists('disable_previous_balance')
2250                                     ? 0
2251                                     : $pr_total
2252                                   )
2253                )
2254       );
2255     if ( $multisection ) {
2256       $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2257                                       sprintf('%.2f', $self->charged );
2258     }else{
2259       push @total_items, $total;
2260     }
2261     push @buf,['','-----------'];
2262     push @buf,['Total Charges',
2263                $money_char.
2264                sprintf( '%10.2f', $self->charged +
2265                                     ( $conf->exists('disable_previous_balance')
2266                                         ? 0
2267                                         : $pr_total
2268                                     )
2269                       )
2270               ];
2271     push @buf,['',''];
2272   }
2273   
2274   unless ( $conf->exists('disable_previous_balance') ) {
2275     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2276   
2277     # credits
2278     my $credittotal = 0;
2279     foreach my $credit ( $self->_items_credits ) {
2280       my $total;
2281       $total->{'total_item'} = &$escape_function($credit->{'description'});
2282       $credittotal += $credit->{'amount'};
2283       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2284       $adjusttotal += $credit->{'amount'};
2285       if ( $multisection ) {
2286         my $money = $old_latex ? '' : $money_char;
2287         push @detail_items, {
2288           ext_description => [],
2289           ref          => '',
2290           quantity     => '',
2291           description  => &$escape_function($credit->{'description'}),
2292           amount       => $money. $credit->{'amount'},
2293           product_code => '',
2294           section      => $adjust_section,
2295         };
2296       }else{
2297         push @total_items, $total;
2298       }
2299     }
2300     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2301   
2302     # credits (again)
2303     foreach ( $self->cust_credited ) {
2304   
2305       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2306
2307       my $reason = substr($_->cust_credit->reason,0,32);
2308       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2309       $reason = " ($reason) " if $reason;
2310       push @buf,[
2311         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
2312         $money_char. sprintf("%10.2f",$_->amount)
2313       ];
2314     }
2315
2316     # payments
2317     my $paymenttotal = 0;
2318     foreach my $payment ( $self->_items_payments ) {
2319       my $total = {};
2320       $total->{'total_item'} = &$escape_function($payment->{'description'});
2321       $paymenttotal += $payment->{'amount'};
2322       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2323       $adjusttotal += $payment->{'amount'};
2324       if ( $multisection ) {
2325         my $money = $old_latex ? '' : $money_char;
2326         push @detail_items, {
2327           ext_description => [],
2328           ref          => '',
2329           quantity     => '',
2330           description  => &$escape_function($payment->{'description'}),
2331           amount       => $money. $payment->{'amount'},
2332           product_code => '',
2333           section      => $adjust_section,
2334         };
2335       }else{
2336         push @total_items, $total;
2337       }
2338       push @buf, [ $payment->{'description'},
2339                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2340                  ];
2341     }
2342     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2343   
2344     if ( $multisection ) {
2345       $adjust_section->{'subtotal'} = $other_money_char.
2346                                       sprintf('%.2f', $adjusttotal);
2347       push @sections, $adjust_section;
2348     }
2349
2350     { 
2351       my $total;
2352       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2353       $total->{'total_amount'} =
2354         &$embolden_function(
2355           $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2356         );
2357       if ( $multisection ) {
2358         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2359                                          $total->{'total_amount'};
2360       }else{
2361         push @total_items, $total;
2362       }
2363       push @buf,['','-----------'];
2364       push @buf,[$self->balance_due_msg, $money_char. 
2365         sprintf("%10.2f", $balance_due ) ];
2366     }
2367   }
2368
2369   if ( $multisection ) {
2370     push @sections, @$late_sections
2371       if $unsquelched;
2372   }
2373
2374   $invoice_lines = 0;
2375   my $wasfunc = 0;
2376   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2377     /invoice_lines\((\d*)\)/;
2378     $invoice_lines += $1 || scalar(@buf);
2379     $wasfunc=1;
2380   }
2381   die "no invoice_lines() functions in template?"
2382     if ( $format eq 'template' && !$wasfunc );
2383
2384   if ($format eq 'template') {
2385
2386     if ( $invoice_lines ) {
2387       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2388       $invoice_data{'total_pages'}++
2389         if scalar(@buf) % $invoice_lines;
2390     }
2391
2392     #setup subroutine for the template
2393     sub FS::cust_bill::_template::invoice_lines {
2394       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2395       map { 
2396         scalar(@FS::cust_bill::_template::buf)
2397           ? shift @FS::cust_bill::_template::buf
2398           : [ '', '' ];
2399       }
2400       ( 1 .. $lines );
2401     }
2402
2403     my $lines;
2404     my @collect;
2405     while (@buf) {
2406       push @collect, split("\n",
2407         $text_template->fill_in( HASH => \%invoice_data,
2408                                  PACKAGE => 'FS::cust_bill::_template'
2409                                )
2410       );
2411       $FS::cust_bill::_template::page++;
2412     }
2413     map "$_\n", @collect;
2414   }else{
2415     warn "filling in template for invoice ". $self->invnum. "\n"
2416       if $DEBUG;
2417     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2418       if $DEBUG > 1;
2419
2420     $text_template->fill_in(HASH => \%invoice_data);
2421   }
2422 }
2423
2424 =item print_ps [ TIME [ , TEMPLATE ] ]
2425
2426 Returns an postscript invoice, as a scalar.
2427
2428 TIME an optional value used to control the printing of overdue messages.  The
2429 default is now.  It isn't the date of the invoice; that's the `_date' field.
2430 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2431 L<Time::Local> and L<Date::Parse> for conversion functions.
2432
2433 =cut
2434
2435 sub print_ps {
2436   my $self = shift;
2437
2438   my ($file, $lfile) = $self->print_latex(@_);
2439   my $ps = generate_ps($file);
2440   unlink($lfile);
2441
2442   $ps;
2443 }
2444
2445 =item print_pdf [ TIME [ , TEMPLATE ] ]
2446
2447 Returns an PDF invoice, as a scalar.
2448
2449 TIME an optional value used to control the printing of overdue messages.  The
2450 default is now.  It isn't the date of the invoice; that's the `_date' field.
2451 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2452 L<Time::Local> and L<Date::Parse> for conversion functions.
2453
2454 =cut
2455
2456 sub print_pdf {
2457   my $self = shift;
2458
2459   my ($file, $lfile) = $self->print_latex(@_);
2460   my $pdf = generate_pdf($file);
2461   unlink($lfile);
2462
2463   $pdf;
2464 }
2465
2466 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2467
2468 Returns an HTML invoice, as a scalar.
2469
2470 TIME an optional value used to control the printing of overdue messages.  The
2471 default is now.  It isn't the date of the invoice; that's the `_date' field.
2472 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2473 L<Time::Local> and L<Date::Parse> for conversion functions.
2474
2475 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2476 when emailing the invoice as part of a multipart/related MIME email.
2477
2478 =cut
2479
2480 sub print_html {
2481   my $self = shift;
2482   my %params;
2483   if ( ref $_[0]  ) {
2484     %params = %{ shift() }; 
2485   }else{
2486     $params{'time'} = shift;
2487     $params{'template'} = shift;
2488     $params{'cid'} = shift;
2489   }
2490
2491   $params{'format'} = 'html';
2492
2493   $self->print_generic( %params );
2494 }
2495
2496 # quick subroutine for print_latex
2497 #
2498 # There are ten characters that LaTeX treats as special characters, which
2499 # means that they do not simply typeset themselves: 
2500 #      # $ % & ~ _ ^ \ { }
2501 #
2502 # TeX ignores blanks following an escaped character; if you want a blank (as
2503 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2504
2505 sub _latex_escape {
2506   my $value = shift;
2507   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2508   $value =~ s/([<>])/\$$1\$/g;
2509   $value;
2510 }
2511
2512 #utility methods for print_*
2513
2514 sub _translate_old_latex_format {
2515   warn "_translate_old_latex_format called\n"
2516     if $DEBUG; 
2517
2518   my @template = ();
2519   while ( @_ ) {
2520     my $line = shift;
2521   
2522     if ( $line =~ /^%%Detail\s*$/ ) {
2523   
2524       push @template, q![@--!,
2525                       q!  foreach my $_tr_line (@detail_items) {!,
2526                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2527                       q!      $_tr_line->{'description'} .= !, 
2528                       q!        "\\tabularnewline\n~~".!,
2529                       q!        join( "\\tabularnewline\n~~",!,
2530                       q!          @{$_tr_line->{'ext_description'}}!,
2531                       q!        );!,
2532                       q!    }!;
2533
2534       while ( ( my $line_item_line = shift )
2535               !~ /^%%EndDetail\s*$/                            ) {
2536         $line_item_line =~ s/'/\\'/g;    # nice LTS
2537         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2538         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2539         push @template, "    \$OUT .= '$line_item_line';";
2540       }
2541   
2542       push @template, '}',
2543                       '--@]';
2544
2545     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2546
2547       push @template, '[@--',
2548                       '  foreach my $_tr_line (@total_items) {';
2549
2550       while ( ( my $total_item_line = shift )
2551               !~ /^%%EndTotalDetails\s*$/                      ) {
2552         $total_item_line =~ s/'/\\'/g;    # nice LTS
2553         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2554         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2555         push @template, "    \$OUT .= '$total_item_line';";
2556       }
2557
2558       push @template, '}',
2559                       '--@]';
2560
2561     } else {
2562       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2563       push @template, $line;  
2564     }
2565   
2566   }
2567
2568   if ($DEBUG) {
2569     warn "$_\n" foreach @template;
2570   }
2571
2572   (@template);
2573 }
2574
2575 sub terms {
2576   my $self = shift;
2577
2578   #check for an invoice- specific override (eventually)
2579   
2580   #check for a customer- specific override
2581   return $self->cust_main->invoice_terms
2582     if $self->cust_main->invoice_terms;
2583
2584   #use configured default or default default
2585   $conf->config('invoice_default_terms') || 'Payable upon receipt';
2586 }
2587
2588 sub due_date {
2589   my $self = shift;
2590   my $duedate = '';
2591   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2592     $duedate = $self->_date() + ( $1 * 86400 );
2593   }
2594   $duedate;
2595 }
2596
2597 sub due_date2str {
2598   my $self = shift;
2599   $self->due_date ? time2str(shift, $self->due_date) : '';
2600 }
2601
2602 sub balance_due_msg {
2603   my $self = shift;
2604   my $msg = 'Balance Due';
2605   return $msg unless $self->terms;
2606   if ( $self->due_date ) {
2607     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2608   } elsif ( $self->terms ) {
2609     $msg .= ' - '. $self->terms;
2610   }
2611   $msg;
2612 }
2613
2614 sub balance_due_date {
2615   my $self = shift;
2616   my $duedate = '';
2617   if (    $conf->exists('invoice_default_terms') 
2618        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2619     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2620   }
2621   $duedate;
2622 }
2623
2624 =item invnum_date_pretty
2625
2626 Returns a string with the invoice number and date, for example:
2627 "Invoice #54 (3/20/2008)"
2628
2629 =cut
2630
2631 sub invnum_date_pretty {
2632   my $self = shift;
2633   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2634 }
2635
2636 =item _date_pretty
2637
2638 Returns a string with the date, for example: "3/20/2008"
2639
2640 =cut
2641
2642 sub _date_pretty {
2643   my $self = shift;
2644   time2str('%x', $self->_date);
2645 }
2646
2647 sub _items_sections {
2648   my $self = shift;
2649   my $late = shift;
2650
2651   my %s = ();
2652   my %l = ();
2653
2654   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2655   {
2656
2657     if ( $cust_bill_pkg->pkgnum > 0 ) {
2658       my $usage = $cust_bill_pkg->usage;
2659
2660       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2661         my $desc = $display->section;
2662         my $type = $display->type;
2663
2664         if ( $display->post_total ) {
2665           if (! $type || $type eq 'S') {
2666             $l{$desc} += $cust_bill_pkg->setup
2667               if ( $cust_bill_pkg->setup != 0 );
2668           }
2669
2670           if (! $type) {
2671             $l{$desc} += $cust_bill_pkg->recur
2672               if ( $cust_bill_pkg->recur != 0 );
2673           }
2674
2675           if ($type && $type eq 'R') {
2676             $l{$desc} += $cust_bill_pkg->recur - $usage
2677               if ( $cust_bill_pkg->recur != 0 );
2678           }
2679           
2680           if ($type && $type eq 'U') {
2681             $l{$desc} += $usage;
2682           }
2683
2684         } else {
2685           if (! $type || $type eq 'S') {
2686             $s{$desc} += $cust_bill_pkg->setup
2687               if ( $cust_bill_pkg->setup != 0 );
2688           }
2689
2690           if (! $type) {
2691             $s{$desc} += $cust_bill_pkg->recur
2692               if ( $cust_bill_pkg->recur != 0 );
2693           }
2694
2695           if ($type && $type eq 'R') {
2696             $s{$desc} += $cust_bill_pkg->recur - $usage
2697               if ( $cust_bill_pkg->recur != 0 );
2698           }
2699           
2700           if ($type && $type eq 'U') {
2701             $s{$desc} += $usage;
2702           }
2703
2704         }
2705
2706       }
2707
2708     }
2709
2710   }
2711
2712   push @$late, map { { 'description' => $_,
2713                        'subtotal'    => $l{$_},
2714                        'post_total'  => 1,
2715                    } } sort keys %l;
2716
2717   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2718
2719 }
2720
2721 sub _items {
2722   my $self = shift;
2723
2724   #my @display = scalar(@_)
2725   #              ? @_
2726   #              : qw( _items_previous _items_pkg );
2727   #              #: qw( _items_pkg );
2728   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2729   my @display = qw( _items_previous _items_pkg );
2730
2731   my @b = ();
2732   foreach my $display ( @display ) {
2733     push @b, $self->$display(@_);
2734   }
2735   @b;
2736 }
2737
2738 sub _items_previous {
2739   my $self = shift;
2740   my $cust_main = $self->cust_main;
2741   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2742   my @b = ();
2743   foreach ( @pr_cust_bill ) {
2744     push @b, {
2745       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2746                        ' ('. time2str('%x',$_->_date). ')',
2747       #'pkgpart'     => 'N/A',
2748       'pkgnum'      => 'N/A',
2749       'amount'      => sprintf("%.2f", $_->owed),
2750     };
2751   }
2752   @b;
2753
2754   #{
2755   #    'description'     => 'Previous Balance',
2756   #    #'pkgpart'         => 'N/A',
2757   #    'pkgnum'          => 'N/A',
2758   #    'amount'          => sprintf("%10.2f", $pr_total ),
2759   #    'ext_description' => [ map {
2760   #                                 "Invoice ". $_->invnum.
2761   #                                 " (". time2str("%x",$_->_date). ") ".
2762   #                                 sprintf("%10.2f", $_->owed)
2763   #                         } @pr_cust_bill ],
2764
2765   #};
2766 }
2767
2768 sub _items_pkg {
2769   my $self = shift;
2770   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2771   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2772 }
2773
2774 sub _taxsort {
2775   return 0 unless $a cmp $b;
2776   return -1 if $b eq 'Tax';
2777   return 1 if $a eq 'Tax';
2778   return -1 if $b eq 'Other surcharges';
2779   return 1 if $a eq 'Other surcharges';
2780   $a cmp $b;
2781 }
2782
2783 sub _items_tax {
2784   my $self = shift;
2785   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2786   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2787 }
2788
2789 sub _items_cust_bill_pkg {
2790   my $self = shift;
2791   my $cust_bill_pkg = shift;
2792   my %opt = @_;
2793
2794   my $format = $opt{format} || '';
2795   my $escape_function = $opt{escape_function} || sub { shift };
2796   my $format_function = $opt{format_function} || '';
2797   my $unsquelched = $opt{unsquelched} || '';
2798   my $section = $opt{section}->{description} if $opt{section};
2799
2800   my @b = ();
2801   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2802   {
2803     foreach my $display ( grep { defined($section)
2804                                  ? $_->section eq $section
2805                                  : 1
2806                                }
2807                           $cust_bill_pkg->cust_bill_pkg_display
2808                         )
2809     {
2810
2811       my $type = $display->type;
2812
2813       my $cust_pkg = $cust_bill_pkg->cust_pkg;
2814
2815       my $desc = $cust_bill_pkg->desc;
2816       $desc = substr($desc, 0, 50). '...'
2817         if $format eq 'latex' && length($desc) > 50;
2818
2819       my %details_opt = ( 'format'          => $format,
2820                           'escape_function' => $escape_function,
2821                           'format_function' => $format_function,
2822                         );
2823
2824       if ( $cust_bill_pkg->pkgnum > 0 ) {
2825
2826         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2827
2828           my $description = $desc;
2829           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2830
2831           my @d = map &{$escape_function}($_),
2832                          $cust_pkg->h_labels_short($self->_date);
2833           push @d, $cust_bill_pkg->details(%details_opt)
2834             if $cust_bill_pkg->recur == 0;
2835
2836           push @b, {
2837             description     => $description,
2838             #pkgpart         => $part_pkg->pkgpart,
2839             pkgnum          => $cust_bill_pkg->pkgnum,
2840             amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2841             unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2842             quantity        => $cust_bill_pkg->quantity,
2843             ext_description => \@d,
2844           };
2845
2846         }
2847
2848         if ( $cust_bill_pkg->recur != 0 &&
2849              ( !$type || $type eq 'R' || $type eq 'U' )
2850            )
2851         {
2852
2853           my $is_summary = $display->summary;
2854           my $description = $is_summary ? "Usage charges" : $desc;
2855
2856           unless ( $conf->exists('disable_line_item_date_ranges') ) {
2857             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2858                             " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2859           }
2860
2861           #at least until cust_bill_pkg has "past" ranges in addition to
2862           #the "future" sdate/edate ones... see #3032
2863           my @d = ();
2864           push @d, map &{$escape_function}($_),
2865                          $cust_pkg->h_labels_short($self->_date)
2866                                                 #$cust_bill_pkg->edate,
2867                                                 #$cust_bill_pkg->sdate),
2868             ;
2869   
2870           @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2871           push @d, $cust_bill_pkg->details(%details_opt)
2872             unless ($is_summary || $type && $type eq 'R');
2873   
2874           my $amount = 0;
2875           if (!$type) {
2876             $amount = $cust_bill_pkg->recur;
2877           }elsif($type eq 'R') {
2878             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2879           }elsif($type eq 'U') {
2880             $amount = $cust_bill_pkg->usage;
2881           }
2882   
2883           push @b, {
2884             description     => $description,
2885             #pkgpart         => $part_pkg->pkgpart,
2886             pkgnum          => $cust_bill_pkg->pkgnum,
2887             amount          => sprintf("%.2f", $amount),
2888             unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2889             quantity        => $cust_bill_pkg->quantity,
2890             ext_description => \@d,
2891           } unless ( $type eq 'U' && ! $amount );
2892
2893         }
2894
2895       } else { #pkgnum tax or one-shot line item (??)
2896
2897         if ( $cust_bill_pkg->setup != 0 ) {
2898           push @b, {
2899             'description' => $desc,
2900             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2901           };
2902         }
2903         if ( $cust_bill_pkg->recur != 0 ) {
2904           push @b, {
2905             'description' => "$desc (".
2906                              time2str("%x", $cust_bill_pkg->sdate). ' - '.
2907                              time2str("%x", $cust_bill_pkg->edate). ')',
2908             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2909           };
2910         }
2911
2912       }
2913
2914     }
2915
2916   }
2917
2918   @b;
2919
2920 }
2921
2922 sub _items_credits {
2923   my $self = shift;
2924
2925   my @b;
2926   #credits
2927   foreach ( $self->cust_credited ) {
2928
2929     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2930
2931     my $reason = $_->cust_credit->reason;
2932     #my $reason = substr($_->cust_credit->reason,0,32);
2933     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2934     $reason = " ($reason) " if $reason;
2935     push @b, {
2936       #'description' => 'Credit ref\#'. $_->crednum.
2937       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2938       #                 $reason,
2939       'description' => 'Credit applied '.
2940                        time2str("%x",$_->cust_credit->_date). $reason,
2941       'amount'      => sprintf("%.2f",$_->amount),
2942     };
2943   }
2944   #foreach ( @cr_cust_credit ) {
2945   #  push @buf,[
2946   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2947   #    $money_char. sprintf("%10.2f",$_->credited)
2948   #  ];
2949   #}
2950
2951   @b;
2952
2953 }
2954
2955 sub _items_payments {
2956   my $self = shift;
2957
2958   my @b;
2959   #get & print payments
2960   foreach ( $self->cust_bill_pay ) {
2961
2962     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2963
2964     push @b, {
2965       'description' => "Payment received ".
2966                        time2str("%x",$_->cust_pay->_date ),
2967       'amount'      => sprintf("%.2f", $_->amount )
2968     };
2969   }
2970
2971   @b;
2972
2973 }
2974
2975
2976 =back
2977
2978 =head1 SUBROUTINES
2979
2980 =over 4
2981
2982 =item process_reprint
2983
2984 =cut
2985
2986 sub process_reprint {
2987   process_re_X('print', @_);
2988 }
2989
2990 =item process_reemail
2991
2992 =cut
2993
2994 sub process_reemail {
2995   process_re_X('email', @_);
2996 }
2997
2998 =item process_refax
2999
3000 =cut
3001
3002 sub process_refax {
3003   process_re_X('fax', @_);
3004 }
3005
3006 =item process_reftp
3007
3008 =cut
3009
3010 sub process_reftp {
3011   process_re_X('ftp', @_);
3012 }
3013
3014 =item respool
3015
3016 =cut
3017
3018 sub process_respool {
3019   process_re_X('spool', @_);
3020 }
3021
3022 use Storable qw(thaw);
3023 use Data::Dumper;
3024 use MIME::Base64;
3025 sub process_re_X {
3026   my( $method, $job ) = ( shift, shift );
3027   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3028
3029   my $param = thaw(decode_base64(shift));
3030   warn Dumper($param) if $DEBUG;
3031
3032   re_X(
3033     $method,
3034     $job,
3035     %$param,
3036   );
3037
3038 }
3039
3040 sub re_X {
3041   my($method, $job, %param ) = @_;
3042   if ( $DEBUG ) {
3043     warn "re_X $method for job $job with param:\n".
3044          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3045   }
3046
3047   #some false laziness w/search/cust_bill.html
3048   my $distinct = '';
3049   my $orderby = 'ORDER BY cust_bill._date';
3050
3051   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3052
3053   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3054      
3055   my @cust_bill = qsearch( {
3056     #'select'    => "cust_bill.*",
3057     'table'     => 'cust_bill',
3058     'addl_from' => $addl_from,
3059     'hashref'   => {},
3060     'extra_sql' => $extra_sql,
3061     'order_by'  => $orderby,
3062     'debug' => 1,
3063   } );
3064
3065   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3066
3067   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3068     if $DEBUG;
3069
3070   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3071   foreach my $cust_bill ( @cust_bill ) {
3072     $cust_bill->$method();
3073
3074     if ( $job ) { #progressbar foo
3075       $num++;
3076       if ( time - $min_sec > $last ) {
3077         my $error = $job->update_statustext(
3078           int( 100 * $num / scalar(@cust_bill) )
3079         );
3080         die $error if $error;
3081         $last = time;
3082       }
3083     }
3084
3085   }
3086
3087 }
3088
3089 =back
3090
3091 =head1 CLASS METHODS
3092
3093 =over 4
3094
3095 =item owed_sql
3096
3097 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3098
3099 =cut
3100
3101 sub owed_sql {
3102   my $class = shift;
3103   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3104 }
3105
3106 =item net_sql
3107
3108 Returns an SQL fragment to retreive the net amount (charged minus credited).
3109
3110 =cut
3111
3112 sub net_sql {
3113   my $class = shift;
3114   'charged - '. $class->credited_sql;
3115 }
3116
3117 =item paid_sql
3118
3119 Returns an SQL fragment to retreive the amount paid against this invoice.
3120
3121 =cut
3122
3123 sub paid_sql {
3124   #my $class = shift;
3125   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3126        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
3127 }
3128
3129 =item credited_sql
3130
3131 Returns an SQL fragment to retreive the amount credited against this invoice.
3132
3133 =cut
3134
3135 sub credited_sql {
3136   #my $class = shift;
3137   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3138        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
3139 }
3140
3141 =item search_sql HASHREF
3142
3143 Class method which returns an SQL WHERE fragment to search for parameters
3144 specified in HASHREF.  Valid parameters are
3145
3146 =over 4
3147
3148 =item begin
3149
3150 Epoch date (UNIX timestamp) setting a lower bound for _date values
3151
3152 =item end
3153
3154 Epoch date (UNIX timestamp) setting an upper bound for _date values
3155
3156 =item invnum_min
3157
3158 =item invnum_max
3159
3160 =item agentnum
3161
3162 =item owed
3163
3164 =item net
3165
3166 =item days
3167
3168 =item newest_percust
3169
3170 =back
3171
3172 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3173
3174 =cut
3175
3176 sub search_sql {
3177   my($class, $param) = @_;
3178   if ( $DEBUG ) {
3179     warn "$me search_sql called with params: \n".
3180          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3181   }
3182
3183   my @search = ();
3184
3185   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3186     push @search, "cust_bill._date >= $1";
3187   }
3188   if ( $param->{'end'} =~ /^(\d+)$/ ) {
3189     push @search, "cust_bill._date < $1";
3190   }
3191   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3192     push @search, "cust_bill.invnum >= $1";
3193   }
3194   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3195     push @search, "cust_bill.invnum <= $1";
3196   }
3197   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3198     push @search, "cust_main.agentnum = $1";
3199   }
3200
3201   push @search, '0 != '. FS::cust_bill->owed_sql
3202     if $param->{'open'};
3203
3204   push @search, '0 != '. FS::cust_bill->net_sql
3205     if $param->{'net'};
3206
3207   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3208     if $param->{'days'};
3209
3210   if ( $param->{'newest_percust'} ) {
3211
3212     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3213     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3214
3215     my @newest_where = map { my $x = $_;
3216                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3217                              $x;
3218                            }
3219                            grep ! /^cust_main./, @search;
3220     my $newest_where = scalar(@newest_where)
3221                          ? ' AND '. join(' AND ', @newest_where)
3222                          : '';
3223
3224
3225     push @search, "cust_bill._date = (
3226       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3227         WHERE newest_cust_bill.custnum = cust_bill.custnum
3228           $newest_where
3229     )";
3230
3231   }
3232
3233   my $curuser = $FS::CurrentUser::CurrentUser;
3234   if ( $curuser->username eq 'fs_queue'
3235        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3236     my $username = $1;
3237     my $newuser = qsearchs('access_user', {
3238       'username' => $username,
3239       'disabled' => '',
3240     } );
3241     if ( $newuser ) {
3242       $curuser = $newuser;
3243     } else {
3244       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3245     }
3246   }
3247
3248   push @search, $curuser->agentnums_sql;
3249
3250   join(' AND ', @search );
3251
3252 }
3253
3254 =back
3255
3256 =head1 BUGS
3257
3258 The delete method.
3259
3260 =head1 SEE ALSO
3261
3262 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3263 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3264 documentation.
3265
3266 =cut
3267
3268 1;
3269