config option to omit statement type items from invoices
[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 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 =cut
754
755 sub queueable_send {
756   my %opt = @_;
757
758   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
759     or die "invalid invoice number: " . $opt{invnum};
760
761   my @args = ( $opt{template}, $opt{agentnum} );
762   push @args, $opt{invoice_from}
763     if exists($opt{invoice_from}) && $opt{invoice_from};
764
765   my $error = $self->send( @args );
766   die $error if $error;
767
768 }
769
770 sub send {
771   my $self = shift;
772   my $template = scalar(@_) ? shift : '';
773   if ( scalar(@_) && $_[0]  ) {
774     my $agentnums = ref($_[0]) ? shift : [ shift ];
775     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
776   }
777
778   my $invoice_from =
779     scalar(@_)
780       ? shift
781       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
782
783   my @invoicing_list = $self->cust_main->invoicing_list;
784
785   $self->email($template, $invoice_from)
786     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
787
788   $self->print($template)
789     if grep { $_ eq 'POST' } @invoicing_list; #postal
790
791   $self->fax($template)
792     if grep { $_ eq 'FAX' } @invoicing_list; #fax
793
794   '';
795
796 }
797
798 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
799
800 Emails this invoice.
801
802 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
803
804 INVOICE_FROM, if specified, overrides the default email invoice From: address.
805
806 =cut
807
808 sub queueable_email {
809   my %opt = @_;
810
811   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
812     or die "invalid invoice number: " . $opt{invnum};
813
814   my @args = ( $opt{template} );
815   push @args, $opt{invoice_from}
816     if exists($opt{invoice_from}) && $opt{invoice_from};
817
818   my $error = $self->email( @args );
819   die $error if $error;
820
821 }
822
823 sub email {
824   my $self = shift;
825   my $template = scalar(@_) ? shift : '';
826   my $invoice_from =
827     scalar(@_)
828       ? shift
829       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
830
831   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
832                             $self->cust_main->invoicing_list;
833
834   #better to notify this person than silence
835   @invoicing_list = ($invoice_from) unless @invoicing_list;
836
837   my $error = send_email(
838     $self->generate_email(
839       'from'       => $invoice_from,
840       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
841       'template'   => $template,
842     )
843   );
844   die "can't email invoice: $error\n" if $error;
845   #die "$error\n" if $error;
846
847 }
848
849 =item lpr_data [ TEMPLATENAME ]
850
851 Returns the postscript or plaintext for this invoice as an arrayref.
852
853 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
854
855 =cut
856
857 sub lpr_data {
858   my( $self, $template) = @_;
859   $conf->exists('invoice_latex')
860     ? [ $self->print_ps('', $template) ]
861     : [ $self->print_text('', $template) ];
862 }
863
864 =item print [ TEMPLATENAME ]
865
866 Prints this invoice.
867
868 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
869
870 =cut
871
872 sub print {
873   my $self = shift;
874   my $template = scalar(@_) ? shift : '';
875
876   do_print $self->lpr_data($template);
877 }
878
879 =item fax [ TEMPLATENAME ] 
880
881 Faxes this invoice.
882
883 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
884
885 =cut
886
887 sub fax {
888   my $self = shift;
889   my $template = scalar(@_) ? shift : '';
890
891   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
892     unless $conf->exists('invoice_latex');
893
894   my $dialstring = $self->cust_main->getfield('fax');
895   #Check $dialstring?
896
897   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
898                         'dialstring' => $dialstring,
899                       );
900   die $error if $error;
901
902 }
903
904 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
905
906 Like B<send>, but only sends the invoice if it is the newest open invoice for
907 this customer.
908
909 =cut
910
911 sub send_if_newest {
912   my $self = shift;
913
914   return ''
915     if scalar(
916                grep { $_->owed > 0 } 
917                     qsearch('cust_bill', {
918                       'custnum' => $self->custnum,
919                       #'_date'   => { op=>'>', value=>$self->_date },
920                       'invnum'  => { op=>'>', value=>$self->invnum },
921                     } )
922              );
923     
924   $self->send(@_);
925 }
926
927 =item send_csv OPTION => VALUE, ...
928
929 Sends invoice as a CSV data-file to a remote host with the specified protocol.
930
931 Options are:
932
933 protocol - currently only "ftp"
934 server
935 username
936 password
937 dir
938
939 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
940 and YYMMDDHHMMSS is a timestamp.
941
942 See L</print_csv> for a description of the output format.
943
944 =cut
945
946 sub send_csv {
947   my($self, %opt) = @_;
948
949   #create file(s)
950
951   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
952   mkdir $spooldir, 0700 unless -d $spooldir;
953
954   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
955   my $file = "$spooldir/$tracctnum.csv";
956   
957   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
958
959   open(CSV, ">$file") or die "can't open $file: $!";
960   print CSV $header;
961
962   print CSV $detail;
963
964   close CSV;
965
966   my $net;
967   if ( $opt{protocol} eq 'ftp' ) {
968     eval "use Net::FTP;";
969     die $@ if $@;
970     $net = Net::FTP->new($opt{server}) or die @$;
971   } else {
972     die "unknown protocol: $opt{protocol}";
973   }
974
975   $net->login( $opt{username}, $opt{password} )
976     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
977
978   $net->binary or die "can't set binary mode";
979
980   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
981
982   $net->put($file) or die "can't put $file: $!";
983
984   $net->quit;
985
986   unlink $file;
987
988 }
989
990 =item spool_csv
991
992 Spools CSV invoice data.
993
994 Options are:
995
996 =over 4
997
998 =item format - 'default' or 'billco'
999
1000 =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>).
1001
1002 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1003
1004 =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.
1005
1006 =back
1007
1008 =cut
1009
1010 sub spool_csv {
1011   my($self, %opt) = @_;
1012
1013   my $cust_main = $self->cust_main;
1014
1015   if ( $opt{'dest'} ) {
1016     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1017                              $cust_main->invoicing_list;
1018     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1019                      || ! keys %invoicing_list;
1020   }
1021
1022   if ( $opt{'balanceover'} ) {
1023     return 'N/A'
1024       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1025   }
1026
1027   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1028   mkdir $spooldir, 0700 unless -d $spooldir;
1029
1030   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1031
1032   my $file =
1033     "$spooldir/".
1034     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1035     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1036     '.csv';
1037   
1038   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1039
1040   open(CSV, ">>$file") or die "can't open $file: $!";
1041   flock(CSV, LOCK_EX);
1042   seek(CSV, 0, 2);
1043
1044   print CSV $header;
1045
1046   if ( lc($opt{'format'}) eq 'billco' ) {
1047
1048     flock(CSV, LOCK_UN);
1049     close CSV;
1050
1051     $file =
1052       "$spooldir/".
1053       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1054       '-detail.csv';
1055
1056     open(CSV,">>$file") or die "can't open $file: $!";
1057     flock(CSV, LOCK_EX);
1058     seek(CSV, 0, 2);
1059   }
1060
1061   print CSV $detail;
1062
1063   flock(CSV, LOCK_UN);
1064   close CSV;
1065
1066   return '';
1067
1068 }
1069
1070 =item print_csv OPTION => VALUE, ...
1071
1072 Returns CSV data for this invoice.
1073
1074 Options are:
1075
1076 format - 'default' or 'billco'
1077
1078 Returns a list consisting of two scalars.  The first is a single line of CSV
1079 header information for this invoice.  The second is one or more lines of CSV
1080 detail information for this invoice.
1081
1082 If I<format> is not specified or "default", the fields of the CSV file are as
1083 follows:
1084
1085 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1086
1087 =over 4
1088
1089 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1090
1091 B<record_type> is C<cust_bill> for the initial header line only.  The
1092 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1093 fields are filled in.
1094
1095 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1096 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1097 are filled in.
1098
1099 =item invnum - invoice number
1100
1101 =item custnum - customer number
1102
1103 =item _date - invoice date
1104
1105 =item charged - total invoice amount
1106
1107 =item first - customer first name
1108
1109 =item last - customer first name
1110
1111 =item company - company name
1112
1113 =item address1 - address line 1
1114
1115 =item address2 - address line 1
1116
1117 =item city
1118
1119 =item state
1120
1121 =item zip
1122
1123 =item country
1124
1125 =item pkg - line item description
1126
1127 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1128
1129 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1130
1131 =item sdate - start date for recurring fee
1132
1133 =item edate - end date for recurring fee
1134
1135 =back
1136
1137 If I<format> is "billco", the fields of the header CSV file are as follows:
1138
1139   +-------------------------------------------------------------------+
1140   |                        FORMAT HEADER FILE                         |
1141   |-------------------------------------------------------------------|
1142   | Field | Description                   | Name       | Type | Width |
1143   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1144   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1145   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1146   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1147   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1148   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1149   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1150   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1151   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1152   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1153   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1154   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1155   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1156   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1157   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1158   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1159   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1160   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1161   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1162   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1163   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1164   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1165   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1166   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1167   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1168   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1169   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1170   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1171   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1172   +-------+-------------------------------+------------+------+-------+
1173
1174 If I<format> is "billco", the fields of the detail CSV file are as follows:
1175
1176                                   FORMAT FOR DETAIL FILE
1177         |                            |           |      |
1178   Field | Description                | Name      | Type | Width
1179   1     | N/A-Leave Empty            | RC        | CHAR |     2
1180   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1181   3     | Account Number             | TRACCTNUM | CHAR |    15
1182   4     | Invoice Number             | TRINVOICE | CHAR |    15
1183   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1184   6     | Transaction Detail         | DETAILS   | CHAR |   100
1185   7     | Amount                     | AMT       | NUM* |     9
1186   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1187   9     | Grouping Code              | GROUP     | CHAR |     2
1188   10    | User Defined               | ACCT CODE | CHAR |    15
1189
1190 =cut
1191
1192 sub print_csv {
1193   my($self, %opt) = @_;
1194   
1195   eval "use Text::CSV_XS";
1196   die $@ if $@;
1197
1198   my $cust_main = $self->cust_main;
1199
1200   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1201
1202   if ( lc($opt{'format'}) eq 'billco' ) {
1203
1204     my $taxtotal = 0;
1205     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1206
1207     my $duedate = '';
1208     if (    $conf->exists('invoice_default_terms') 
1209          && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1210       $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1211     }
1212
1213     my( $previous_balance, @unused ) = $self->previous; #previous balance
1214
1215     my $pmt_cr_applied = 0;
1216     $pmt_cr_applied += $_->{'amount'}
1217       foreach ( $self->_items_payments, $self->_items_credits ) ;
1218
1219     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1220
1221     $csv->combine(
1222       '',                         #  1 | N/A-Leave Empty               CHAR   2
1223       '',                         #  2 | N/A-Leave Empty               CHAR  15
1224       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1225       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1226       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1227       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1228       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1229       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1230       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1231       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1232       '',                         # 10 | Ancillary Billing Information CHAR  30
1233       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1234       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1235
1236       # XXX ?
1237       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1238
1239       # XXX ?
1240       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1241
1242       $previous_balance,          # 15 | Previous Balance              NUM*   9
1243       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1244       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1245       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1246       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1247       '',                         # 20 | 30 Day Aging                  NUM*   9
1248       '',                         # 21 | 60 Day Aging                  NUM*   9
1249       '',                         # 22 | 90 Day Aging                  NUM*   9
1250       'N',                        # 23 | Y/N                           CHAR   1
1251       '',                         # 24 | Remittance automation         CHAR 100
1252       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1253       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1254       '0',                        # 27 | Federal Tax***                NUM*   9
1255       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1256       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1257     );
1258
1259   } else {
1260   
1261     $csv->combine(
1262       'cust_bill',
1263       $self->invnum,
1264       $self->custnum,
1265       time2str("%x", $self->_date),
1266       sprintf("%.2f", $self->charged),
1267       ( map { $cust_main->getfield($_) }
1268           qw( first last company address1 address2 city state zip country ) ),
1269       map { '' } (1..5),
1270     ) or die "can't create csv";
1271   }
1272
1273   my $header = $csv->string. "\n";
1274
1275   my $detail = '';
1276   if ( lc($opt{'format'}) eq 'billco' ) {
1277
1278     my $lineseq = 0;
1279     foreach my $item ( $self->_items_pkg ) {
1280
1281       $csv->combine(
1282         '',                     #  1 | N/A-Leave Empty            CHAR   2
1283         '',                     #  2 | N/A-Leave Empty            CHAR  15
1284         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1285         $self->invnum,          #  4 | Invoice Number             CHAR  15
1286         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1287         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1288         $item->{'amount'},      #  7 | Amount                     NUM*   9
1289         '',                     #  8 | Line Format Control**      CHAR   2
1290         '',                     #  9 | Grouping Code              CHAR   2
1291         '',                     # 10 | User Defined               CHAR  15
1292       );
1293
1294       $detail .= $csv->string. "\n";
1295
1296     }
1297
1298   } else {
1299
1300     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1301
1302       my($pkg, $setup, $recur, $sdate, $edate);
1303       if ( $cust_bill_pkg->pkgnum ) {
1304       
1305         ($pkg, $setup, $recur, $sdate, $edate) = (
1306           $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1307           ( $cust_bill_pkg->setup != 0
1308             ? sprintf("%.2f", $cust_bill_pkg->setup )
1309             : '' ),
1310           ( $cust_bill_pkg->recur != 0
1311             ? sprintf("%.2f", $cust_bill_pkg->recur )
1312             : '' ),
1313           ( $cust_bill_pkg->sdate 
1314             ? time2str("%x", $cust_bill_pkg->sdate)
1315             : '' ),
1316           ($cust_bill_pkg->edate 
1317             ?time2str("%x", $cust_bill_pkg->edate)
1318             : '' ),
1319         );
1320   
1321       } else { #pkgnum tax
1322         next unless $cust_bill_pkg->setup != 0;
1323         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1324                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1325                          : 'Tax';
1326         ($pkg, $setup, $recur, $sdate, $edate) =
1327           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1328       }
1329   
1330       $csv->combine(
1331         'cust_bill_pkg',
1332         $self->invnum,
1333         ( map { '' } (1..11) ),
1334         ($pkg, $setup, $recur, $sdate, $edate)
1335       ) or die "can't create csv";
1336
1337       $detail .= $csv->string. "\n";
1338
1339     }
1340
1341   }
1342
1343   ( $header, $detail );
1344
1345 }
1346
1347 =item comp
1348
1349 Pays this invoice with a compliemntary payment.  If there is an error,
1350 returns the error, otherwise returns false.
1351
1352 =cut
1353
1354 sub comp {
1355   my $self = shift;
1356   my $cust_pay = new FS::cust_pay ( {
1357     'invnum'   => $self->invnum,
1358     'paid'     => $self->owed,
1359     '_date'    => '',
1360     'payby'    => 'COMP',
1361     'payinfo'  => $self->cust_main->payinfo,
1362     'paybatch' => '',
1363   } );
1364   $cust_pay->insert;
1365 }
1366
1367 =item realtime_card
1368
1369 Attempts to pay this invoice with a credit card payment via a
1370 Business::OnlinePayment realtime gateway.  See
1371 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1372 for supported processors.
1373
1374 =cut
1375
1376 sub realtime_card {
1377   my $self = shift;
1378   $self->realtime_bop( 'CC', @_ );
1379 }
1380
1381 =item realtime_ach
1382
1383 Attempts to pay this invoice with an electronic check (ACH) payment via a
1384 Business::OnlinePayment realtime gateway.  See
1385 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1386 for supported processors.
1387
1388 =cut
1389
1390 sub realtime_ach {
1391   my $self = shift;
1392   $self->realtime_bop( 'ECHECK', @_ );
1393 }
1394
1395 =item realtime_lec
1396
1397 Attempts to pay this invoice with phone bill (LEC) payment via a
1398 Business::OnlinePayment realtime gateway.  See
1399 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1400 for supported processors.
1401
1402 =cut
1403
1404 sub realtime_lec {
1405   my $self = shift;
1406   $self->realtime_bop( 'LEC', @_ );
1407 }
1408
1409 sub realtime_bop {
1410   my( $self, $method ) = @_;
1411
1412   my $cust_main = $self->cust_main;
1413   my $balance = $cust_main->balance;
1414   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1415   $amount = sprintf("%.2f", $amount);
1416   return "not run (balance $balance)" unless $amount > 0;
1417
1418   my $description = 'Internet Services';
1419   if ( $conf->exists('business-onlinepayment-description') ) {
1420     my $dtempl = $conf->config('business-onlinepayment-description');
1421
1422     my $agent_obj = $cust_main->agent
1423       or die "can't retreive agent for $cust_main (agentnum ".
1424              $cust_main->agentnum. ")";
1425     my $agent = $agent_obj->agent;
1426     my $pkgs = join(', ',
1427       map { $_->cust_pkg->part_pkg->pkg }
1428         grep { $_->pkgnum } $self->cust_bill_pkg
1429     );
1430     $description = eval qq("$dtempl");
1431   }
1432
1433   $cust_main->realtime_bop($method, $amount,
1434     'description' => $description,
1435     'invnum'      => $self->invnum,
1436   );
1437
1438 }
1439
1440 =item batch_card OPTION => VALUE...
1441
1442 Adds a payment for this invoice to the pending credit card batch (see
1443 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1444 runs the payment using a realtime gateway.
1445
1446 =cut
1447
1448 sub batch_card {
1449   my ($self, %options) = @_;
1450   my $cust_main = $self->cust_main;
1451
1452   $options{invnum} = $self->invnum;
1453   
1454   $cust_main->batch_card(%options);
1455 }
1456
1457 sub _agent_template {
1458   my $self = shift;
1459   $self->cust_main->agent_template;
1460 }
1461
1462 sub _agent_invoice_from {
1463   my $self = shift;
1464   $self->cust_main->agent_invoice_from;
1465 }
1466
1467 =item print_text [ TIME [ , TEMPLATE ] ]
1468
1469 Returns an text invoice, as a list of lines.
1470
1471 TIME an optional value used to control the printing of overdue messages.  The
1472 default is now.  It isn't the date of the invoice; that's the `_date' field.
1473 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1474 L<Time::Local> and L<Date::Parse> for conversion functions.
1475
1476 =cut
1477
1478 #still some false laziness w/_items stuff (and send_csv)
1479 sub print_text {
1480
1481   my( $self, $today, $template ) = @_;
1482   $today ||= time;
1483
1484 #  my $invnum = $self->invnum;
1485   my $cust_main = $self->cust_main;
1486   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1487     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1488
1489   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1490 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1491   #my $balance_due = $self->owed + $pr_total - $cr_total;
1492   my $balance_due = $self->owed + $pr_total;
1493
1494   #my @collect = ();
1495   #my($description,$amount);
1496   @buf = ();
1497
1498   #previous balance
1499   unless ($conf->exists('disable_previous_balance')) {
1500     foreach ( @pr_cust_bill ) {
1501       push @buf, [
1502         "Previous Balance, Invoice #". $_->invnum. 
1503                    " (". time2str("%x",$_->_date). ")",
1504         $money_char. sprintf("%10.2f",$_->owed)
1505       ];
1506     }
1507     if (@pr_cust_bill) {
1508       push @buf,['','-----------'];
1509       push @buf,[ 'Total Previous Balance',
1510                   $money_char. sprintf("%10.2f",$pr_total ) ];
1511       push @buf,['',''];
1512     }
1513   }
1514
1515   #new charges
1516   foreach my $cust_bill_pkg (
1517     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1518     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1519   ) {
1520
1521     my $desc = $cust_bill_pkg->desc;
1522
1523     if ( $cust_bill_pkg->pkgnum > 0 ) {
1524
1525       if ( $cust_bill_pkg->setup != 0 ) {
1526         my $description = $desc;
1527         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1528         push @buf, [ $description,
1529                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1530         push @buf,
1531           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1532               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1533       }
1534
1535       if ( $cust_bill_pkg->recur != 0 ) {
1536         push @buf, [
1537           $desc .
1538             ( $conf->exists('disable_line_item_date_ranges')
1539               ? ''
1540               : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1541                        time2str("%x", $cust_bill_pkg->edate) . ")"
1542             ),
1543           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1544         ];
1545         push @buf,
1546           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1547               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1548                                                   $cust_bill_pkg->sdate );
1549       }
1550
1551       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1552
1553     } else { #pkgnum tax or one-shot line item
1554
1555       if ( $cust_bill_pkg->setup != 0 ) {
1556         push @buf, [ $desc,
1557                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1558       }
1559       if ( $cust_bill_pkg->recur != 0 ) {
1560         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1561                               . time2str("%x", $cust_bill_pkg->edate). ")",
1562                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1563                    ];
1564       }
1565
1566     }
1567
1568   }
1569
1570   push @buf,['','-----------'];
1571   push @buf,[ ( $conf->exists('disable_previous_balance')
1572                 ? 'Total Charges'
1573                 : 'Total New Charges'),
1574              $money_char. sprintf("%10.2f",$self->charged) ];
1575   push @buf,['',''];
1576
1577   unless ($conf->exists('disable_previous_balance')) {
1578     push @buf,['','-----------'];
1579     push @buf,['Total Charges',
1580                $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1581     push @buf,['',''];
1582
1583     #credits
1584     foreach ( $self->cust_credited ) {
1585
1586       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1587
1588       my $reason = substr($_->cust_credit->reason,0,32);
1589       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1590       $reason = " ($reason) " if $reason;
1591       push @buf,[
1592         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1593           $reason,
1594         $money_char. sprintf("%10.2f",$_->amount)
1595       ];
1596     }
1597     #foreach ( @cr_cust_credit ) {
1598     #  push @buf,[
1599     #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1600     #    $money_char. sprintf("%10.2f",$_->credited)
1601     #  ];
1602     #}
1603
1604     #get & print payments
1605     foreach ( $self->cust_bill_pay ) {
1606
1607       #something more elaborate if $_->amount ne ->cust_pay->paid ?
1608
1609       push @buf,[
1610         "Payment received ". time2str("%x",$_->cust_pay->_date ),
1611         $money_char. sprintf("%10.2f",$_->amount )
1612       ];
1613     }
1614
1615     #balance due
1616     my $balance_due_msg = $self->balance_due_msg;
1617
1618     push @buf,['','-----------'];
1619     push @buf,[$balance_due_msg, $money_char. 
1620       sprintf("%10.2f", $balance_due ) ];
1621   }
1622
1623   #create the template
1624   $template ||= $self->_agent_template;
1625   my $templatefile = 'invoice_template';
1626   $templatefile .= "_$template" if length($template);
1627   my @invoice_template = $conf->config($templatefile)
1628     or die "cannot load config file $templatefile";
1629   $invoice_lines = 0;
1630   my $wasfunc = 0;
1631   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1632     /invoice_lines\((\d*)\)/;
1633     $invoice_lines += $1 || scalar(@buf);
1634     $wasfunc=1;
1635   }
1636   die "no invoice_lines() functions in template?" unless $wasfunc;
1637   my $invoice_template = new Text::Template (
1638     TYPE   => 'ARRAY',
1639     SOURCE => [ map "$_\n", @invoice_template ],
1640   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1641   $invoice_template->compile()
1642     or die "can't compile template: $Text::Template::ERROR";
1643
1644   #setup template variables
1645   package FS::cust_bill::_template; #!
1646   use vars qw( $custnum $invnum $date $agent @address $overdue
1647                $page $total_pages @buf );
1648
1649   $custnum = $self->custnum;
1650   $invnum = $self->invnum;
1651   $date = $self->_date;
1652   $agent = $self->cust_main->agent->agent;
1653   $page = 1;
1654
1655   if ( $FS::cust_bill::invoice_lines ) {
1656     $total_pages =
1657       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1658     $total_pages++
1659       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1660   } else {
1661     $total_pages = 1;
1662   }
1663
1664   #format address (variable for the template)
1665   my $l = 0;
1666   @address = ( '', '', '', '', '', '' );
1667   package FS::cust_bill; #!
1668   $FS::cust_bill::_template::address[$l++] =
1669     $cust_main->payname.
1670       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1671         ? " (P.O. #". $cust_main->payinfo. ")"
1672         : ''
1673       )
1674   ;
1675   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1676     if $cust_main->company;
1677   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1678   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1679     if $cust_main->address2;
1680   $FS::cust_bill::_template::address[$l++] =
1681     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1682
1683   my $countrydefault = $conf->config('countrydefault') || 'US';
1684   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1685     unless $cust_main->country eq $countrydefault;
1686
1687         #  #overdue? (variable for the template)
1688         #  $FS::cust_bill::_template::overdue = ( 
1689         #    $balance_due > 0
1690         #    && $today > $self->_date 
1691         ##    && $self->printed > 1
1692         #    && $self->printed > 0
1693         #  );
1694
1695   #and subroutine for the template
1696   sub FS::cust_bill::_template::invoice_lines {
1697     my $lines = shift || scalar(@buf);
1698     map { 
1699       scalar(@buf) ? shift @buf : [ '', '' ];
1700     }
1701     ( 1 .. $lines );
1702   }
1703
1704   #and fill it in
1705   $FS::cust_bill::_template::page = 1;
1706   my $lines;
1707   my @collect;
1708   while (@buf) {
1709     push @collect, split("\n",
1710       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1711     );
1712     $FS::cust_bill::_template::page++;
1713   }
1714
1715   map "$_\n", @collect;
1716
1717 }
1718
1719 =item print_latex [ TIME [ , TEMPLATE ] ]
1720
1721 Internal method - returns a filename of a filled-in LaTeX template for this
1722 invoice (Note: add ".tex" to get the actual filename).
1723
1724 See print_ps and print_pdf for methods that return PostScript and PDF output.
1725
1726 TIME an optional value used to control the printing of overdue messages.  The
1727 default is now.  It isn't the date of the invoice; that's the `_date' field.
1728 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1729 L<Time::Local> and L<Date::Parse> for conversion functions.
1730
1731 =cut
1732
1733 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1734 sub print_latex {
1735
1736   my( $self, $today, $template ) = @_;
1737   $today ||= time;
1738   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1739     if $DEBUG;
1740
1741   my $cust_main = $self->cust_main;
1742   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1743     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1744
1745   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1746 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1747   #my $balance_due = $self->owed + $pr_total - $cr_total;
1748   my $balance_due = $self->owed + $pr_total;
1749
1750   #create the template
1751   $template ||= $self->_agent_template;
1752   my $templatefile = 'invoice_latex';
1753   my $suffix = length($template) ? "_$template" : '';
1754   $templatefile .= $suffix;
1755   my @invoice_template = map "$_\n", $conf->config($templatefile)
1756     or die "cannot load config file $templatefile";
1757
1758   my($format, $text_template);
1759   if ( grep { /^%%Detail/ } @invoice_template ) {
1760     #change this to a die when the old code is removed
1761     warn "old-style invoice template $templatefile; ".
1762          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1763     $format = 'old';
1764   } else {
1765     $format = 'Text::Template';
1766     $text_template = new Text::Template(
1767       TYPE => 'ARRAY',
1768       SOURCE => \@invoice_template,
1769       DELIMITERS => [ '[@--', '--@]' ],
1770     );
1771
1772     $text_template->compile()
1773       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1774   }
1775
1776   my $returnaddress;
1777   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1778     $returnaddress = join("\n",
1779       $conf->config_orbase('invoice_latexreturnaddress', $template)
1780     );
1781   } else {
1782     $returnaddress = '~';
1783   }
1784
1785   my %invoice_data = (
1786     'custnum'      => $self->custnum,
1787     'invnum'       => $self->invnum,
1788     'date'         => time2str('%b %o, %Y', $self->_date),
1789     'today'        => time2str('%b %o, %Y', $today),
1790     'agent'        => _latex_escape($cust_main->agent->agent),
1791     'payname'      => _latex_escape($cust_main->payname),
1792     'company'      => _latex_escape($cust_main->company),
1793     'address1'     => _latex_escape($cust_main->address1),
1794     'address2'     => _latex_escape($cust_main->address2),
1795     'city'         => _latex_escape($cust_main->city),
1796     'state'        => _latex_escape($cust_main->state),
1797     'zip'          => _latex_escape($cust_main->zip),
1798     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1799     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1800     'returnaddress' => $returnaddress,
1801     'quantity'     => 1,
1802     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1803     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1804     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1805   );
1806
1807   my $countrydefault = $conf->config('countrydefault') || 'US';
1808   if ( $cust_main->country eq $countrydefault ) {
1809     $invoice_data{'country'} = '';
1810   } else {
1811     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1812   }
1813
1814   $invoice_data{'notes'} =
1815     join("\n",
1816 #  #do variable substitutions in notes
1817 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1818         $conf->config_orbase('invoice_latexnotes', $template)
1819     );
1820   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1821     if $DEBUG;
1822
1823   $invoice_data{'footer'} =~ s/\n+$//;
1824   $invoice_data{'smallfooter'} =~ s/\n+$//;
1825   $invoice_data{'notes'} =~ s/\n+$//;
1826
1827   $invoice_data{'po_line'} =
1828     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1829       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1830       : '~';
1831
1832   my @filled_in = ();
1833   if ( $format eq 'old' ) {
1834   
1835     my @line_item = ();
1836     my @total_item = ();
1837     while ( @invoice_template ) {
1838       my $line = shift @invoice_template;
1839   
1840       if ( $line =~ /^%%Detail\s*$/ ) {
1841   
1842         while ( ( my $line_item_line = shift @invoice_template )
1843                 !~ /^%%EndDetail\s*$/                            ) {
1844           push @line_item, $line_item_line;
1845         }
1846         foreach my $line_item ( $self->_items ) {
1847         #foreach my $line_item ( $self->_items_pkg ) {
1848           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1849           $invoice_data{'description'} =
1850             _latex_escape($line_item->{'description'});
1851           if ( exists $line_item->{'ext_description'} ) {
1852             $invoice_data{'description'} .=
1853               "\\tabularnewline\n~~".
1854               join( "\\tabularnewline\n~~",
1855                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1856                   );
1857           }
1858           $invoice_data{'amount'} = $line_item->{'amount'};
1859           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1860           push @filled_in,
1861             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1862         }
1863   
1864       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1865   
1866         while ( ( my $total_item_line = shift @invoice_template )
1867                 !~ /^%%EndTotalDetails\s*$/                      ) {
1868           push @total_item, $total_item_line;
1869         }
1870   
1871         my @total_fill = ();
1872   
1873         my $taxtotal = 0;
1874         foreach my $tax ( $self->_items_tax ) {
1875           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1876           $taxtotal += $tax->{'amount'};
1877           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1878           push @total_fill,
1879             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1880                 @total_item;
1881         }
1882
1883         if ( $taxtotal ) {
1884           $invoice_data{'total_item'} = 'Sub-total';
1885           $invoice_data{'total_amount'} =
1886             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1887           unshift @total_fill,
1888             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1889                 @total_item;
1890         }
1891   
1892         $invoice_data{'total_item'} = '\textbf{Total}';
1893         $invoice_data{'total_amount'} =
1894           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1895         push @total_fill,
1896           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1897               @total_item;
1898   
1899         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1900   
1901         # credits
1902         foreach my $credit ( $self->_items_credits ) {
1903           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1904           #$credittotal
1905           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1906           push @total_fill, 
1907             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1908                 @total_item;
1909         }
1910   
1911         # payments
1912         foreach my $payment ( $self->_items_payments ) {
1913           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1914           #$paymenttotal
1915           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1916           push @total_fill, 
1917             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1918                 @total_item;
1919         }
1920   
1921         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1922         $invoice_data{'total_amount'} =
1923           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1924         push @total_fill,
1925           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1926               @total_item;
1927   
1928         push @filled_in, @total_fill;
1929   
1930       } else {
1931         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1932         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1933         push @filled_in, $line;
1934       }
1935   
1936     }
1937
1938     sub nounder {
1939       my $var = $1;
1940       $var =~ s/_/\-/g;
1941       $var;
1942     }
1943
1944   } elsif ( $format eq 'Text::Template' ) {
1945
1946     my @detail_items = ();
1947     my @total_items = ();
1948
1949     $invoice_data{'detail_items'} = \@detail_items;
1950     $invoice_data{'total_items'} = \@total_items;
1951   
1952     foreach my $line_item ( $self->_items($conf->exists('disable_previous_balance') ? qw( _items_pkg ) : () ) ) {
1953       my $detail = {
1954         ext_description => [],
1955       };
1956       $detail->{'ref'} = $line_item->{'pkgnum'};
1957       $detail->{'quantity'} = 1;
1958       $detail->{'description'} = _latex_escape($line_item->{'description'});
1959       if ( exists $line_item->{'ext_description'} ) {
1960         @{$detail->{'ext_description'}} = map {
1961           _latex_escape($_);
1962         } @{$line_item->{'ext_description'}};
1963       }
1964       $detail->{'amount'} = $line_item->{'amount'};
1965       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1966   
1967       push @detail_items, $detail;
1968     }
1969   
1970   
1971     my $taxtotal = 0;
1972     foreach my $tax ( $self->_items_tax ) {
1973       my $total = {};
1974       $total->{'total_item'} = _latex_escape($tax->{'description'});
1975       $taxtotal += $tax->{'amount'};
1976       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1977       push @total_items, $total;
1978     }
1979   
1980     if ( $taxtotal ) {
1981       my $total = {};
1982       $total->{'total_item'} = 'Sub-total';
1983       $total->{'total_amount'} =
1984         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1985       unshift @total_items, $total;
1986     }
1987   
1988     {
1989       my $total = {};
1990       $total->{'total_item'} = '\textbf{Total}';
1991       $total->{'total_amount'} =
1992         '\textbf{\dollar '.
1993         sprintf( '%.2f',
1994                  $self->charged + ( $conf->exists('disable_previous_balance')
1995                                     ? 0
1996                                     : $pr_total
1997                                   )
1998                ).
1999       '}';
2000       push @total_items, $total;
2001     }
2002   
2003     unless ($conf->exists('disable_previous_balance')) {
2004       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2005   
2006       # credits
2007       foreach my $credit ( $self->_items_credits ) {
2008         my $total;
2009         $total->{'total_item'} = _latex_escape($credit->{'description'});
2010         #$credittotal
2011         $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2012         push @total_items, $total;
2013       }
2014   
2015       # payments
2016       foreach my $payment ( $self->_items_payments ) {
2017         my $total = {};
2018         $total->{'total_item'} = _latex_escape($payment->{'description'});
2019         #$paymenttotal
2020         $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2021         push @total_items, $total;
2022       }
2023   
2024       { 
2025         my $total;
2026         $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2027         $total->{'total_amount'} =
2028           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2029         push @total_items, $total;
2030       }
2031     }
2032
2033   } else {
2034     die "guru meditation #54";
2035   }
2036
2037   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2038   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2039                            DIR      => $dir,
2040                            SUFFIX   => '.tex',
2041                            UNLINK   => 0,
2042                          ) or die "can't open temp file: $!\n";
2043   if ( $format eq 'old' ) {
2044     print $fh join('', @filled_in );
2045   } elsif ( $format eq 'Text::Template' ) {
2046     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2047   } else {
2048     die "guru meditation #32";
2049   }
2050   close $fh;
2051
2052   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2053   return $1;
2054
2055 }
2056
2057 =item print_ps [ TIME [ , TEMPLATE ] ]
2058
2059 Returns an postscript invoice, as a scalar.
2060
2061 TIME an optional value used to control the printing of overdue messages.  The
2062 default is now.  It isn't the date of the invoice; that's the `_date' field.
2063 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2064 L<Time::Local> and L<Date::Parse> for conversion functions.
2065
2066 =cut
2067
2068 sub print_ps {
2069   my $self = shift;
2070
2071   my $file = $self->print_latex(@_);
2072   FS::Misc::generate_ps($file);
2073
2074 }
2075
2076 =item print_pdf [ TIME [ , TEMPLATE ] ]
2077
2078 Returns an PDF invoice, as a scalar.
2079
2080 TIME an optional value used to control the printing of overdue messages.  The
2081 default is now.  It isn't the date of the invoice; that's the `_date' field.
2082 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2083 L<Time::Local> and L<Date::Parse> for conversion functions.
2084
2085 =cut
2086
2087 sub print_pdf {
2088   my $self = shift;
2089
2090   my $file = $self->print_latex(@_);
2091
2092   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2093   chdir($dir);
2094
2095   #system('pdflatex', "$file.tex");
2096   #system('pdflatex', "$file.tex");
2097   #! LaTeX Error: Unknown graphics extension: .eps.
2098
2099   my $sfile = shell_quote $file;
2100
2101   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2102     or die "pslatex $file.tex failed; see $file.log for details?\n";
2103   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2104     or die "pslatex $file.tex failed; see $file.log for details?\n";
2105
2106   #system('dvipdf', "$file.dvi", "$file.pdf" );
2107   system(
2108     "dvips -q -t letter -f $sfile.dvi ".
2109     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2110     "     -c save pop -"
2111   ) == 0
2112     or die "dvips | gs failed: $!";
2113
2114   open(PDF, "<$file.pdf")
2115     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2116
2117   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2118
2119   my $pdf = '';
2120   while (<PDF>) {
2121     $pdf .= $_;
2122   }
2123
2124   close PDF;
2125
2126   return $pdf;
2127
2128 }
2129
2130 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2131
2132 Returns an HTML invoice, as a scalar.
2133
2134 TIME an optional value used to control the printing of overdue messages.  The
2135 default is now.  It isn't the date of the invoice; that's the `_date' field.
2136 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2137 L<Time::Local> and L<Date::Parse> for conversion functions.
2138
2139 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2140 when emailing the invoice as part of a multipart/related MIME email.
2141
2142 =cut
2143
2144 #some falze laziness w/print_text and print_latex (and send_csv)
2145 sub print_html {
2146   my( $self, $today, $template, $cid ) = @_;
2147   $today ||= time;
2148
2149   my $cust_main = $self->cust_main;
2150   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2151     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2152
2153   $template ||= $self->_agent_template;
2154   my $templatefile = 'invoice_html';
2155   my $suffix = length($template) ? "_$template" : '';
2156   $templatefile .= $suffix;
2157   my @html_template = map "$_\n", $conf->config($templatefile)
2158     or die "cannot load config file $templatefile";
2159
2160   my $html_template = new Text::Template(
2161     TYPE   => 'ARRAY',
2162     SOURCE => \@html_template,
2163     DELIMITERS => [ '<%=', '%>' ],
2164   );
2165
2166   $html_template->compile()
2167     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2168
2169   my %invoice_data = (
2170     'custnum'      => $self->custnum,
2171     'invnum'       => $self->invnum,
2172     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2173     'today'        => time2str('%b %o, %Y', $today),
2174     'agent'        => encode_entities($cust_main->agent->agent),
2175     'payname'      => encode_entities($cust_main->payname),
2176     'company'      => encode_entities($cust_main->company),
2177     'address1'     => encode_entities($cust_main->address1),
2178     'address2'     => encode_entities($cust_main->address2),
2179     'city'         => encode_entities($cust_main->city),
2180     'state'        => encode_entities($cust_main->state),
2181     'zip'          => encode_entities($cust_main->zip),
2182     'terms'        => $conf->config('invoice_default_terms')
2183                       || 'Payable upon receipt',
2184     'cid'          => $cid,
2185     'template'     => $template,
2186 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2187   );
2188
2189   if (
2190          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2191       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2192   ) {
2193     $invoice_data{'returnaddress'} =
2194       join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2195   } else {
2196     $invoice_data{'returnaddress'} =
2197       join("\n", map { 
2198                        s/~/&nbsp;/g;
2199                        s/\\\\\*?\s*$/<BR>/;
2200                        s/\\hyphenation\{[\w\s\-]+\}//;
2201                        $_;
2202                      }
2203                      $conf->config_orbase( 'invoice_latexreturnaddress',
2204                                            $template
2205                                          )
2206           );
2207   }
2208
2209   my $countrydefault = $conf->config('countrydefault') || 'US';
2210   if ( $cust_main->country eq $countrydefault ) {
2211     $invoice_data{'country'} = '';
2212   } else {
2213     $invoice_data{'country'} =
2214       encode_entities(code2country($cust_main->country));
2215   }
2216
2217   if (
2218          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2219       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2220   ) {
2221     $invoice_data{'notes'} =
2222       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2223   } else {
2224     $invoice_data{'notes'} = 
2225       join("\n", map { 
2226                        s/%%(.*)$/<!-- $1 -->/g;
2227                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2228                        s/\\begin\{enumerate\}/<ol>/g;
2229                        s/\\item /  <li>/g;
2230                        s/\\end\{enumerate\}/<\/ol>/g;
2231                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2232                        s/\\\\\*/<br>/g;
2233                        s/\\dollar ?/\$/g;
2234                        s/\\#/#/g;
2235                        s/~/&nbsp;/g;
2236                        $_;
2237                      } 
2238                      $conf->config_orbase('invoice_latexnotes', $template)
2239           );
2240   }
2241
2242 #  #do variable substitutions in notes
2243 #  $invoice_data{'notes'} =
2244 #    join("\n",
2245 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2246 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2247 #    );
2248
2249   if (
2250          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2251       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2252   ) {
2253    $invoice_data{'footer'} =
2254      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2255   } else {
2256    $invoice_data{'footer'} =
2257        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2258                       $conf->config_orbase('invoice_latexfooter', $template)
2259            );
2260   }
2261
2262   $invoice_data{'po_line'} =
2263     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2264       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2265       : '';
2266
2267   my $money_char = $conf->config('money_char') || '$';
2268
2269   foreach my $line_item ( $self->_items($conf->exists('disable_previous_balance') ? qw( _items_pkg ) : () ) ) {
2270     my $detail = {
2271       ext_description => [],
2272     };
2273     $detail->{'ref'} = $line_item->{'pkgnum'};
2274     $detail->{'description'} = encode_entities($line_item->{'description'});
2275     if ( exists $line_item->{'ext_description'} ) {
2276       @{$detail->{'ext_description'}} = map {
2277         encode_entities($_);
2278       } @{$line_item->{'ext_description'}};
2279     }
2280     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2281     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2282
2283     push @{$invoice_data{'detail_items'}}, $detail;
2284   }
2285
2286
2287   my $taxtotal = 0;
2288   foreach my $tax ( $self->_items_tax ) {
2289     my $total = {};
2290     $total->{'total_item'} = encode_entities($tax->{'description'});
2291     $taxtotal += $tax->{'amount'};
2292     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2293     push @{$invoice_data{'total_items'}}, $total;
2294   }
2295
2296   if ( $taxtotal ) {
2297     my $total = {};
2298     $total->{'total_item'} = 'Sub-total';
2299     $total->{'total_amount'} =
2300       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2301     unshift @{$invoice_data{'total_items'}}, $total;
2302   }
2303
2304   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2305   {
2306     my $total = {};
2307     $total->{'total_item'} = '<b>Total</b>';
2308     $total->{'total_amount'} =
2309       "<b>$money_char".
2310       sprintf( '%.2f',
2311                $self->charged + ( $conf->exists('disable_previous_balance')
2312                                   ? 0
2313                                   : $pr_total
2314                                 )
2315              ).
2316       '</b>';
2317     push @{$invoice_data{'total_items'}}, $total;
2318   }
2319
2320   unless ($conf->exists('disable_previous_balance')) {
2321     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2322
2323     # credits
2324     foreach my $credit ( $self->_items_credits ) {
2325       my $total;
2326       $total->{'total_item'} = encode_entities($credit->{'description'});
2327       #$credittotal
2328       $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2329       push @{$invoice_data{'total_items'}}, $total;
2330     }
2331
2332     # payments
2333     foreach my $payment ( $self->_items_payments ) {
2334       my $total = {};
2335       $total->{'total_item'} = encode_entities($payment->{'description'});
2336       #$paymenttotal
2337       $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2338       push @{$invoice_data{'total_items'}}, $total;
2339     }
2340
2341     { 
2342       my $total;
2343       $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2344       $total->{'total_amount'} =
2345         "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2346       push @{$invoice_data{'total_items'}}, $total;
2347     }
2348   }
2349
2350   $html_template->fill_in( HASH => \%invoice_data);
2351 }
2352
2353 # quick subroutine for print_latex
2354 #
2355 # There are ten characters that LaTeX treats as special characters, which
2356 # means that they do not simply typeset themselves: 
2357 #      # $ % & ~ _ ^ \ { }
2358 #
2359 # TeX ignores blanks following an escaped character; if you want a blank (as
2360 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2361
2362 sub _latex_escape {
2363   my $value = shift;
2364   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2365   $value =~ s/([<>])/\$$1\$/g;
2366   $value;
2367 }
2368
2369 #utility methods for print_*
2370
2371 sub balance_due_msg {
2372   my $self = shift;
2373   my $msg = 'Balance Due';
2374   return $msg unless $conf->exists('invoice_default_terms');
2375   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2376     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2377   } elsif ( $conf->config('invoice_default_terms') ) {
2378     $msg .= ' - '. $conf->config('invoice_default_terms');
2379   }
2380   $msg;
2381 }
2382
2383 sub _items {
2384   my $self = shift;
2385   my @display = scalar(@_)
2386                 ? @_
2387                 : qw( _items_previous _items_pkg );
2388                 #: qw( _items_pkg );
2389                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2390   my @b = ();
2391   foreach my $display ( @display ) {
2392     push @b, $self->$display(@_);
2393   }
2394   @b;
2395 }
2396
2397 sub _items_previous {
2398   my $self = shift;
2399   my $cust_main = $self->cust_main;
2400   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2401   my @b = ();
2402   foreach ( @pr_cust_bill ) {
2403     push @b, {
2404       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2405                        ' ('. time2str('%x',$_->_date). ')',
2406       #'pkgpart'     => 'N/A',
2407       'pkgnum'      => 'N/A',
2408       'amount'      => sprintf("%.2f", $_->owed),
2409     };
2410   }
2411   @b;
2412
2413   #{
2414   #    'description'     => 'Previous Balance',
2415   #    #'pkgpart'         => 'N/A',
2416   #    'pkgnum'          => 'N/A',
2417   #    'amount'          => sprintf("%10.2f", $pr_total ),
2418   #    'ext_description' => [ map {
2419   #                                 "Invoice ". $_->invnum.
2420   #                                 " (". time2str("%x",$_->_date). ") ".
2421   #                                 sprintf("%10.2f", $_->owed)
2422   #                         } @pr_cust_bill ],
2423
2424   #};
2425 }
2426
2427 sub _items_pkg {
2428   my $self = shift;
2429   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2430   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2431 }
2432
2433 sub _items_tax {
2434   my $self = shift;
2435   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2436   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2437 }
2438
2439 sub _items_cust_bill_pkg {
2440   my $self = shift;
2441   my $cust_bill_pkg = shift;
2442
2443   my @b = ();
2444   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2445
2446     my $cust_pkg = $cust_bill_pkg->cust_pkg;
2447
2448     my $desc = $cust_bill_pkg->desc;
2449
2450     if ( $cust_bill_pkg->pkgnum > 0 ) {
2451
2452       if ( $cust_bill_pkg->setup != 0 ) {
2453         my $description = $desc;
2454         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2455         my @d = $cust_pkg->h_labels_short($self->_date);
2456         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2457         push @b, {
2458           description     => $description,
2459           #pkgpart         => $part_pkg->pkgpart,
2460           pkgnum          => $cust_bill_pkg->pkgnum,
2461           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2462           ext_description => \@d,
2463         };
2464       }
2465
2466       if ( $cust_bill_pkg->recur != 0 ) {
2467         push @b, {
2468           description     => $desc .
2469                              ( $conf->exists('disable_line_item_date_ranges')
2470                                ? ''
2471                                : " (" .time2str("%x", $cust_bill_pkg->sdate).
2472                                  " - ".time2str("%x", $cust_bill_pkg->edate).")"
2473                              ),
2474           #pkgpart         => $part_pkg->pkgpart,
2475           pkgnum          => $cust_bill_pkg->pkgnum,
2476           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2477           ext_description =>
2478             #at least until cust_bill_pkg has "past" ranges in addition to
2479             #the "future" sdate/edate ones... see #3032
2480             [ $cust_pkg->h_labels_short( $self->_date ),
2481                                          #$cust_bill_pkg->edate,
2482                                          #$cust_bill_pkg->sdate),
2483               $cust_bill_pkg->details,
2484             ],
2485         };
2486       }
2487
2488     } else { #pkgnum tax or one-shot line item (??)
2489
2490       if ( $cust_bill_pkg->setup != 0 ) {
2491         push @b, {
2492           'description' => $desc,
2493           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2494         };
2495       }
2496       if ( $cust_bill_pkg->recur != 0 ) {
2497         push @b, {
2498           'description' => "$desc (".
2499                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2500                            time2str("%x", $cust_bill_pkg->edate). ')',
2501           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2502         };
2503       }
2504
2505     }
2506
2507   }
2508
2509   @b;
2510
2511 }
2512
2513 sub _items_credits {
2514   my $self = shift;
2515
2516   my @b;
2517   #credits
2518   foreach ( $self->cust_credited ) {
2519
2520     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2521
2522     my $reason = $_->cust_credit->reason;
2523     #my $reason = substr($_->cust_credit->reason,0,32);
2524     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2525     $reason = " ($reason) " if $reason;
2526     push @b, {
2527       #'description' => 'Credit ref\#'. $_->crednum.
2528       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2529       #                 $reason,
2530       'description' => 'Credit applied '.
2531                        time2str("%x",$_->cust_credit->_date). $reason,
2532       'amount'      => sprintf("%.2f",$_->amount),
2533     };
2534   }
2535   #foreach ( @cr_cust_credit ) {
2536   #  push @buf,[
2537   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2538   #    $money_char. sprintf("%10.2f",$_->credited)
2539   #  ];
2540   #}
2541
2542   @b;
2543
2544 }
2545
2546 sub _items_payments {
2547   my $self = shift;
2548
2549   my @b;
2550   #get & print payments
2551   foreach ( $self->cust_bill_pay ) {
2552
2553     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2554
2555     push @b, {
2556       'description' => "Payment received ".
2557                        time2str("%x",$_->cust_pay->_date ),
2558       'amount'      => sprintf("%.2f", $_->amount )
2559     };
2560   }
2561
2562   @b;
2563
2564 }
2565
2566
2567 =back
2568
2569 =head1 SUBROUTINES
2570
2571 =over 4
2572
2573 =item reprint
2574
2575 =cut
2576
2577 sub process_reprint {
2578   process_re_X('print', @_);
2579 }
2580
2581 =item reemail
2582
2583 =cut
2584
2585 sub process_reemail {
2586   process_re_X('email', @_);
2587 }
2588
2589 =item refax
2590
2591 =cut
2592
2593 sub process_refax {
2594   process_re_X('fax', @_);
2595 }
2596
2597 use Storable qw(thaw);
2598 use Data::Dumper;
2599 use MIME::Base64;
2600 sub process_re_X {
2601   my( $method, $job ) = ( shift, shift );
2602   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2603
2604   my $param = thaw(decode_base64(shift));
2605   warn Dumper($param) if $DEBUG;
2606
2607   re_X(
2608     $method,
2609     $job,
2610     %$param,
2611   );
2612
2613 }
2614
2615 sub re_X {
2616   my($method, $job, %param ) = @_;
2617   if ( $DEBUG ) {
2618     warn "re_X $method for job $job with param:\n".
2619          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2620   }
2621
2622   #some false laziness w/search/cust_bill.html
2623   my $distinct = '';
2624   my $orderby = 'ORDER BY cust_bill._date';
2625
2626   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2627
2628   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2629      
2630   my @cust_bill = qsearch( {
2631     #'select'    => "cust_bill.*",
2632     'table'     => 'cust_bill',
2633     'addl_from' => $addl_from,
2634     'hashref'   => {},
2635     'extra_sql' => $extra_sql,
2636     'order_by'  => $orderby,
2637     'debug' => 1,
2638   } );
2639
2640   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2641     if $DEBUG;
2642
2643   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2644   foreach my $cust_bill ( @cust_bill ) {
2645     $cust_bill->$method();
2646
2647     if ( $job ) { #progressbar foo
2648       $num++;
2649       if ( time - $min_sec > $last ) {
2650         my $error = $job->update_statustext(
2651           int( 100 * $num / scalar(@cust_bill) )
2652         );
2653         die $error if $error;
2654         $last = time;
2655       }
2656     }
2657
2658   }
2659
2660 }
2661
2662 =back
2663
2664 =head1 CLASS METHODS
2665
2666 =over 4
2667
2668 =item owed_sql
2669
2670 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2671
2672 =cut
2673
2674 sub owed_sql {
2675   my $class = shift;
2676   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2677 }
2678
2679 =item net_sql
2680
2681 Returns an SQL fragment to retreive the net amount (charged minus credited).
2682
2683 =cut
2684
2685 sub net_sql {
2686   my $class = shift;
2687   'charged - '. $class->credited_sql;
2688 }
2689
2690 =item paid_sql
2691
2692 Returns an SQL fragment to retreive the amount paid against this invoice.
2693
2694 =cut
2695
2696 sub paid_sql {
2697   #my $class = shift;
2698   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2699        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
2700 }
2701
2702 =item credited_sql
2703
2704 Returns an SQL fragment to retreive the amount credited against this invoice.
2705
2706 =cut
2707
2708 sub credited_sql {
2709   #my $class = shift;
2710   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2711        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
2712 }
2713
2714 =item search_sql HASHREF
2715
2716 Class method which returns an SQL WHERE fragment to search for parameters
2717 specified in HASHREF.  Valid parameters are
2718
2719 =over 4
2720
2721 =item begin
2722
2723 Epoch date (UNIX timestamp) setting a lower bound for _date values
2724
2725 =item end
2726
2727 Epoch date (UNIX timestamp) setting an upper bound for _date values
2728
2729 =item invnum_min
2730
2731 =item invnum_max
2732
2733 =item agentnum
2734
2735 =item owed
2736
2737 =item net
2738
2739 =item days
2740
2741 =item newest_percust
2742
2743 =back
2744
2745 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2746
2747 =cut
2748
2749 sub search_sql {
2750   my($class, $param) = @_;
2751   if ( $DEBUG ) {
2752     warn "$me search_sql called with params: \n".
2753          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
2754   }
2755
2756   my @search = ();
2757
2758   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2759     push @search, "cust_bill._date >= $1";
2760   }
2761   if ( $param->{'end'} =~ /^(\d+)$/ ) {
2762     push @search, "cust_bill._date < $1";
2763   }
2764   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2765     push @search, "cust_bill.invnum >= $1";
2766   }
2767   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2768     push @search, "cust_bill.invnum <= $1";
2769   }
2770   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2771     push @search, "cust_main.agentnum = $1";
2772   }
2773
2774   push @search, '0 != '. FS::cust_bill->owed_sql
2775     if $param->{'open'};
2776
2777   push @search, '0 != '. FS::cust_bill->net_sql
2778     if $param->{'net'};
2779
2780   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2781     if $param->{'days'};
2782
2783   if ( $param->{'newest_percust'} ) {
2784
2785     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2786     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2787
2788     my @newest_where = map { my $x = $_;
2789                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
2790                              $x;
2791                            }
2792                            grep ! /^cust_main./, @search;
2793     my $newest_where = scalar(@newest_where)
2794                          ? ' AND '. join(' AND ', @newest_where)
2795                          : '';
2796
2797
2798     push @search, "cust_bill._date = (
2799       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2800         WHERE newest_cust_bill.custnum = cust_bill.custnum
2801           $newest_where
2802     )";
2803
2804   }
2805
2806   my $curuser = $FS::CurrentUser::CurrentUser;
2807   if ( $curuser->username eq 'fs_queue'
2808        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2809     my $username = $1;
2810     my $newuser = qsearchs('access_user', {
2811       'username' => $username,
2812       'disabled' => '',
2813     } );
2814     if ( $newuser ) {
2815       $curuser = $newuser;
2816     } else {
2817       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2818     }
2819   }
2820
2821   push @search, $curuser->agentnums_sql;
2822
2823   join(' AND ', @search );
2824
2825 }
2826
2827 =back
2828
2829 =head1 BUGS
2830
2831 The delete method.
2832
2833 =head1 SEE ALSO
2834
2835 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2836 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2837 documentation.
2838
2839 =cut
2840
2841 1;
2842