rest of per-agent config for company_name, company_address, logo, etc.. RT#3989
[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   foreach my $line_item ( $conf->exists('disable_previous_balance') 
2084                             ? ()
2085                             : $self->_items_previous
2086                         )
2087   {
2088     my $detail = {
2089       ext_description => [],
2090     };
2091     $detail->{'ref'} = $line_item->{'pkgnum'};
2092     $detail->{'quantity'} = 1;
2093     $detail->{'section'} = $previous_section;
2094     $detail->{'description'} = &$escape_function($line_item->{'description'});
2095     if ( exists $line_item->{'ext_description'} ) {
2096       @{$detail->{'ext_description'}} = map {
2097         &$escape_function($_);
2098       } @{$line_item->{'ext_description'}};
2099     }
2100     $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2101                           $line_item->{'amount'};
2102     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2103   
2104     push @detail_items, $detail;
2105     push @buf, [ $detail->{'description'},
2106                  $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2107                ];
2108   }
2109   
2110   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2111     push @buf, ['','-----------'];
2112     push @buf, [ 'Total Previous Balance',
2113                  $money_char. sprintf("%10.2f", $pr_total) ];
2114     push @buf, ['',''];
2115   }
2116
2117   foreach my $section (@sections, @$late_sections) {
2118
2119     $section->{'subtotal'} = $other_money_char.
2120                              sprintf('%.2f', $section->{'subtotal'})
2121       if $multisection;
2122
2123     if ( $section->{'description'} ) {
2124       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2125                    [ '', '' ],
2126                  );
2127     }
2128
2129     my %options = ();
2130     $options{'section'} = $section if $multisection;
2131     $options{'format'} = $format;
2132     $options{'escape_function'} = $escape_function;
2133     $options{'format_function'} = sub { () } unless $unsquelched;
2134     $options{'unsquelched'} = $unsquelched;
2135
2136     foreach my $line_item ( $self->_items_pkg(%options) ) {
2137       my $detail = {
2138         ext_description => [],
2139       };
2140       $detail->{'ref'} = $line_item->{'pkgnum'};
2141       $detail->{'quantity'} = $line_item->{'quantity'};
2142       $detail->{'section'} = $section;
2143       $detail->{'description'} = &$escape_function($line_item->{'description'});
2144       if ( exists $line_item->{'ext_description'} ) {
2145         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2146       }
2147       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2148                               $line_item->{'amount'};
2149       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2150                                  $line_item->{'unit_amount'};
2151       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2152   
2153       push @detail_items, $detail;
2154       push @buf, ( [ $detail->{'description'},
2155                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2156                    ],
2157                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2158                  );
2159     }
2160
2161     if ( $section->{'description'} ) {
2162       push @buf, ( ['','-----------'],
2163                    [ $section->{'description'}. ' sub-total',
2164                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2165                    ],
2166                    [ '', '' ],
2167                    [ '', '' ],
2168                  );
2169     }
2170   
2171   }
2172   
2173   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2174     unshift @sections, $previous_section if $pr_total;
2175   }
2176
2177   foreach my $tax ( $self->_items_tax ) {
2178
2179     $taxtotal += $tax->{'amount'};
2180
2181     my $description = &$escape_function( $tax->{'description'} );
2182     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2183
2184     if ( $multisection ) {
2185
2186       my $money = $old_latex ? '' : $money_char;
2187       push @detail_items, {
2188         ext_description => [],
2189         ref          => '',
2190         quantity     => '',
2191         description  => $description,
2192         amount       => $money. $amount,
2193         product_code => '',
2194         section      => $tax_section,
2195       };
2196
2197     } else {
2198
2199       push @total_items, {
2200         'total_item'   => $description,
2201         'total_amount' => $other_money_char. $amount,
2202       };
2203
2204     }
2205
2206     push @buf,[ $description,
2207                 $money_char. $amount,
2208               ];
2209
2210   }
2211   
2212   if ( $taxtotal ) {
2213     my $total = {};
2214     $total->{'total_item'} = 'Sub-total';
2215     $total->{'total_amount'} =
2216       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2217
2218     if ( $multisection ) {
2219       $tax_section->{'subtotal'} = $other_money_char.
2220                                    sprintf('%.2f', $taxtotal);
2221       $tax_section->{'pretotal'} = 'New charges sub-total '.
2222                                    $total->{'total_amount'};
2223       push @sections, $tax_section if $taxtotal;
2224     }else{
2225       unshift @total_items, $total;
2226     }
2227   }
2228   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2229   
2230   push @buf,['','-----------'];
2231   push @buf,[( $conf->exists('disable_previous_balance') 
2232                ? 'Total Charges'
2233                : 'Total New Charges'
2234              ),
2235              $money_char. sprintf("%10.2f",$self->charged) ];
2236   push @buf,['',''];
2237
2238   {
2239     my $total = {};
2240     $total->{'total_item'} = &$embolden_function('Total');
2241     $total->{'total_amount'} =
2242       &$embolden_function(
2243         $other_money_char.
2244         sprintf( '%.2f',
2245                  $self->charged + ( $conf->exists('disable_previous_balance')
2246                                     ? 0
2247                                     : $pr_total
2248                                   )
2249                )
2250       );
2251     if ( $multisection ) {
2252       $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2253                                       sprintf('%.2f', $self->charged );
2254     }else{
2255       push @total_items, $total;
2256     }
2257     push @buf,['','-----------'];
2258     push @buf,['Total Charges',
2259                $money_char.
2260                sprintf( '%10.2f', $self->charged +
2261                                     ( $conf->exists('disable_previous_balance')
2262                                         ? 0
2263                                         : $pr_total
2264                                     )
2265                       )
2266               ];
2267     push @buf,['',''];
2268   }
2269   
2270   unless ( $conf->exists('disable_previous_balance') ) {
2271     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2272   
2273     # credits
2274     my $credittotal = 0;
2275     foreach my $credit ( $self->_items_credits ) {
2276       my $total;
2277       $total->{'total_item'} = &$escape_function($credit->{'description'});
2278       $credittotal += $credit->{'amount'};
2279       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2280       $adjusttotal += $credit->{'amount'};
2281       if ( $multisection ) {
2282         my $money = $old_latex ? '' : $money_char;
2283         push @detail_items, {
2284           ext_description => [],
2285           ref          => '',
2286           quantity     => '',
2287           description  => &$escape_function($credit->{'description'}),
2288           amount       => $money. $credit->{'amount'},
2289           product_code => '',
2290           section      => $adjust_section,
2291         };
2292       }else{
2293         push @total_items, $total;
2294       }
2295     }
2296     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2297   
2298     # credits (again)
2299     foreach ( $self->cust_credited ) {
2300   
2301       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2302
2303       my $reason = substr($_->cust_credit->reason,0,32);
2304       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2305       $reason = " ($reason) " if $reason;
2306       push @buf,[
2307         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
2308         $money_char. sprintf("%10.2f",$_->amount)
2309       ];
2310     }
2311
2312     # payments
2313     my $paymenttotal = 0;
2314     foreach my $payment ( $self->_items_payments ) {
2315       my $total = {};
2316       $total->{'total_item'} = &$escape_function($payment->{'description'});
2317       $paymenttotal += $payment->{'amount'};
2318       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2319       $adjusttotal += $payment->{'amount'};
2320       if ( $multisection ) {
2321         my $money = $old_latex ? '' : $money_char;
2322         push @detail_items, {
2323           ext_description => [],
2324           ref          => '',
2325           quantity     => '',
2326           description  => &$escape_function($payment->{'description'}),
2327           amount       => $money. $payment->{'amount'},
2328           product_code => '',
2329           section      => $adjust_section,
2330         };
2331       }else{
2332         push @total_items, $total;
2333       }
2334       push @buf, [ $payment->{'description'},
2335                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2336                  ];
2337     }
2338     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2339   
2340     if ( $multisection ) {
2341       $adjust_section->{'subtotal'} = $other_money_char.
2342                                       sprintf('%.2f', $adjusttotal);
2343       push @sections, $adjust_section;
2344     }
2345
2346     { 
2347       my $total;
2348       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2349       $total->{'total_amount'} =
2350         &$embolden_function(
2351           $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2352         );
2353       if ( $multisection ) {
2354         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2355                                          $total->{'total_amount'};
2356       }else{
2357         push @total_items, $total;
2358       }
2359       push @buf,['','-----------'];
2360       push @buf,[$self->balance_due_msg, $money_char. 
2361         sprintf("%10.2f", $balance_due ) ];
2362     }
2363   }
2364
2365   if ( $multisection ) {
2366     push @sections, @$late_sections
2367       if $unsquelched;
2368   }
2369
2370   $invoice_lines = 0;
2371   my $wasfunc = 0;
2372   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2373     /invoice_lines\((\d*)\)/;
2374     $invoice_lines += $1 || scalar(@buf);
2375     $wasfunc=1;
2376   }
2377   die "no invoice_lines() functions in template?"
2378     if ( $format eq 'template' && !$wasfunc );
2379
2380   if ($format eq 'template') {
2381
2382     if ( $invoice_lines ) {
2383       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2384       $invoice_data{'total_pages'}++
2385         if scalar(@buf) % $invoice_lines;
2386     }
2387
2388     #setup subroutine for the template
2389     sub FS::cust_bill::_template::invoice_lines {
2390       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2391       map { 
2392         scalar(@FS::cust_bill::_template::buf)
2393           ? shift @FS::cust_bill::_template::buf
2394           : [ '', '' ];
2395       }
2396       ( 1 .. $lines );
2397     }
2398
2399     my $lines;
2400     my @collect;
2401     while (@buf) {
2402       push @collect, split("\n",
2403         $text_template->fill_in( HASH => \%invoice_data,
2404                                  PACKAGE => 'FS::cust_bill::_template'
2405                                )
2406       );
2407       $FS::cust_bill::_template::page++;
2408     }
2409     map "$_\n", @collect;
2410   }else{
2411     warn "filling in template for invoice ". $self->invnum. "\n"
2412       if $DEBUG;
2413     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2414       if $DEBUG > 1;
2415
2416     $text_template->fill_in(HASH => \%invoice_data);
2417   }
2418 }
2419
2420 =item print_ps [ TIME [ , TEMPLATE ] ]
2421
2422 Returns an postscript invoice, as a scalar.
2423
2424 TIME an optional value used to control the printing of overdue messages.  The
2425 default is now.  It isn't the date of the invoice; that's the `_date' field.
2426 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2427 L<Time::Local> and L<Date::Parse> for conversion functions.
2428
2429 =cut
2430
2431 sub print_ps {
2432   my $self = shift;
2433
2434   my ($file, $lfile) = $self->print_latex(@_);
2435   my $ps = generate_ps($file);
2436   unlink($lfile);
2437
2438   $ps;
2439 }
2440
2441 =item print_pdf [ TIME [ , TEMPLATE ] ]
2442
2443 Returns an PDF invoice, as a scalar.
2444
2445 TIME an optional value used to control the printing of overdue messages.  The
2446 default is now.  It isn't the date of the invoice; that's the `_date' field.
2447 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2448 L<Time::Local> and L<Date::Parse> for conversion functions.
2449
2450 =cut
2451
2452 sub print_pdf {
2453   my $self = shift;
2454
2455   my ($file, $lfile) = $self->print_latex(@_);
2456   my $pdf = generate_pdf($file);
2457   unlink($lfile);
2458
2459   $pdf;
2460 }
2461
2462 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2463
2464 Returns an HTML invoice, as a scalar.
2465
2466 TIME an optional value used to control the printing of overdue messages.  The
2467 default is now.  It isn't the date of the invoice; that's the `_date' field.
2468 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2469 L<Time::Local> and L<Date::Parse> for conversion functions.
2470
2471 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2472 when emailing the invoice as part of a multipart/related MIME email.
2473
2474 =cut
2475
2476 sub print_html {
2477   my $self = shift;
2478   my %params;
2479   if ( ref $_[0]  ) {
2480     %params = %{ shift() }; 
2481   }else{
2482     $params{'time'} = shift;
2483     $params{'template'} = shift;
2484     $params{'cid'} = shift;
2485   }
2486
2487   $params{'format'} = 'html';
2488
2489   $self->print_generic( %params );
2490 }
2491
2492 # quick subroutine for print_latex
2493 #
2494 # There are ten characters that LaTeX treats as special characters, which
2495 # means that they do not simply typeset themselves: 
2496 #      # $ % & ~ _ ^ \ { }
2497 #
2498 # TeX ignores blanks following an escaped character; if you want a blank (as
2499 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2500
2501 sub _latex_escape {
2502   my $value = shift;
2503   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2504   $value =~ s/([<>])/\$$1\$/g;
2505   $value;
2506 }
2507
2508 #utility methods for print_*
2509
2510 sub _translate_old_latex_format {
2511   warn "_translate_old_latex_format called\n"
2512     if $DEBUG; 
2513
2514   my @template = ();
2515   while ( @_ ) {
2516     my $line = shift;
2517   
2518     if ( $line =~ /^%%Detail\s*$/ ) {
2519   
2520       push @template, q![@--!,
2521                       q!  foreach my $_tr_line (@detail_items) {!,
2522                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2523                       q!      $_tr_line->{'description'} .= !, 
2524                       q!        "\\tabularnewline\n~~".!,
2525                       q!        join( "\\tabularnewline\n~~",!,
2526                       q!          @{$_tr_line->{'ext_description'}}!,
2527                       q!        );!,
2528                       q!    }!;
2529
2530       while ( ( my $line_item_line = shift )
2531               !~ /^%%EndDetail\s*$/                            ) {
2532         $line_item_line =~ s/'/\\'/g;    # nice LTS
2533         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2534         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2535         push @template, "    \$OUT .= '$line_item_line';";
2536       }
2537   
2538       push @template, '}',
2539                       '--@]';
2540
2541     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2542
2543       push @template, '[@--',
2544                       '  foreach my $_tr_line (@total_items) {';
2545
2546       while ( ( my $total_item_line = shift )
2547               !~ /^%%EndTotalDetails\s*$/                      ) {
2548         $total_item_line =~ s/'/\\'/g;    # nice LTS
2549         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2550         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2551         push @template, "    \$OUT .= '$total_item_line';";
2552       }
2553
2554       push @template, '}',
2555                       '--@]';
2556
2557     } else {
2558       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2559       push @template, $line;  
2560     }
2561   
2562   }
2563
2564   if ($DEBUG) {
2565     warn "$_\n" foreach @template;
2566   }
2567
2568   (@template);
2569 }
2570
2571 sub terms {
2572   my $self = shift;
2573
2574   #check for an invoice- specific override (eventually)
2575   
2576   #check for a customer- specific override
2577   return $self->cust_main->invoice_terms
2578     if $self->cust_main->invoice_terms;
2579
2580   #use configured default or default default
2581   $conf->config('invoice_default_terms') || 'Payable upon receipt';
2582 }
2583
2584 sub due_date {
2585   my $self = shift;
2586   my $duedate = '';
2587   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2588     $duedate = $self->_date() + ( $1 * 86400 );
2589   }
2590   $duedate;
2591 }
2592
2593 sub due_date2str {
2594   my $self = shift;
2595   $self->due_date ? time2str(shift, $self->due_date) : '';
2596 }
2597
2598 sub balance_due_msg {
2599   my $self = shift;
2600   my $msg = 'Balance Due';
2601   return $msg unless $self->terms;
2602   if ( $self->due_date ) {
2603     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2604   } elsif ( $self->terms ) {
2605     $msg .= ' - '. $self->terms;
2606   }
2607   $msg;
2608 }
2609
2610 sub balance_due_date {
2611   my $self = shift;
2612   my $duedate = '';
2613   if (    $conf->exists('invoice_default_terms') 
2614        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2615     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2616   }
2617   $duedate;
2618 }
2619
2620 =item invnum_date_pretty
2621
2622 Returns a string with the invoice number and date, for example:
2623 "Invoice #54 (3/20/2008)"
2624
2625 =cut
2626
2627 sub invnum_date_pretty {
2628   my $self = shift;
2629   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2630 }
2631
2632 =item _date_pretty
2633
2634 Returns a string with the date, for example: "3/20/2008"
2635
2636 =cut
2637
2638 sub _date_pretty {
2639   my $self = shift;
2640   time2str('%x', $self->_date);
2641 }
2642
2643 sub _items_sections {
2644   my $self = shift;
2645   my $late = shift;
2646
2647   my %s = ();
2648   my %l = ();
2649
2650   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2651   {
2652
2653     if ( $cust_bill_pkg->pkgnum > 0 ) {
2654       my $usage = $cust_bill_pkg->usage;
2655
2656       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2657         my $desc = $display->section;
2658         my $type = $display->type;
2659
2660         if ( $display->post_total ) {
2661           if (! $type || $type eq 'S') {
2662             $l{$desc} += $cust_bill_pkg->setup
2663               if ( $cust_bill_pkg->setup != 0 );
2664           }
2665
2666           if (! $type) {
2667             $l{$desc} += $cust_bill_pkg->recur
2668               if ( $cust_bill_pkg->recur != 0 );
2669           }
2670
2671           if ($type && $type eq 'R') {
2672             $l{$desc} += $cust_bill_pkg->recur - $usage
2673               if ( $cust_bill_pkg->recur != 0 );
2674           }
2675           
2676           if ($type && $type eq 'U') {
2677             $l{$desc} += $usage;
2678           }
2679
2680         } else {
2681           if (! $type || $type eq 'S') {
2682             $s{$desc} += $cust_bill_pkg->setup
2683               if ( $cust_bill_pkg->setup != 0 );
2684           }
2685
2686           if (! $type) {
2687             $s{$desc} += $cust_bill_pkg->recur
2688               if ( $cust_bill_pkg->recur != 0 );
2689           }
2690
2691           if ($type && $type eq 'R') {
2692             $s{$desc} += $cust_bill_pkg->recur - $usage
2693               if ( $cust_bill_pkg->recur != 0 );
2694           }
2695           
2696           if ($type && $type eq 'U') {
2697             $s{$desc} += $usage;
2698           }
2699
2700         }
2701
2702       }
2703
2704     }
2705
2706   }
2707
2708   push @$late, map { { 'description' => $_,
2709                        'subtotal'    => $l{$_},
2710                        'post_total'  => 1,
2711                    } } sort keys %l;
2712
2713   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2714
2715 }
2716
2717 sub _items {
2718   my $self = shift;
2719
2720   #my @display = scalar(@_)
2721   #              ? @_
2722   #              : qw( _items_previous _items_pkg );
2723   #              #: qw( _items_pkg );
2724   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2725   my @display = qw( _items_previous _items_pkg );
2726
2727   my @b = ();
2728   foreach my $display ( @display ) {
2729     push @b, $self->$display(@_);
2730   }
2731   @b;
2732 }
2733
2734 sub _items_previous {
2735   my $self = shift;
2736   my $cust_main = $self->cust_main;
2737   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2738   my @b = ();
2739   foreach ( @pr_cust_bill ) {
2740     push @b, {
2741       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2742                        ' ('. time2str('%x',$_->_date). ')',
2743       #'pkgpart'     => 'N/A',
2744       'pkgnum'      => 'N/A',
2745       'amount'      => sprintf("%.2f", $_->owed),
2746     };
2747   }
2748   @b;
2749
2750   #{
2751   #    'description'     => 'Previous Balance',
2752   #    #'pkgpart'         => 'N/A',
2753   #    'pkgnum'          => 'N/A',
2754   #    'amount'          => sprintf("%10.2f", $pr_total ),
2755   #    'ext_description' => [ map {
2756   #                                 "Invoice ". $_->invnum.
2757   #                                 " (". time2str("%x",$_->_date). ") ".
2758   #                                 sprintf("%10.2f", $_->owed)
2759   #                         } @pr_cust_bill ],
2760
2761   #};
2762 }
2763
2764 sub _items_pkg {
2765   my $self = shift;
2766   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2767   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2768 }
2769
2770 sub _taxsort {
2771   return 0 unless $a cmp $b;
2772   return -1 if $b eq 'Tax';
2773   return 1 if $a eq 'Tax';
2774   return -1 if $b eq 'Other surcharges';
2775   return 1 if $a eq 'Other surcharges';
2776   $a cmp $b;
2777 }
2778
2779 sub _items_tax {
2780   my $self = shift;
2781   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2782   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2783 }
2784
2785 sub _items_cust_bill_pkg {
2786   my $self = shift;
2787   my $cust_bill_pkg = shift;
2788   my %opt = @_;
2789
2790   my $format = $opt{format} || '';
2791   my $escape_function = $opt{escape_function} || sub { shift };
2792   my $format_function = $opt{format_function} || '';
2793   my $unsquelched = $opt{unsquelched} || '';
2794   my $section = $opt{section}->{description} if $opt{section};
2795
2796   my @b = ();
2797   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2798   {
2799     foreach my $display ( grep { defined($section)
2800                                  ? $_->section eq $section
2801                                  : 1
2802                                }
2803                           $cust_bill_pkg->cust_bill_pkg_display
2804                         )
2805     {
2806
2807       my $type = $display->type;
2808
2809       my $cust_pkg = $cust_bill_pkg->cust_pkg;
2810
2811       my $desc = $cust_bill_pkg->desc;
2812       $desc = substr($desc, 0, 50). '...'
2813         if $format eq 'latex' && length($desc) > 50;
2814
2815       my %details_opt = ( 'format'          => $format,
2816                           'escape_function' => $escape_function,
2817                           'format_function' => $format_function,
2818                         );
2819
2820       if ( $cust_bill_pkg->pkgnum > 0 ) {
2821
2822         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2823
2824           my $description = $desc;
2825           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2826
2827           my @d = map &{$escape_function}($_),
2828                          $cust_pkg->h_labels_short($self->_date);
2829           push @d, $cust_bill_pkg->details(%details_opt)
2830             if $cust_bill_pkg->recur == 0;
2831
2832           push @b, {
2833             description     => $description,
2834             #pkgpart         => $part_pkg->pkgpart,
2835             pkgnum          => $cust_bill_pkg->pkgnum,
2836             amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2837             unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2838             quantity        => $cust_bill_pkg->quantity,
2839             ext_description => \@d,
2840           };
2841
2842         }
2843
2844         if ( $cust_bill_pkg->recur != 0 &&
2845              ( !$type || $type eq 'R' || $type eq 'U' )
2846            )
2847         {
2848
2849           my $is_summary = $display->summary;
2850           my $description = $is_summary ? "Usage charges" : $desc;
2851
2852           unless ( $conf->exists('disable_line_item_date_ranges') ) {
2853             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2854                             " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2855           }
2856
2857           #at least until cust_bill_pkg has "past" ranges in addition to
2858           #the "future" sdate/edate ones... see #3032
2859           my @d = ();
2860           push @d, map &{$escape_function}($_),
2861                          $cust_pkg->h_labels_short($self->_date)
2862                                                 #$cust_bill_pkg->edate,
2863                                                 #$cust_bill_pkg->sdate),
2864             ;
2865   
2866           @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2867           push @d, $cust_bill_pkg->details(%details_opt)
2868             unless ($is_summary || $type && $type eq 'R');
2869   
2870           my $amount = 0;
2871           if (!$type) {
2872             $amount = $cust_bill_pkg->recur;
2873           }elsif($type eq 'R') {
2874             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2875           }elsif($type eq 'U') {
2876             $amount = $cust_bill_pkg->usage;
2877           }
2878   
2879           push @b, {
2880             description     => $description,
2881             #pkgpart         => $part_pkg->pkgpart,
2882             pkgnum          => $cust_bill_pkg->pkgnum,
2883             amount          => sprintf("%.2f", $amount),
2884             unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2885             quantity        => $cust_bill_pkg->quantity,
2886             ext_description => \@d,
2887           } unless ( $type eq 'U' && ! $amount );
2888
2889         }
2890
2891       } else { #pkgnum tax or one-shot line item (??)
2892
2893         if ( $cust_bill_pkg->setup != 0 ) {
2894           push @b, {
2895             'description' => $desc,
2896             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2897           };
2898         }
2899         if ( $cust_bill_pkg->recur != 0 ) {
2900           push @b, {
2901             'description' => "$desc (".
2902                              time2str("%x", $cust_bill_pkg->sdate). ' - '.
2903                              time2str("%x", $cust_bill_pkg->edate). ')',
2904             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2905           };
2906         }
2907
2908       }
2909
2910     }
2911
2912   }
2913
2914   @b;
2915
2916 }
2917
2918 sub _items_credits {
2919   my $self = shift;
2920
2921   my @b;
2922   #credits
2923   foreach ( $self->cust_credited ) {
2924
2925     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2926
2927     my $reason = $_->cust_credit->reason;
2928     #my $reason = substr($_->cust_credit->reason,0,32);
2929     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2930     $reason = " ($reason) " if $reason;
2931     push @b, {
2932       #'description' => 'Credit ref\#'. $_->crednum.
2933       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2934       #                 $reason,
2935       'description' => 'Credit applied '.
2936                        time2str("%x",$_->cust_credit->_date). $reason,
2937       'amount'      => sprintf("%.2f",$_->amount),
2938     };
2939   }
2940   #foreach ( @cr_cust_credit ) {
2941   #  push @buf,[
2942   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2943   #    $money_char. sprintf("%10.2f",$_->credited)
2944   #  ];
2945   #}
2946
2947   @b;
2948
2949 }
2950
2951 sub _items_payments {
2952   my $self = shift;
2953
2954   my @b;
2955   #get & print payments
2956   foreach ( $self->cust_bill_pay ) {
2957
2958     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2959
2960     push @b, {
2961       'description' => "Payment received ".
2962                        time2str("%x",$_->cust_pay->_date ),
2963       'amount'      => sprintf("%.2f", $_->amount )
2964     };
2965   }
2966
2967   @b;
2968
2969 }
2970
2971
2972 =back
2973
2974 =head1 SUBROUTINES
2975
2976 =over 4
2977
2978 =item process_reprint
2979
2980 =cut
2981
2982 sub process_reprint {
2983   process_re_X('print', @_);
2984 }
2985
2986 =item process_reemail
2987
2988 =cut
2989
2990 sub process_reemail {
2991   process_re_X('email', @_);
2992 }
2993
2994 =item process_refax
2995
2996 =cut
2997
2998 sub process_refax {
2999   process_re_X('fax', @_);
3000 }
3001
3002 =item process_reftp
3003
3004 =cut
3005
3006 sub process_reftp {
3007   process_re_X('ftp', @_);
3008 }
3009
3010 =item respool
3011
3012 =cut
3013
3014 sub process_respool {
3015   process_re_X('spool', @_);
3016 }
3017
3018 use Storable qw(thaw);
3019 use Data::Dumper;
3020 use MIME::Base64;
3021 sub process_re_X {
3022   my( $method, $job ) = ( shift, shift );
3023   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3024
3025   my $param = thaw(decode_base64(shift));
3026   warn Dumper($param) if $DEBUG;
3027
3028   re_X(
3029     $method,
3030     $job,
3031     %$param,
3032   );
3033
3034 }
3035
3036 sub re_X {
3037   my($method, $job, %param ) = @_;
3038   if ( $DEBUG ) {
3039     warn "re_X $method for job $job with param:\n".
3040          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3041   }
3042
3043   #some false laziness w/search/cust_bill.html
3044   my $distinct = '';
3045   my $orderby = 'ORDER BY cust_bill._date';
3046
3047   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3048
3049   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3050      
3051   my @cust_bill = qsearch( {
3052     #'select'    => "cust_bill.*",
3053     'table'     => 'cust_bill',
3054     'addl_from' => $addl_from,
3055     'hashref'   => {},
3056     'extra_sql' => $extra_sql,
3057     'order_by'  => $orderby,
3058     'debug' => 1,
3059   } );
3060
3061   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3062
3063   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3064     if $DEBUG;
3065
3066   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3067   foreach my $cust_bill ( @cust_bill ) {
3068     $cust_bill->$method();
3069
3070     if ( $job ) { #progressbar foo
3071       $num++;
3072       if ( time - $min_sec > $last ) {
3073         my $error = $job->update_statustext(
3074           int( 100 * $num / scalar(@cust_bill) )
3075         );
3076         die $error if $error;
3077         $last = time;
3078       }
3079     }
3080
3081   }
3082
3083 }
3084
3085 =back
3086
3087 =head1 CLASS METHODS
3088
3089 =over 4
3090
3091 =item owed_sql
3092
3093 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3094
3095 =cut
3096
3097 sub owed_sql {
3098   my $class = shift;
3099   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3100 }
3101
3102 =item net_sql
3103
3104 Returns an SQL fragment to retreive the net amount (charged minus credited).
3105
3106 =cut
3107
3108 sub net_sql {
3109   my $class = shift;
3110   'charged - '. $class->credited_sql;
3111 }
3112
3113 =item paid_sql
3114
3115 Returns an SQL fragment to retreive the amount paid against this invoice.
3116
3117 =cut
3118
3119 sub paid_sql {
3120   #my $class = shift;
3121   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3122        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
3123 }
3124
3125 =item credited_sql
3126
3127 Returns an SQL fragment to retreive the amount credited against this invoice.
3128
3129 =cut
3130
3131 sub credited_sql {
3132   #my $class = shift;
3133   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3134        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
3135 }
3136
3137 =item search_sql HASHREF
3138
3139 Class method which returns an SQL WHERE fragment to search for parameters
3140 specified in HASHREF.  Valid parameters are
3141
3142 =over 4
3143
3144 =item begin
3145
3146 Epoch date (UNIX timestamp) setting a lower bound for _date values
3147
3148 =item end
3149
3150 Epoch date (UNIX timestamp) setting an upper bound for _date values
3151
3152 =item invnum_min
3153
3154 =item invnum_max
3155
3156 =item agentnum
3157
3158 =item owed
3159
3160 =item net
3161
3162 =item days
3163
3164 =item newest_percust
3165
3166 =back
3167
3168 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3169
3170 =cut
3171
3172 sub search_sql {
3173   my($class, $param) = @_;
3174   if ( $DEBUG ) {
3175     warn "$me search_sql called with params: \n".
3176          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3177   }
3178
3179   my @search = ();
3180
3181   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3182     push @search, "cust_bill._date >= $1";
3183   }
3184   if ( $param->{'end'} =~ /^(\d+)$/ ) {
3185     push @search, "cust_bill._date < $1";
3186   }
3187   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3188     push @search, "cust_bill.invnum >= $1";
3189   }
3190   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3191     push @search, "cust_bill.invnum <= $1";
3192   }
3193   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3194     push @search, "cust_main.agentnum = $1";
3195   }
3196
3197   push @search, '0 != '. FS::cust_bill->owed_sql
3198     if $param->{'open'};
3199
3200   push @search, '0 != '. FS::cust_bill->net_sql
3201     if $param->{'net'};
3202
3203   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3204     if $param->{'days'};
3205
3206   if ( $param->{'newest_percust'} ) {
3207
3208     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3209     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3210
3211     my @newest_where = map { my $x = $_;
3212                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3213                              $x;
3214                            }
3215                            grep ! /^cust_main./, @search;
3216     my $newest_where = scalar(@newest_where)
3217                          ? ' AND '. join(' AND ', @newest_where)
3218                          : '';
3219
3220
3221     push @search, "cust_bill._date = (
3222       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3223         WHERE newest_cust_bill.custnum = cust_bill.custnum
3224           $newest_where
3225     )";
3226
3227   }
3228
3229   my $curuser = $FS::CurrentUser::CurrentUser;
3230   if ( $curuser->username eq 'fs_queue'
3231        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3232     my $username = $1;
3233     my $newuser = qsearchs('access_user', {
3234       'username' => $username,
3235       'disabled' => '',
3236     } );
3237     if ( $newuser ) {
3238       $curuser = $newuser;
3239     } else {
3240       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3241     }
3242   }
3243
3244   push @search, $curuser->agentnums_sql;
3245
3246   join(' AND ', @search );
3247
3248 }
3249
3250 =back
3251
3252 =head1 BUGS
3253
3254 The delete method.
3255
3256 =head1 SEE ALSO
3257
3258 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3259 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3260 documentation.
3261
3262 =cut
3263
3264 1;
3265