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