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