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