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