per-customer invoice terms override
[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  { $_->part_pkg->pay_weight || 0 }
436              grep { $_ }
437              map  { $_->cust_pkg }
438                   @open_lineitems
439            );
440       my $max_credit_weight =
441         max( map  { $_->part_pkg->credit_weight || 0 }
442              grep { $_ } 
443              map  { $_->cust_pkg }
444                   @open_lineitems
445            );
446
447       #if both are the same... payments first?  it has to be something
448       if ( $max_pay_weight >= $max_credit_weight ) {
449         $app = 'pay';
450       } else {
451         $app = 'credit';
452       }
453     
454     } elsif ( @payments ) {
455       $app = 'pay';
456     } elsif ( @credits ) {
457       $app = 'credit';
458     } else {
459       die "guru meditation #12 and 35";
460     }
461
462     if ( $app eq 'pay' ) {
463
464       my $payment = shift @payments;
465
466       $app = new FS::cust_bill_pay {
467         'paynum'  => $payment->paynum,
468         'amount'  => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
469       };
470
471     } elsif ( $app eq 'credit' ) {
472
473       my $credit = shift @credits;
474
475       $app = new FS::cust_credit_bill {
476         'crednum' => $credit->crednum,
477         'amount'  => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
478       };
479
480     } else {
481       die "guru meditation #12 and 35";
482     }
483
484     $app->invnum( $self->invnum );
485
486     my $error = $app->insert;
487     die $error if $error;
488
489   }
490
491 }
492
493 =item generate_email PARAMHASH
494
495 PARAMHASH can contain the following:
496
497 =over 4
498
499 =item from       => sender address, required
500
501 =item tempate    => alternate template name, optional
502
503 =item print_text => text attachment arrayref, optional
504
505 =item subject    => email subject, optional
506
507 =back
508
509 Returns an argument list to be passed to L<FS::Misc::send_email>.
510
511 =cut
512
513 use MIME::Entity;
514
515 sub generate_email {
516
517   my $self = shift;
518   my %args = @_;
519
520   my $me = '[FS::cust_bill::generate_email]';
521
522   my %return = (
523     'from'      => $args{'from'},
524     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
525   );
526
527   if (ref($args{'to'}) eq 'ARRAY') {
528     $return{'to'} = $args{'to'};
529   } else {
530     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
531                            $self->cust_main->invoicing_list
532                     ];
533   }
534
535   if ( $conf->exists('invoice_html') ) {
536
537     warn "$me creating HTML/text multipart message"
538       if $DEBUG;
539
540     $return{'nobody'} = 1;
541
542     my $alternative = build MIME::Entity
543       'Type'        => 'multipart/alternative',
544       'Encoding'    => '7bit',
545       'Disposition' => 'inline'
546     ;
547
548     my $data;
549     if ( $conf->exists('invoice_email_pdf')
550          and scalar($conf->config('invoice_email_pdf_note')) ) {
551
552       warn "$me using 'invoice_email_pdf_note' in multipart message"
553         if $DEBUG;
554       $data = [ map { $_ . "\n" }
555                     $conf->config('invoice_email_pdf_note')
556               ];
557
558     } else {
559
560       warn "$me not using 'invoice_email_pdf_note' in multipart message"
561         if $DEBUG;
562       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
563         $data = $args{'print_text'};
564       } else {
565         $data = [ $self->print_text('', $args{'template'}) ];
566       }
567
568     }
569
570     $alternative->attach(
571       'Type'        => 'text/plain',
572       #'Encoding'    => 'quoted-printable',
573       'Encoding'    => '7bit',
574       'Data'        => $data,
575       'Disposition' => 'inline',
576     );
577
578     $args{'from'} =~ /\@([\w\.\-]+)/;
579     my $from = $1 || 'example.com';
580     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
581
582     my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
583     my $file;
584     if ( defined($args{'template'}) && length($args{'template'})
585          && -e "$path/logo_". $args{'template'}. ".png"
586        )
587     {
588       $file = "$path/logo_". $args{'template'}. ".png";
589     } else {
590       $file = "$path/logo.png";
591     }
592
593     my $image = build MIME::Entity
594       'Type'       => 'image/png',
595       'Encoding'   => 'base64',
596       'Path'       => $file,
597       'Filename'   => 'logo.png',
598       'Content-ID' => "<$content_id>",
599     ;
600
601     $alternative->attach(
602       'Type'        => 'text/html',
603       'Encoding'    => 'quoted-printable',
604       'Data'        => [ '<html>',
605                          '  <head>',
606                          '    <title>',
607                          '      '. encode_entities($return{'subject'}), 
608                          '    </title>',
609                          '  </head>',
610                          '  <body bgcolor="#e8e8e8">',
611                          $self->print_html('', $args{'template'}, $content_id),
612                          '  </body>',
613                          '</html>',
614                        ],
615       'Disposition' => 'inline',
616       #'Filename'    => 'invoice.pdf',
617     );
618
619     if ( $conf->exists('invoice_email_pdf') ) {
620
621       #attaching pdf too:
622       # multipart/mixed
623       #   multipart/related
624       #     multipart/alternative
625       #       text/plain
626       #       text/html
627       #     image/png
628       #   application/pdf
629
630       my $related = build MIME::Entity 'Type'     => 'multipart/related',
631                                        'Encoding' => '7bit';
632
633       #false laziness w/Misc::send_email
634       $related->head->replace('Content-type',
635         $related->mime_type.
636         '; boundary="'. $related->head->multipart_boundary. '"'.
637         '; type=multipart/alternative'
638       );
639
640       $related->add_part($alternative);
641
642       $related->add_part($image);
643
644       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
645
646       $return{'mimeparts'} = [ $related, $pdf ];
647
648     } else {
649
650       #no other attachment:
651       # multipart/related
652       #   multipart/alternative
653       #     text/plain
654       #     text/html
655       #   image/png
656
657       $return{'content-type'} = 'multipart/related';
658       $return{'mimeparts'} = [ $alternative, $image ];
659       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
660       #$return{'disposition'} = 'inline';
661
662     }
663   
664   } else {
665
666     if ( $conf->exists('invoice_email_pdf') ) {
667       warn "$me creating PDF attachment"
668         if $DEBUG;
669
670       #mime parts arguments a la MIME::Entity->build().
671       $return{'mimeparts'} = [
672         { $self->mimebuild_pdf('', $args{'template'}) }
673       ];
674     }
675   
676     if ( $conf->exists('invoice_email_pdf')
677          and scalar($conf->config('invoice_email_pdf_note')) ) {
678
679       warn "$me using 'invoice_email_pdf_note'"
680         if $DEBUG;
681       $return{'body'} = [ map { $_ . "\n" }
682                               $conf->config('invoice_email_pdf_note')
683                         ];
684
685     } else {
686
687       warn "$me not using 'invoice_email_pdf_note'"
688         if $DEBUG;
689       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
690         $return{'body'} = $args{'print_text'};
691       } else {
692         $return{'body'} = [ $self->print_text('', $args{'template'}) ];
693       }
694
695     }
696
697   }
698
699   %return;
700
701 }
702
703 =item mimebuild_pdf
704
705 Returns a list suitable for passing to MIME::Entity->build(), representing
706 this invoice as PDF attachment.
707
708 =cut
709
710 sub mimebuild_pdf {
711   my $self = shift;
712   (
713     'Type'        => 'application/pdf',
714     'Encoding'    => 'base64',
715     'Data'        => [ $self->print_pdf(@_) ],
716     'Disposition' => 'attachment',
717     'Filename'    => 'invoice.pdf',
718   );
719 }
720
721 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
722
723 Sends this invoice to the destinations configured for this customer: sends
724 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
725
726 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
727
728 AGENTNUM, if specified, means that this invoice will only be sent for customers
729 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
730 single agent) or an arrayref of agentnums.
731
732 INVOICE_FROM, if specified, overrides the default email invoice From: address.
733
734 =cut
735
736 sub queueable_send {
737   my %opt = @_;
738
739   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
740     or die "invalid invoice number: " . $opt{invnum};
741
742   my @args = ( $opt{template}, $opt{agentnum} );
743   push @args, $opt{invoice_from}
744     if exists($opt{invoice_from}) && $opt{invoice_from};
745
746   my $error = $self->send( @args );
747   die $error if $error;
748
749 }
750
751 sub send {
752   my $self = shift;
753   my $template = scalar(@_) ? shift : '';
754   if ( scalar(@_) && $_[0]  ) {
755     my $agentnums = ref($_[0]) ? shift : [ shift ];
756     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
757   }
758
759   my $invoice_from =
760     scalar(@_)
761       ? shift
762       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
763
764   my @invoicing_list = $self->cust_main->invoicing_list;
765
766   $self->email($template, $invoice_from)
767     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
768
769   $self->print($template)
770     if grep { $_ eq 'POST' } @invoicing_list; #postal
771
772   $self->fax($template)
773     if grep { $_ eq 'FAX' } @invoicing_list; #fax
774
775   '';
776
777 }
778
779 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
780
781 Emails this invoice.
782
783 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
784
785 INVOICE_FROM, if specified, overrides the default email invoice From: address.
786
787 =cut
788
789 sub queueable_email {
790   my %opt = @_;
791
792   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
793     or die "invalid invoice number: " . $opt{invnum};
794
795   my @args = ( $opt{template} );
796   push @args, $opt{invoice_from}
797     if exists($opt{invoice_from}) && $opt{invoice_from};
798
799   my $error = $self->email( @args );
800   die $error if $error;
801
802 }
803
804 sub email {
805   my $self = shift;
806   my $template = scalar(@_) ? shift : '';
807   my $invoice_from =
808     scalar(@_)
809       ? shift
810       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
811
812   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
813                             $self->cust_main->invoicing_list;
814
815   #better to notify this person than silence
816   @invoicing_list = ($invoice_from) unless @invoicing_list;
817
818   my $error = send_email(
819     $self->generate_email(
820       'from'       => $invoice_from,
821       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
822       'template'   => $template,
823     )
824   );
825   die "can't email invoice: $error\n" if $error;
826   #die "$error\n" if $error;
827
828 }
829
830 =item lpr_data [ TEMPLATENAME ]
831
832 Returns the postscript or plaintext for this invoice as an arrayref.
833
834 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
835
836 =cut
837
838 sub lpr_data {
839   my( $self, $template) = @_;
840   $conf->exists('invoice_latex')
841     ? [ $self->print_ps('', $template) ]
842     : [ $self->print_text('', $template) ];
843 }
844
845 =item print [ TEMPLATENAME ]
846
847 Prints this invoice.
848
849 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
850
851 =cut
852
853 sub print {
854   my $self = shift;
855   my $template = scalar(@_) ? shift : '';
856
857   my $lpr = $conf->config('lpr');
858
859   my $outerr = '';
860   run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
861   if ( $? ) {
862     $outerr = ": $outerr" if length($outerr);
863     die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
864   }
865
866 }
867
868 =item fax [ TEMPLATENAME ] 
869
870 Faxes this invoice.
871
872 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
873
874 =cut
875
876 sub fax {
877   my $self = shift;
878   my $template = scalar(@_) ? shift : '';
879
880   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
881     unless $conf->exists('invoice_latex');
882
883   my $dialstring = $self->cust_main->getfield('fax');
884   #Check $dialstring?
885
886   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
887                         'dialstring' => $dialstring,
888                       );
889   die $error if $error;
890
891 }
892
893 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
894
895 Like B<send>, but only sends the invoice if it is the newest open invoice for
896 this customer.
897
898 =cut
899
900 sub send_if_newest {
901   my $self = shift;
902
903   return ''
904     if scalar(
905                grep { $_->owed > 0 } 
906                     qsearch('cust_bill', {
907                       'custnum' => $self->custnum,
908                       #'_date'   => { op=>'>', value=>$self->_date },
909                       'invnum'  => { op=>'>', value=>$self->invnum },
910                     } )
911              );
912     
913   $self->send(@_);
914 }
915
916 =item send_csv OPTION => VALUE, ...
917
918 Sends invoice as a CSV data-file to a remote host with the specified protocol.
919
920 Options are:
921
922 protocol - currently only "ftp"
923 server
924 username
925 password
926 dir
927
928 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
929 and YYMMDDHHMMSS is a timestamp.
930
931 See L</print_csv> for a description of the output format.
932
933 =cut
934
935 sub send_csv {
936   my($self, %opt) = @_;
937
938   #create file(s)
939
940   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
941   mkdir $spooldir, 0700 unless -d $spooldir;
942
943   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
944   my $file = "$spooldir/$tracctnum.csv";
945   
946   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
947
948   open(CSV, ">$file") or die "can't open $file: $!";
949   print CSV $header;
950
951   print CSV $detail;
952
953   close CSV;
954
955   my $net;
956   if ( $opt{protocol} eq 'ftp' ) {
957     eval "use Net::FTP;";
958     die $@ if $@;
959     $net = Net::FTP->new($opt{server}) or die @$;
960   } else {
961     die "unknown protocol: $opt{protocol}";
962   }
963
964   $net->login( $opt{username}, $opt{password} )
965     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
966
967   $net->binary or die "can't set binary mode";
968
969   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
970
971   $net->put($file) or die "can't put $file: $!";
972
973   $net->quit;
974
975   unlink $file;
976
977 }
978
979 =item spool_csv
980
981 Spools CSV invoice data.
982
983 Options are:
984
985 =over 4
986
987 =item format - 'default' or 'billco'
988
989 =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>).
990
991 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
992
993 =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.
994
995 =back
996
997 =cut
998
999 sub spool_csv {
1000   my($self, %opt) = @_;
1001
1002   my $cust_main = $self->cust_main;
1003
1004   if ( $opt{'dest'} ) {
1005     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1006                              $cust_main->invoicing_list;
1007     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1008                      || ! keys %invoicing_list;
1009   }
1010
1011   if ( $opt{'balanceover'} ) {
1012     return 'N/A'
1013       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1014   }
1015
1016   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1017   mkdir $spooldir, 0700 unless -d $spooldir;
1018
1019   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1020
1021   my $file =
1022     "$spooldir/".
1023     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1024     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1025     '.csv';
1026   
1027   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1028
1029   open(CSV, ">>$file") or die "can't open $file: $!";
1030   flock(CSV, LOCK_EX);
1031   seek(CSV, 0, 2);
1032
1033   print CSV $header;
1034
1035   if ( lc($opt{'format'}) eq 'billco' ) {
1036
1037     flock(CSV, LOCK_UN);
1038     close CSV;
1039
1040     $file =
1041       "$spooldir/".
1042       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1043       '-detail.csv';
1044
1045     open(CSV,">>$file") or die "can't open $file: $!";
1046     flock(CSV, LOCK_EX);
1047     seek(CSV, 0, 2);
1048   }
1049
1050   print CSV $detail;
1051
1052   flock(CSV, LOCK_UN);
1053   close CSV;
1054
1055   return '';
1056
1057 }
1058
1059 =item print_csv OPTION => VALUE, ...
1060
1061 Returns CSV data for this invoice.
1062
1063 Options are:
1064
1065 format - 'default' or 'billco'
1066
1067 Returns a list consisting of two scalars.  The first is a single line of CSV
1068 header information for this invoice.  The second is one or more lines of CSV
1069 detail information for this invoice.
1070
1071 If I<format> is not specified or "default", the fields of the CSV file are as
1072 follows:
1073
1074 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1075
1076 =over 4
1077
1078 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1079
1080 B<record_type> is C<cust_bill> for the initial header line only.  The
1081 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1082 fields are filled in.
1083
1084 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1085 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1086 are filled in.
1087
1088 =item invnum - invoice number
1089
1090 =item custnum - customer number
1091
1092 =item _date - invoice date
1093
1094 =item charged - total invoice amount
1095
1096 =item first - customer first name
1097
1098 =item last - customer first name
1099
1100 =item company - company name
1101
1102 =item address1 - address line 1
1103
1104 =item address2 - address line 1
1105
1106 =item city
1107
1108 =item state
1109
1110 =item zip
1111
1112 =item country
1113
1114 =item pkg - line item description
1115
1116 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1117
1118 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1119
1120 =item sdate - start date for recurring fee
1121
1122 =item edate - end date for recurring fee
1123
1124 =back
1125
1126 If I<format> is "billco", the fields of the header CSV file are as follows:
1127
1128   +-------------------------------------------------------------------+
1129   |                        FORMAT HEADER FILE                         |
1130   |-------------------------------------------------------------------|
1131   | Field | Description                   | Name       | Type | Width |
1132   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1133   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1134   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1135   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1136   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1137   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1138   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1139   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1140   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1141   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1142   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1143   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1144   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1145   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1146   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1147   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1148   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1149   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1150   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1151   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1152   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1153   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1154   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1155   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1156   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1157   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1158   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1159   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1160   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1161   +-------+-------------------------------+------------+------+-------+
1162
1163 If I<format> is "billco", the fields of the detail CSV file are as follows:
1164
1165                                   FORMAT FOR DETAIL FILE
1166         |                            |           |      |
1167   Field | Description                | Name      | Type | Width
1168   1     | N/A-Leave Empty            | RC        | CHAR |     2
1169   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1170   3     | Account Number             | TRACCTNUM | CHAR |    15
1171   4     | Invoice Number             | TRINVOICE | CHAR |    15
1172   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1173   6     | Transaction Detail         | DETAILS   | CHAR |   100
1174   7     | Amount                     | AMT       | NUM* |     9
1175   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1176   9     | Grouping Code              | GROUP     | CHAR |     2
1177   10    | User Defined               | ACCT CODE | CHAR |    15
1178
1179 =cut
1180
1181 sub print_csv {
1182   my($self, %opt) = @_;
1183   
1184   eval "use Text::CSV_XS";
1185   die $@ if $@;
1186
1187   my $cust_main = $self->cust_main;
1188
1189   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1190
1191   if ( lc($opt{'format'}) eq 'billco' ) {
1192
1193     my $taxtotal = 0;
1194     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1195
1196     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
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), and a filename of
1827 an associated logo (with the .eps extension included).
1828
1829 See print_ps and print_pdf for methods that return PostScript and PDF output.
1830
1831 TIME an optional value used to control the printing of overdue messages.  The
1832 default is now.  It isn't the date of the invoice; that's the `_date' field.
1833 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1834 L<Time::Local> and L<Date::Parse> for conversion functions.
1835
1836 =cut
1837
1838 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1839 sub print_latex {
1840
1841   my( $self, $today, $template ) = @_;
1842   $today ||= time;
1843   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1844     if $DEBUG;
1845
1846   my $cust_main = $self->cust_main;
1847   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1848     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1849
1850   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1851 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1852   #my $balance_due = $self->owed + $pr_total - $cr_total;
1853   my $balance_due = $self->owed + $pr_total;
1854
1855   #create the template
1856   $template ||= $self->_agent_template;
1857   my $templatefile = 'invoice_latex';
1858   my $suffix = length($template) ? "_$template" : '';
1859   $templatefile .= $suffix;
1860   my @invoice_template = map "$_\n", $conf->config($templatefile)
1861     or die "cannot load config file $templatefile";
1862
1863   my($format, $text_template);
1864   if ( grep { /^%%Detail/ } @invoice_template ) {
1865     #change this to a die when the old code is removed
1866     warn "old-style invoice template $templatefile; ".
1867          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1868     $format = 'old';
1869   } else {
1870     $format = 'Text::Template';
1871     $text_template = new Text::Template(
1872       TYPE => 'ARRAY',
1873       SOURCE => \@invoice_template,
1874       DELIMITERS => [ '[@--', '--@]' ],
1875     );
1876
1877     $text_template->compile()
1878       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1879   }
1880
1881   my $returnaddress;
1882   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1883     $returnaddress = join("\n",
1884       $conf->config_orbase('invoice_latexreturnaddress', $template)
1885     );
1886   } else {
1887     $returnaddress = '~';
1888   }
1889
1890   my %invoice_data = (
1891     'custnum'      => $self->custnum,
1892     'invnum'       => $self->invnum,
1893     'date'         => time2str('%b %o, %Y', $self->_date),
1894     'today'        => time2str('%b %o, %Y', $today),
1895     'agent'        => _latex_escape($cust_main->agent->agent),
1896     'payname'      => _latex_escape($cust_main->payname),
1897     'company'      => _latex_escape($cust_main->company),
1898     'address1'     => _latex_escape($cust_main->address1),
1899     'address2'     => _latex_escape($cust_main->address2),
1900     'city'         => _latex_escape($cust_main->city),
1901     'state'        => _latex_escape($cust_main->state),
1902     'zip'          => _latex_escape($cust_main->zip),
1903     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1904     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1905     'returnaddress' => $returnaddress,
1906     'quantity'     => 1,
1907     'terms'        => $self->terms,
1908     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1909     # better hang on to conf_dir for a while
1910     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1911   );
1912
1913   my $countrydefault = $conf->config('countrydefault') || 'US';
1914   if ( $cust_main->country eq $countrydefault ) {
1915     $invoice_data{'country'} = '';
1916   } else {
1917     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1918   }
1919
1920   $invoice_data{'notes'} =
1921     join("\n",
1922 #  #do variable substitutions in notes
1923 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1924         $conf->config_orbase('invoice_latexnotes', $template)
1925     );
1926   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1927     if $DEBUG;
1928
1929   $invoice_data{'footer'} =~ s/\n+$//;
1930   $invoice_data{'smallfooter'} =~ s/\n+$//;
1931   $invoice_data{'notes'} =~ s/\n+$//;
1932
1933   $invoice_data{'po_line'} =
1934     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1935       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1936       : '~';
1937
1938   my @filled_in = ();
1939   if ( $format eq 'old' ) {
1940   
1941     my @line_item = ();
1942     my @total_item = ();
1943     while ( @invoice_template ) {
1944       my $line = shift @invoice_template;
1945   
1946       if ( $line =~ /^%%Detail\s*$/ ) {
1947   
1948         while ( ( my $line_item_line = shift @invoice_template )
1949                 !~ /^%%EndDetail\s*$/                            ) {
1950           push @line_item, $line_item_line;
1951         }
1952         foreach my $line_item ( $self->_items ) {
1953         #foreach my $line_item ( $self->_items_pkg ) {
1954           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1955           $invoice_data{'description'} =
1956             _latex_escape($line_item->{'description'});
1957           if ( exists $line_item->{'ext_description'} ) {
1958             $invoice_data{'description'} .=
1959               "\\tabularnewline\n~~".
1960               join( "\\tabularnewline\n~~",
1961                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1962                   );
1963           }
1964           $invoice_data{'amount'} = $line_item->{'amount'};
1965           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1966           push @filled_in,
1967             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1968         }
1969   
1970       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1971   
1972         while ( ( my $total_item_line = shift @invoice_template )
1973                 !~ /^%%EndTotalDetails\s*$/                      ) {
1974           push @total_item, $total_item_line;
1975         }
1976   
1977         my @total_fill = ();
1978   
1979         my $taxtotal = 0;
1980         foreach my $tax ( $self->_items_tax ) {
1981           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1982           $taxtotal += $tax->{'amount'};
1983           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1984           push @total_fill,
1985             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1986                 @total_item;
1987         }
1988
1989         if ( $taxtotal ) {
1990           $invoice_data{'total_item'} = 'Sub-total';
1991           $invoice_data{'total_amount'} =
1992             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1993           unshift @total_fill,
1994             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1995                 @total_item;
1996         }
1997   
1998         $invoice_data{'total_item'} = '\textbf{Total}';
1999         $invoice_data{'total_amount'} =
2000           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2001         push @total_fill,
2002           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2003               @total_item;
2004   
2005         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2006   
2007         # credits
2008         foreach my $credit ( $self->_items_credits ) {
2009           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
2010           #$credittotal
2011           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
2012           push @total_fill, 
2013             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2014                 @total_item;
2015         }
2016   
2017         # payments
2018         foreach my $payment ( $self->_items_payments ) {
2019           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2020           #$paymenttotal
2021           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2022           push @total_fill, 
2023             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2024                 @total_item;
2025         }
2026   
2027         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2028         $invoice_data{'total_amount'} =
2029           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2030         push @total_fill,
2031           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2032               @total_item;
2033   
2034         push @filled_in, @total_fill;
2035   
2036       } else {
2037         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2038         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2039         push @filled_in, $line;
2040       }
2041   
2042     }
2043
2044     sub nounder {
2045       my $var = $1;
2046       $var =~ s/_/\-/g;
2047       $var;
2048     }
2049
2050   } elsif ( $format eq 'Text::Template' ) {
2051
2052     my @detail_items = ();
2053     my @total_items = ();
2054
2055     $invoice_data{'detail_items'} = \@detail_items;
2056     $invoice_data{'total_items'} = \@total_items;
2057   
2058     foreach my $line_item ( $self->_items ) {
2059       my $detail = {
2060         ext_description => [],
2061       };
2062       $detail->{'ref'} = $line_item->{'pkgnum'};
2063       $detail->{'quantity'} = 1;
2064       $detail->{'description'} = _latex_escape($line_item->{'description'});
2065       if ( exists $line_item->{'ext_description'} ) {
2066         @{$detail->{'ext_description'}} = map {
2067           _latex_escape($_);
2068         } @{$line_item->{'ext_description'}};
2069       }
2070       $detail->{'amount'} = $line_item->{'amount'};
2071       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2072   
2073       push @detail_items, $detail;
2074     }
2075   
2076   
2077     my $taxtotal = 0;
2078     foreach my $tax ( $self->_items_tax ) {
2079       my $total = {};
2080       $total->{'total_item'} = _latex_escape($tax->{'description'});
2081       $taxtotal += $tax->{'amount'};
2082       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2083       push @total_items, $total;
2084     }
2085   
2086     if ( $taxtotal ) {
2087       my $total = {};
2088       $total->{'total_item'} = 'Sub-total';
2089       $total->{'total_amount'} =
2090         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2091       unshift @total_items, $total;
2092     }
2093   
2094     {
2095       my $total = {};
2096       $total->{'total_item'} = '\textbf{Total}';
2097       $total->{'total_amount'} =
2098         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2099       push @total_items, $total;
2100     }
2101   
2102     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2103   
2104     # credits
2105     foreach my $credit ( $self->_items_credits ) {
2106       my $total;
2107       $total->{'total_item'} = _latex_escape($credit->{'description'});
2108       #$credittotal
2109       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2110       push @total_items, $total;
2111     }
2112   
2113     # payments
2114     foreach my $payment ( $self->_items_payments ) {
2115       my $total = {};
2116       $total->{'total_item'} = _latex_escape($payment->{'description'});
2117       #$paymenttotal
2118       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2119       push @total_items, $total;
2120     }
2121   
2122     { 
2123       my $total;
2124       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2125       $total->{'total_amount'} =
2126         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2127       push @total_items, $total;
2128     }
2129
2130   } else {
2131     die "guru meditation #54";
2132   }
2133
2134   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2135   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2136                            DIR      => $dir,
2137                            SUFFIX   => '.eps',
2138                            UNLINK   => 0,
2139                          ) or die "can't open temp file: $!\n";
2140
2141   if ($template && $conf->exists("logo_${template}.eps")) {
2142     print $lh $conf->config_binary("logo_${template}.eps")
2143       or die "can't write temp file: $!\n";
2144   }else{
2145     print $lh $conf->config_binary('logo.eps')
2146       or die "can't write temp file: $!\n";
2147   }
2148   close $lh;
2149   $invoice_data{'logo_file'} = $lh->filename;
2150
2151   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2152                            DIR      => $dir,
2153                            SUFFIX   => '.tex',
2154                            UNLINK   => 0,
2155                          ) or die "can't open temp file: $!\n";
2156   if ( $format eq 'old' ) {
2157     print $fh join('', @filled_in );
2158   } elsif ( $format eq 'Text::Template' ) {
2159     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2160   } else {
2161     die "guru meditation #32";
2162   }
2163   close $fh;
2164
2165   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2166   return ($1, $invoice_data{'logo_file'});
2167
2168 }
2169
2170 =item print_ps [ TIME [ , TEMPLATE ] ]
2171
2172 Returns an postscript invoice, as a scalar.
2173
2174 TIME an optional value used to control the printing of overdue messages.  The
2175 default is now.  It isn't the date of the invoice; that's the `_date' field.
2176 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2177 L<Time::Local> and L<Date::Parse> for conversion functions.
2178
2179 =cut
2180
2181 sub print_ps {
2182   my $self = shift;
2183
2184   my ($file, $lfile) = $self->print_latex(@_);
2185
2186   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2187   chdir($dir);
2188
2189   my $sfile = shell_quote $file;
2190
2191   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2192     or die "pslatex $file.tex failed; see $file.log for details?\n";
2193   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2194     or die "pslatex $file.tex failed; see $file.log for details?\n";
2195
2196   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2197     or die "dvips failed";
2198
2199   open(POSTSCRIPT, "<$file.ps")
2200     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2201
2202   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2203   unlink("$lfile");
2204
2205   my $ps = '';
2206   while (<POSTSCRIPT>) {
2207     $ps .= $_;
2208   }
2209
2210   close POSTSCRIPT;
2211
2212   return $ps;
2213
2214 }
2215
2216 =item print_pdf [ TIME [ , TEMPLATE ] ]
2217
2218 Returns an PDF invoice, as a scalar.
2219
2220 TIME an optional value used to control the printing of overdue messages.  The
2221 default is now.  It isn't the date of the invoice; that's the `_date' field.
2222 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2223 L<Time::Local> and L<Date::Parse> for conversion functions.
2224
2225 =cut
2226
2227 sub print_pdf {
2228   my $self = shift;
2229
2230   my ($file, $lfile) = $self->print_latex(@_);
2231
2232   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2233   chdir($dir);
2234
2235   #system('pdflatex', "$file.tex");
2236   #system('pdflatex', "$file.tex");
2237   #! LaTeX Error: Unknown graphics extension: .eps.
2238
2239   my $sfile = shell_quote $file;
2240
2241   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2242     or die "pslatex $file.tex failed; see $file.log for details?\n";
2243   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2244     or die "pslatex $file.tex failed; see $file.log for details?\n";
2245
2246   #system('dvipdf', "$file.dvi", "$file.pdf" );
2247   system(
2248     "dvips -q -t letter -f $sfile.dvi ".
2249     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2250     "     -c save pop -"
2251   ) == 0
2252     or die "dvips | gs failed: $!";
2253
2254   open(PDF, "<$file.pdf")
2255     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2256
2257   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2258   unlink("$lfile");
2259
2260   my $pdf = '';
2261   while (<PDF>) {
2262     $pdf .= $_;
2263   }
2264
2265   close PDF;
2266
2267   return $pdf;
2268
2269 }
2270
2271 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2272
2273 Returns an HTML invoice, as a scalar.
2274
2275 TIME an optional value used to control the printing of overdue messages.  The
2276 default is now.  It isn't the date of the invoice; that's the `_date' field.
2277 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2278 L<Time::Local> and L<Date::Parse> for conversion functions.
2279
2280 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2281 when emailing the invoice as part of a multipart/related MIME email.
2282
2283 =cut
2284
2285 #some falze laziness w/print_text and print_latex (and send_csv)
2286 sub print_html {
2287   my( $self, $today, $template, $cid ) = @_;
2288   $today ||= time;
2289
2290   my $cust_main = $self->cust_main;
2291   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2292     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2293
2294   $template ||= $self->_agent_template;
2295   my $templatefile = 'invoice_html';
2296   my $suffix = length($template) ? "_$template" : '';
2297   $templatefile .= $suffix;
2298   my @html_template = map "$_\n", $conf->config($templatefile)
2299     or die "cannot load config file $templatefile";
2300
2301   my $html_template = new Text::Template(
2302     TYPE   => 'ARRAY',
2303     SOURCE => \@html_template,
2304     DELIMITERS => [ '<%=', '%>' ],
2305   );
2306
2307   $html_template->compile()
2308     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2309
2310   my %invoice_data = (
2311     'custnum'      => $self->custnum,
2312     'invnum'       => $self->invnum,
2313     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2314     'today'        => time2str('%b %o, %Y', $today),
2315     'agent'        => encode_entities($cust_main->agent->agent),
2316     'payname'      => encode_entities($cust_main->payname),
2317     'company'      => encode_entities($cust_main->company),
2318     'address1'     => encode_entities($cust_main->address1),
2319     'address2'     => encode_entities($cust_main->address2),
2320     'city'         => encode_entities($cust_main->city),
2321     'state'        => encode_entities($cust_main->state),
2322     'zip'          => encode_entities($cust_main->zip),
2323     'terms'        => $self->terms,
2324     'cid'          => $cid,
2325     'template'     => $template,
2326 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2327   );
2328
2329   if (
2330          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2331       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2332   ) {
2333     $invoice_data{'returnaddress'} =
2334       join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2335   } else {
2336     $invoice_data{'returnaddress'} =
2337       join("\n", map { 
2338                        s/~/&nbsp;/g;
2339                        s/\\\\\*?\s*$/<BR>/;
2340                        s/\\hyphenation\{[\w\s\-]+\}//;
2341                        $_;
2342                      }
2343                      $conf->config_orbase( 'invoice_latexreturnaddress',
2344                                            $template
2345                                          )
2346           );
2347   }
2348
2349   my $countrydefault = $conf->config('countrydefault') || 'US';
2350   if ( $cust_main->country eq $countrydefault ) {
2351     $invoice_data{'country'} = '';
2352   } else {
2353     $invoice_data{'country'} =
2354       encode_entities(code2country($cust_main->country));
2355   }
2356
2357   if (
2358          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2359       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2360   ) {
2361     $invoice_data{'notes'} =
2362       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2363   } else {
2364     $invoice_data{'notes'} = 
2365       join("\n", map { 
2366                        s/%%(.*)$/<!-- $1 -->/;
2367                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2368                        s/\\begin\{enumerate\}/<ol>/;
2369                        s/\\item /  <li>/;
2370                        s/\\end\{enumerate\}/<\/ol>/;
2371                        s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2372                        s/\\\\\*/ /;
2373                        $_;
2374                      } 
2375                      $conf->config_orbase('invoice_latexnotes', $template)
2376           );
2377   }
2378
2379 #  #do variable substitutions in notes
2380 #  $invoice_data{'notes'} =
2381 #    join("\n",
2382 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2383 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2384 #    );
2385
2386   if (
2387          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2388       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2389   ) {
2390    $invoice_data{'footer'} =
2391      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2392   } else {
2393    $invoice_data{'footer'} =
2394        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2395                       $conf->config_orbase('invoice_latexfooter', $template)
2396            );
2397   }
2398
2399   $invoice_data{'po_line'} =
2400     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2401       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2402       : '';
2403
2404   my $money_char = $conf->config('money_char') || '$';
2405
2406   foreach my $line_item ( $self->_items ) {
2407     my $detail = {
2408       ext_description => [],
2409     };
2410     $detail->{'ref'} = $line_item->{'pkgnum'};
2411     $detail->{'description'} = encode_entities($line_item->{'description'});
2412     if ( exists $line_item->{'ext_description'} ) {
2413       @{$detail->{'ext_description'}} = map {
2414         encode_entities($_);
2415       } @{$line_item->{'ext_description'}};
2416     }
2417     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2418     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2419
2420     push @{$invoice_data{'detail_items'}}, $detail;
2421   }
2422
2423
2424   my $taxtotal = 0;
2425   foreach my $tax ( $self->_items_tax ) {
2426     my $total = {};
2427     $total->{'total_item'} = encode_entities($tax->{'description'});
2428     $taxtotal += $tax->{'amount'};
2429     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2430     push @{$invoice_data{'total_items'}}, $total;
2431   }
2432
2433   if ( $taxtotal ) {
2434     my $total = {};
2435     $total->{'total_item'} = 'Sub-total';
2436     $total->{'total_amount'} =
2437       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2438     unshift @{$invoice_data{'total_items'}}, $total;
2439   }
2440
2441   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2442   {
2443     my $total = {};
2444     $total->{'total_item'} = '<b>Total</b>';
2445     $total->{'total_amount'} =
2446       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2447     push @{$invoice_data{'total_items'}}, $total;
2448   }
2449
2450   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2451
2452   # credits
2453   foreach my $credit ( $self->_items_credits ) {
2454     my $total;
2455     $total->{'total_item'} = encode_entities($credit->{'description'});
2456     #$credittotal
2457     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2458     push @{$invoice_data{'total_items'}}, $total;
2459   }
2460
2461   # payments
2462   foreach my $payment ( $self->_items_payments ) {
2463     my $total = {};
2464     $total->{'total_item'} = encode_entities($payment->{'description'});
2465     #$paymenttotal
2466     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2467     push @{$invoice_data{'total_items'}}, $total;
2468   }
2469
2470   { 
2471     my $total;
2472     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2473     $total->{'total_amount'} =
2474       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2475     push @{$invoice_data{'total_items'}}, $total;
2476   }
2477
2478   $html_template->fill_in( HASH => \%invoice_data);
2479 }
2480
2481 # quick subroutine for print_latex
2482 #
2483 # There are ten characters that LaTeX treats as special characters, which
2484 # means that they do not simply typeset themselves: 
2485 #      # $ % & ~ _ ^ \ { }
2486 #
2487 # TeX ignores blanks following an escaped character; if you want a blank (as
2488 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2489
2490 sub _latex_escape {
2491   my $value = shift;
2492   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2493   $value =~ s/([<>])/\$$1\$/g;
2494   $value;
2495 }
2496
2497 #utility methods for print_*
2498
2499 sub terms {
2500   my $self = shift;
2501
2502   #check for an invoice- specific override (eventually)
2503   
2504   #check for a customer- specific override
2505   return $self->cust_main->invoice_terms
2506     if $self->cust_main->invoice_terms;
2507
2508   #use configured default or default default
2509   $conf->config('invoice_default_terms') || 'Payable upon receipt';
2510 }
2511
2512 sub due_date {
2513   my $self = shift;
2514   my $duedate = '';
2515   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2516     $duedate = $self->_date() + ( $1 * 86400 );
2517   }
2518   $duedate;
2519 }
2520
2521 sub due_date2str {
2522   my $self = shift;
2523   $self->due_date ? time2str(shift, $self->due_date) : '';
2524 }
2525
2526 sub balance_due_msg {
2527   my $self = shift;
2528   my $msg = 'Balance Due';
2529   return $msg unless $self->terms;
2530   if ( $self->due_date ) {
2531     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2532   } elsif ( $self->terms ) {
2533     $msg .= ' - '. $self->terms;
2534   }
2535   $msg;
2536 }
2537
2538 sub _items {
2539   my $self = shift;
2540   my @display = scalar(@_)
2541                 ? @_
2542                 : qw( _items_previous _items_pkg );
2543                 #: qw( _items_pkg );
2544                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2545   my @b = ();
2546   foreach my $display ( @display ) {
2547     push @b, $self->$display(@_);
2548   }
2549   @b;
2550 }
2551
2552 sub _items_previous {
2553   my $self = shift;
2554   my $cust_main = $self->cust_main;
2555   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2556   my @b = ();
2557   foreach ( @pr_cust_bill ) {
2558     push @b, {
2559       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2560                        ' ('. time2str('%x',$_->_date). ')',
2561       #'pkgpart'     => 'N/A',
2562       'pkgnum'      => 'N/A',
2563       'amount'      => sprintf("%.2f", $_->owed),
2564     };
2565   }
2566   @b;
2567
2568   #{
2569   #    'description'     => 'Previous Balance',
2570   #    #'pkgpart'         => 'N/A',
2571   #    'pkgnum'          => 'N/A',
2572   #    'amount'          => sprintf("%10.2f", $pr_total ),
2573   #    'ext_description' => [ map {
2574   #                                 "Invoice ". $_->invnum.
2575   #                                 " (". time2str("%x",$_->_date). ") ".
2576   #                                 sprintf("%10.2f", $_->owed)
2577   #                         } @pr_cust_bill ],
2578
2579   #};
2580 }
2581
2582 sub _items_pkg {
2583   my $self = shift;
2584   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2585   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2586 }
2587
2588 sub _items_tax {
2589   my $self = shift;
2590   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2591   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2592 }
2593
2594 sub _items_cust_bill_pkg {
2595   my $self = shift;
2596   my $cust_bill_pkg = shift;
2597
2598   my @b = ();
2599   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2600
2601     my $desc = $cust_bill_pkg->desc;
2602
2603     if ( $cust_bill_pkg->pkgnum > 0 ) {
2604
2605       if ( $cust_bill_pkg->setup != 0 ) {
2606         my $description = $desc;
2607         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2608         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2609         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2610         push @b, {
2611           description     => $description,
2612           #pkgpart         => $part_pkg->pkgpart,
2613           pkgnum          => $cust_bill_pkg->pkgnum,
2614           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2615           ext_description => \@d,
2616         };
2617       }
2618
2619       if ( $cust_bill_pkg->recur != 0 ) {
2620         push @b, {
2621           description     => "$desc (" .
2622                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2623                                time2str('%x', $cust_bill_pkg->edate). ')',
2624           #pkgpart         => $part_pkg->pkgpart,
2625           pkgnum          => $cust_bill_pkg->pkgnum,
2626           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2627           ext_description =>
2628             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2629                                                         $cust_bill_pkg->sdate),
2630               $cust_bill_pkg->details,
2631             ],
2632         };
2633       }
2634
2635     } else { #pkgnum tax or one-shot line item (??)
2636
2637       if ( $cust_bill_pkg->setup != 0 ) {
2638         push @b, {
2639           'description' => $desc,
2640           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2641         };
2642       }
2643       if ( $cust_bill_pkg->recur != 0 ) {
2644         push @b, {
2645           'description' => "$desc (".
2646                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2647                            time2str("%x", $cust_bill_pkg->edate). ')',
2648           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2649         };
2650       }
2651
2652     }
2653
2654   }
2655
2656   @b;
2657
2658 }
2659
2660 sub _items_credits {
2661   my $self = shift;
2662
2663   my @b;
2664   #credits
2665   foreach ( $self->cust_credited ) {
2666
2667     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2668
2669     my $reason = $_->cust_credit->reason;
2670     #my $reason = substr($_->cust_credit->reason,0,32);
2671     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2672     $reason = " ($reason) " if $reason;
2673     push @b, {
2674       #'description' => 'Credit ref\#'. $_->crednum.
2675       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2676       #                 $reason,
2677       'description' => 'Credit applied '.
2678                        time2str("%x",$_->cust_credit->_date). $reason,
2679       'amount'      => sprintf("%.2f",$_->amount),
2680     };
2681   }
2682   #foreach ( @cr_cust_credit ) {
2683   #  push @buf,[
2684   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2685   #    $money_char. sprintf("%10.2f",$_->credited)
2686   #  ];
2687   #}
2688
2689   @b;
2690
2691 }
2692
2693 sub _items_payments {
2694   my $self = shift;
2695
2696   my @b;
2697   #get & print payments
2698   foreach ( $self->cust_bill_pay ) {
2699
2700     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2701
2702     push @b, {
2703       'description' => "Payment received ".
2704                        time2str("%x",$_->cust_pay->_date ),
2705       'amount'      => sprintf("%.2f", $_->amount )
2706     };
2707   }
2708
2709   @b;
2710
2711 }
2712
2713
2714 =back
2715
2716 =head1 SUBROUTINES
2717
2718 =over 4
2719
2720 =item reprint
2721
2722 =cut
2723
2724 sub process_reprint {
2725   process_re_X('print', @_);
2726 }
2727
2728 =item reemail
2729
2730 =cut
2731
2732 sub process_reemail {
2733   process_re_X('email', @_);
2734 }
2735
2736 =item refax
2737
2738 =cut
2739
2740 sub process_refax {
2741   process_re_X('fax', @_);
2742 }
2743
2744 use Storable qw(thaw);
2745 use Data::Dumper;
2746 use MIME::Base64;
2747 sub process_re_X {
2748   my( $method, $job ) = ( shift, shift );
2749   warn "process_re_X $method for job $job\n" if $DEBUG;
2750
2751   my $param = thaw(decode_base64(shift));
2752   warn Dumper($param) if $DEBUG;
2753
2754   re_X(
2755     $method,
2756     $job,
2757     %$param,
2758   );
2759
2760 }
2761
2762 sub re_X {
2763   my($method, $job, %param ) = @_;
2764 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2765   if ( $DEBUG ) {
2766     warn "re_X $method for job $job with param:\n".
2767          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2768   }
2769
2770   #some false laziness w/search/cust_bill.html
2771   my $distinct = '';
2772   my $orderby = 'ORDER BY cust_bill._date';
2773
2774   my @where;
2775
2776   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2777     push @where, "cust_bill._date >= $1";
2778   }
2779   if ( $param{'end'} =~ /^(\d+)$/ ) {
2780     push @where, "cust_bill._date < $1";
2781   }
2782   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2783     push @where, "cust_main.agentnum = $1";
2784   }
2785
2786   my $owed =
2787     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2788                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2789              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2790                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2791
2792   push @where, "0 != $owed"
2793     if $param{'open'};
2794
2795   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2796     if $param{'days'};
2797
2798   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2799
2800   my $addl_from = 'left join cust_main using ( custnum )';
2801
2802   if ( $param{'newest_percust'} ) {
2803     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2804     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2805     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2806   }
2807      
2808   my @cust_bill = qsearch( 'cust_bill',
2809                            {},
2810                            "$distinct cust_bill.*",
2811                            $extra_sql,
2812                            '',
2813                            $addl_from
2814                          );
2815
2816   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2817   foreach my $cust_bill ( @cust_bill ) {
2818     $cust_bill->$method();
2819
2820     if ( $job ) { #progressbar foo
2821       $num++;
2822       if ( time - $min_sec > $last ) {
2823         my $error = $job->update_statustext(
2824           int( 100 * $num / scalar(@cust_bill) )
2825         );
2826         die $error if $error;
2827         $last = time;
2828       }
2829     }
2830
2831   }
2832
2833 }
2834
2835 =back
2836
2837 =head1 BUGS
2838
2839 The delete method.
2840
2841 print_text formatting (and some logic :/) is in source, but needs to be
2842 slurped in from a file.  Also number of lines ($=).
2843
2844 =head1 SEE ALSO
2845
2846 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2847 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2848 documentation.
2849
2850 =cut
2851
2852 1;
2853