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