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