config goes in database
[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 = '';
1197     if (    $conf->exists('invoice_default_terms') 
1198          && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1199       $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1200     }
1201
1202     my( $previous_balance, @unused ) = $self->previous; #previous balance
1203
1204     my $pmt_cr_applied = 0;
1205     $pmt_cr_applied += $_->{'amount'}
1206       foreach ( $self->_items_payments, $self->_items_credits ) ;
1207
1208     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1209
1210     $csv->combine(
1211       '',                         #  1 | N/A-Leave Empty               CHAR   2
1212       '',                         #  2 | N/A-Leave Empty               CHAR  15
1213       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1214       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1215       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1216       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1217       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1218       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1219       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1220       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1221       '',                         # 10 | Ancillary Billing Information CHAR  30
1222       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1223       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1224
1225       # XXX ?
1226       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1227
1228       # XXX ?
1229       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1230
1231       $previous_balance,          # 15 | Previous Balance              NUM*   9
1232       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1233       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1234       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1235       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1236       '',                         # 20 | 30 Day Aging                  NUM*   9
1237       '',                         # 21 | 60 Day Aging                  NUM*   9
1238       '',                         # 22 | 90 Day Aging                  NUM*   9
1239       'N',                        # 23 | Y/N                           CHAR   1
1240       '',                         # 24 | Remittance automation         CHAR 100
1241       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1242       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1243       '0',                        # 27 | Federal Tax***                NUM*   9
1244       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1245       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1246     );
1247
1248   } else {
1249   
1250     $csv->combine(
1251       'cust_bill',
1252       $self->invnum,
1253       $self->custnum,
1254       time2str("%x", $self->_date),
1255       sprintf("%.2f", $self->charged),
1256       ( map { $cust_main->getfield($_) }
1257           qw( first last company address1 address2 city state zip country ) ),
1258       map { '' } (1..5),
1259     ) or die "can't create csv";
1260   }
1261
1262   my $header = $csv->string. "\n";
1263
1264   my $detail = '';
1265   if ( lc($opt{'format'}) eq 'billco' ) {
1266
1267     my $lineseq = 0;
1268     foreach my $item ( $self->_items_pkg ) {
1269
1270       $csv->combine(
1271         '',                     #  1 | N/A-Leave Empty            CHAR   2
1272         '',                     #  2 | N/A-Leave Empty            CHAR  15
1273         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1274         $self->invnum,          #  4 | Invoice Number             CHAR  15
1275         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1276         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1277         $item->{'amount'},      #  7 | Amount                     NUM*   9
1278         '',                     #  8 | Line Format Control**      CHAR   2
1279         '',                     #  9 | Grouping Code              CHAR   2
1280         '',                     # 10 | User Defined               CHAR  15
1281       );
1282
1283       $detail .= $csv->string. "\n";
1284
1285     }
1286
1287   } else {
1288
1289     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1290
1291       my($pkg, $setup, $recur, $sdate, $edate);
1292       if ( $cust_bill_pkg->pkgnum ) {
1293       
1294         ($pkg, $setup, $recur, $sdate, $edate) = (
1295           $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1296           ( $cust_bill_pkg->setup != 0
1297             ? sprintf("%.2f", $cust_bill_pkg->setup )
1298             : '' ),
1299           ( $cust_bill_pkg->recur != 0
1300             ? sprintf("%.2f", $cust_bill_pkg->recur )
1301             : '' ),
1302           ( $cust_bill_pkg->sdate 
1303             ? time2str("%x", $cust_bill_pkg->sdate)
1304             : '' ),
1305           ($cust_bill_pkg->edate 
1306             ?time2str("%x", $cust_bill_pkg->edate)
1307             : '' ),
1308         );
1309   
1310       } else { #pkgnum tax
1311         next unless $cust_bill_pkg->setup != 0;
1312         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1313                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1314                          : 'Tax';
1315         ($pkg, $setup, $recur, $sdate, $edate) =
1316           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1317       }
1318   
1319       $csv->combine(
1320         'cust_bill_pkg',
1321         $self->invnum,
1322         ( map { '' } (1..11) ),
1323         ($pkg, $setup, $recur, $sdate, $edate)
1324       ) or die "can't create csv";
1325
1326       $detail .= $csv->string. "\n";
1327
1328     }
1329
1330   }
1331
1332   ( $header, $detail );
1333
1334 }
1335
1336 =item comp
1337
1338 Pays this invoice with a compliemntary payment.  If there is an error,
1339 returns the error, otherwise returns false.
1340
1341 =cut
1342
1343 sub comp {
1344   my $self = shift;
1345   my $cust_pay = new FS::cust_pay ( {
1346     'invnum'   => $self->invnum,
1347     'paid'     => $self->owed,
1348     '_date'    => '',
1349     'payby'    => 'COMP',
1350     'payinfo'  => $self->cust_main->payinfo,
1351     'paybatch' => '',
1352   } );
1353   $cust_pay->insert;
1354 }
1355
1356 =item realtime_card
1357
1358 Attempts to pay this invoice with a credit card payment via a
1359 Business::OnlinePayment realtime gateway.  See
1360 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1361 for supported processors.
1362
1363 =cut
1364
1365 sub realtime_card {
1366   my $self = shift;
1367   $self->realtime_bop( 'CC', @_ );
1368 }
1369
1370 =item realtime_ach
1371
1372 Attempts to pay this invoice with an electronic check (ACH) payment via a
1373 Business::OnlinePayment realtime gateway.  See
1374 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1375 for supported processors.
1376
1377 =cut
1378
1379 sub realtime_ach {
1380   my $self = shift;
1381   $self->realtime_bop( 'ECHECK', @_ );
1382 }
1383
1384 =item realtime_lec
1385
1386 Attempts to pay this invoice with phone bill (LEC) payment via a
1387 Business::OnlinePayment realtime gateway.  See
1388 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1389 for supported processors.
1390
1391 =cut
1392
1393 sub realtime_lec {
1394   my $self = shift;
1395   $self->realtime_bop( 'LEC', @_ );
1396 }
1397
1398 sub realtime_bop {
1399   my( $self, $method ) = @_;
1400
1401   my $cust_main = $self->cust_main;
1402   my $balance = $cust_main->balance;
1403   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1404   $amount = sprintf("%.2f", $amount);
1405   return "not run (balance $balance)" unless $amount > 0;
1406
1407   my $description = 'Internet Services';
1408   if ( $conf->exists('business-onlinepayment-description') ) {
1409     my $dtempl = $conf->config('business-onlinepayment-description');
1410
1411     my $agent_obj = $cust_main->agent
1412       or die "can't retreive agent for $cust_main (agentnum ".
1413              $cust_main->agentnum. ")";
1414     my $agent = $agent_obj->agent;
1415     my $pkgs = join(', ',
1416       map { $_->cust_pkg->part_pkg->pkg }
1417         grep { $_->pkgnum } $self->cust_bill_pkg
1418     );
1419     $description = eval qq("$dtempl");
1420   }
1421
1422   $cust_main->realtime_bop($method, $amount,
1423     'description' => $description,
1424     'invnum'      => $self->invnum,
1425   );
1426
1427 }
1428
1429 =item batch_card OPTION => VALUE...
1430
1431 Adds a payment for this invoice to the pending credit card batch (see
1432 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1433 runs the payment using a realtime gateway.
1434
1435 =cut
1436
1437 sub batch_card {
1438   my ($self, %options) = @_;
1439   my $cust_main = $self->cust_main;
1440
1441   my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1442   return '' unless $amount > 0;
1443   
1444   if ($options{'realtime'}) {
1445     return $cust_main->realtime_bop( FS::payby->payby2bop($cust_main->payby),
1446                                      $amount,
1447                                      %options,
1448                                    );
1449   }
1450
1451   my $oldAutoCommit = $FS::UID::AutoCommit;
1452   local $FS::UID::AutoCommit = 0;
1453   my $dbh = dbh;
1454
1455   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
1456     or return "Cannot lock pay_batch: " . $dbh->errstr;
1457
1458   my %pay_batch = (
1459     'status' => 'O',
1460     'payby'  => FS::payby->payby2payment($cust_main->payby),
1461   );
1462
1463   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
1464
1465   unless ( $pay_batch ) {
1466     $pay_batch = new FS::pay_batch \%pay_batch;
1467     my $error = $pay_batch->insert;
1468     if ( $error ) {
1469       $dbh->rollback if $oldAutoCommit;
1470       die "error creating new batch: $error\n";
1471     }
1472   }
1473
1474   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1475       'batchnum' => $pay_batch->batchnum,
1476       'custnum'  => $cust_main->custnum,
1477   } );
1478
1479   my $cust_pay_batch = new FS::cust_pay_batch ( {
1480     'batchnum' => $pay_batch->batchnum,
1481     'invnum'   => $self->getfield('invnum'),       # is there a better value?
1482                                                    # this field should be
1483                                                    # removed...
1484                                                    # cust_bill_pay_batch now
1485     'custnum'  => $cust_main->custnum,
1486     'last'     => $cust_main->getfield('last'),
1487     'first'    => $cust_main->getfield('first'),
1488     'address1' => $cust_main->address1,
1489     'address2' => $cust_main->address2,
1490     'city'     => $cust_main->city,
1491     'state'    => $cust_main->state,
1492     'zip'      => $cust_main->zip,
1493     'country'  => $cust_main->country,
1494     'payby'    => $cust_main->payby,
1495     'payinfo'  => $cust_main->payinfo,
1496     'exp'      => $cust_main->paydate,
1497     'payname'  => $cust_main->payname,
1498     'amount'   => $amount,                          # consolidating
1499   } );
1500   
1501   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1502     if $old_cust_pay_batch;
1503
1504   my $error;
1505   if ($old_cust_pay_batch) {
1506     $error = $cust_pay_batch->replace($old_cust_pay_batch)
1507   } else {
1508     $error = $cust_pay_batch->insert;
1509   }
1510
1511   if ( $error ) {
1512     $dbh->rollback if $oldAutoCommit;
1513     die $error;
1514   }
1515
1516   my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments;
1517   foreach my $cust_bill ($cust_main->open_cust_bill) {
1518     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
1519     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
1520       'invnum' => $cust_bill->invnum,
1521       'paybatchnum' => $cust_pay_batch->paybatchnum,
1522       'amount' => $cust_bill->owed,
1523       '_date' => time,
1524     };
1525     if ($unapplied >= $cust_bill_pay_batch->amount){
1526       $unapplied -= $cust_bill_pay_batch->amount;
1527       next;
1528     }else{
1529       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
1530                                    $cust_bill_pay_batch->amount - $unapplied ));
1531       $unapplied = 0;
1532     }
1533     $error = $cust_bill_pay_batch->insert;
1534     if ( $error ) {
1535       $dbh->rollback if $oldAutoCommit;
1536       die $error;
1537     }
1538   }
1539
1540   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1541   '';
1542 }
1543
1544 sub _agent_template {
1545   my $self = shift;
1546   $self->_agent_plandata('agent_templatename');
1547 }
1548
1549 sub _agent_invoice_from {
1550   my $self = shift;
1551   $self->_agent_plandata('agent_invoice_from');
1552 }
1553
1554 sub _agent_plandata {
1555   my( $self, $option ) = @_;
1556
1557   my $part_bill_event = qsearchs( 'part_bill_event',
1558     {
1559       'payby'     => $self->cust_main->payby,
1560       'plan'      => 'send_agent',
1561       'plandata'  => { 'op'    => '~',
1562                        'value' => "(^|\n)agentnum ".
1563                                    '([0-9]*, )*'.
1564                                   $self->cust_main->agentnum.
1565                                    '(, [0-9]*)*'.
1566                                   "(\n|\$)",
1567                      },
1568     },
1569     '',
1570     'ORDER BY seconds LIMIT 1'
1571   );
1572
1573   return '' unless $part_bill_event;
1574
1575   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1576     return $1;
1577   } else {
1578     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1579          " plandata for $option";
1580     return '';
1581   }
1582
1583 }
1584
1585 =item print_text [ TIME [ , TEMPLATE ] ]
1586
1587 Returns an text invoice, as a list of lines.
1588
1589 TIME an optional value used to control the printing of overdue messages.  The
1590 default is now.  It isn't the date of the invoice; that's the `_date' field.
1591 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1592 L<Time::Local> and L<Date::Parse> for conversion functions.
1593
1594 =cut
1595
1596 #still some false laziness w/_items stuff (and send_csv)
1597 sub print_text {
1598
1599   my( $self, $today, $template ) = @_;
1600   $today ||= time;
1601
1602 #  my $invnum = $self->invnum;
1603   my $cust_main = $self->cust_main;
1604   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1605     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1606
1607   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1608 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1609   #my $balance_due = $self->owed + $pr_total - $cr_total;
1610   my $balance_due = $self->owed + $pr_total;
1611
1612   #my @collect = ();
1613   #my($description,$amount);
1614   @buf = ();
1615
1616   #previous balance
1617   foreach ( @pr_cust_bill ) {
1618     push @buf, [
1619       "Previous Balance, Invoice #". $_->invnum. 
1620                  " (". time2str("%x",$_->_date). ")",
1621       $money_char. sprintf("%10.2f",$_->owed)
1622     ];
1623   }
1624   if (@pr_cust_bill) {
1625     push @buf,['','-----------'];
1626     push @buf,[ 'Total Previous Balance',
1627                 $money_char. sprintf("%10.2f",$pr_total ) ];
1628     push @buf,['',''];
1629   }
1630
1631   #new charges
1632   foreach my $cust_bill_pkg (
1633     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1634     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1635   ) {
1636
1637     my $desc = $cust_bill_pkg->desc;
1638
1639     if ( $cust_bill_pkg->pkgnum > 0 ) {
1640
1641       if ( $cust_bill_pkg->setup != 0 ) {
1642         my $description = $desc;
1643         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1644         push @buf, [ $description,
1645                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1646         push @buf,
1647           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1648               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1649       }
1650
1651       if ( $cust_bill_pkg->recur != 0 ) {
1652         push @buf, [
1653           "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1654                       time2str("%x", $cust_bill_pkg->edate) . ")",
1655           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1656         ];
1657         push @buf,
1658           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1659               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1660                                                   $cust_bill_pkg->sdate );
1661       }
1662
1663       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1664
1665     } else { #pkgnum tax or one-shot line item
1666
1667       if ( $cust_bill_pkg->setup != 0 ) {
1668         push @buf, [ $desc,
1669                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1670       }
1671       if ( $cust_bill_pkg->recur != 0 ) {
1672         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1673                               . time2str("%x", $cust_bill_pkg->edate). ")",
1674                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1675                    ];
1676       }
1677
1678     }
1679
1680   }
1681
1682   push @buf,['','-----------'];
1683   push @buf,['Total New Charges',
1684              $money_char. sprintf("%10.2f",$self->charged) ];
1685   push @buf,['',''];
1686
1687   push @buf,['','-----------'];
1688   push @buf,['Total Charges',
1689              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1690   push @buf,['',''];
1691
1692   #credits
1693   foreach ( $self->cust_credited ) {
1694
1695     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1696
1697     my $reason = substr($_->cust_credit->reason,0,32);
1698     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1699     $reason = " ($reason) " if $reason;
1700     push @buf,[
1701       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1702         $reason,
1703       $money_char. sprintf("%10.2f",$_->amount)
1704     ];
1705   }
1706   #foreach ( @cr_cust_credit ) {
1707   #  push @buf,[
1708   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1709   #    $money_char. sprintf("%10.2f",$_->credited)
1710   #  ];
1711   #}
1712
1713   #get & print payments
1714   foreach ( $self->cust_bill_pay ) {
1715
1716     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1717
1718     push @buf,[
1719       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1720       $money_char. sprintf("%10.2f",$_->amount )
1721     ];
1722   }
1723
1724   #balance due
1725   my $balance_due_msg = $self->balance_due_msg;
1726
1727   push @buf,['','-----------'];
1728   push @buf,[$balance_due_msg, $money_char. 
1729     sprintf("%10.2f", $balance_due ) ];
1730
1731   #create the template
1732   $template ||= $self->_agent_template;
1733   my $templatefile = 'invoice_template';
1734   $templatefile .= "_$template" if length($template);
1735   my @invoice_template = $conf->config($templatefile)
1736     or die "cannot load config file $templatefile";
1737   $invoice_lines = 0;
1738   my $wasfunc = 0;
1739   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1740     /invoice_lines\((\d*)\)/;
1741     $invoice_lines += $1 || scalar(@buf);
1742     $wasfunc=1;
1743   }
1744   die "no invoice_lines() functions in template?" unless $wasfunc;
1745   my $invoice_template = new Text::Template (
1746     TYPE   => 'ARRAY',
1747     SOURCE => [ map "$_\n", @invoice_template ],
1748   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1749   $invoice_template->compile()
1750     or die "can't compile template: $Text::Template::ERROR";
1751
1752   #setup template variables
1753   package FS::cust_bill::_template; #!
1754   use vars qw( $custnum $invnum $date $agent @address $overdue
1755                $page $total_pages @buf );
1756
1757   $custnum = $self->custnum;
1758   $invnum = $self->invnum;
1759   $date = $self->_date;
1760   $agent = $self->cust_main->agent->agent;
1761   $page = 1;
1762
1763   if ( $FS::cust_bill::invoice_lines ) {
1764     $total_pages =
1765       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1766     $total_pages++
1767       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1768   } else {
1769     $total_pages = 1;
1770   }
1771
1772   #format address (variable for the template)
1773   my $l = 0;
1774   @address = ( '', '', '', '', '', '' );
1775   package FS::cust_bill; #!
1776   $FS::cust_bill::_template::address[$l++] =
1777     $cust_main->payname.
1778       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1779         ? " (P.O. #". $cust_main->payinfo. ")"
1780         : ''
1781       )
1782   ;
1783   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1784     if $cust_main->company;
1785   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1786   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1787     if $cust_main->address2;
1788   $FS::cust_bill::_template::address[$l++] =
1789     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1790
1791   my $countrydefault = $conf->config('countrydefault') || 'US';
1792   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1793     unless $cust_main->country eq $countrydefault;
1794
1795         #  #overdue? (variable for the template)
1796         #  $FS::cust_bill::_template::overdue = ( 
1797         #    $balance_due > 0
1798         #    && $today > $self->_date 
1799         ##    && $self->printed > 1
1800         #    && $self->printed > 0
1801         #  );
1802
1803   #and subroutine for the template
1804   sub FS::cust_bill::_template::invoice_lines {
1805     my $lines = shift || scalar(@buf);
1806     map { 
1807       scalar(@buf) ? shift @buf : [ '', '' ];
1808     }
1809     ( 1 .. $lines );
1810   }
1811
1812   #and fill it in
1813   $FS::cust_bill::_template::page = 1;
1814   my $lines;
1815   my @collect;
1816   while (@buf) {
1817     push @collect, split("\n",
1818       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1819     );
1820     $FS::cust_bill::_template::page++;
1821   }
1822
1823   map "$_\n", @collect;
1824
1825 }
1826
1827 =item print_latex [ TIME [ , TEMPLATE ] ]
1828
1829 Internal method - returns a filename of a filled-in LaTeX template for this
1830 invoice (Note: add ".tex" to get the actual filename), and a filename of
1831 an associated logo (with the .eps extension included).
1832
1833 See print_ps and print_pdf for methods that return PostScript and PDF output.
1834
1835 TIME an optional value used to control the printing of overdue messages.  The
1836 default is now.  It isn't the date of the invoice; that's the `_date' field.
1837 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1838 L<Time::Local> and L<Date::Parse> for conversion functions.
1839
1840 =cut
1841
1842 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1843 sub print_latex {
1844
1845   my( $self, $today, $template ) = @_;
1846   $today ||= time;
1847   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1848     if $DEBUG;
1849
1850   my $cust_main = $self->cust_main;
1851   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1852     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1853
1854   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1855 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1856   #my $balance_due = $self->owed + $pr_total - $cr_total;
1857   my $balance_due = $self->owed + $pr_total;
1858
1859   #create the template
1860   $template ||= $self->_agent_template;
1861   my $templatefile = 'invoice_latex';
1862   my $suffix = length($template) ? "_$template" : '';
1863   $templatefile .= $suffix;
1864   my @invoice_template = map "$_\n", $conf->config($templatefile)
1865     or die "cannot load config file $templatefile";
1866
1867   my($format, $text_template);
1868   if ( grep { /^%%Detail/ } @invoice_template ) {
1869     #change this to a die when the old code is removed
1870     warn "old-style invoice template $templatefile; ".
1871          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1872     $format = 'old';
1873   } else {
1874     $format = 'Text::Template';
1875     $text_template = new Text::Template(
1876       TYPE => 'ARRAY',
1877       SOURCE => \@invoice_template,
1878       DELIMITERS => [ '[@--', '--@]' ],
1879     );
1880
1881     $text_template->compile()
1882       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1883   }
1884
1885   my $returnaddress;
1886   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1887     $returnaddress = join("\n",
1888       $conf->config_orbase('invoice_latexreturnaddress', $template)
1889     );
1890   } else {
1891     $returnaddress = '~';
1892   }
1893
1894   my %invoice_data = (
1895     'custnum'      => $self->custnum,
1896     'invnum'       => $self->invnum,
1897     'date'         => time2str('%b %o, %Y', $self->_date),
1898     'today'        => time2str('%b %o, %Y', $today),
1899     'agent'        => _latex_escape($cust_main->agent->agent),
1900     'payname'      => _latex_escape($cust_main->payname),
1901     'company'      => _latex_escape($cust_main->company),
1902     'address1'     => _latex_escape($cust_main->address1),
1903     'address2'     => _latex_escape($cust_main->address2),
1904     'city'         => _latex_escape($cust_main->city),
1905     'state'        => _latex_escape($cust_main->state),
1906     'zip'          => _latex_escape($cust_main->zip),
1907     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1908     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1909     'returnaddress' => $returnaddress,
1910     'quantity'     => 1,
1911     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1912     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1913     # better hang on to conf_dir for a while
1914     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1915   );
1916
1917   my $countrydefault = $conf->config('countrydefault') || 'US';
1918   if ( $cust_main->country eq $countrydefault ) {
1919     $invoice_data{'country'} = '';
1920   } else {
1921     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1922   }
1923
1924   $invoice_data{'notes'} =
1925     join("\n",
1926 #  #do variable substitutions in notes
1927 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1928         $conf->config_orbase('invoice_latexnotes', $template)
1929     );
1930   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1931     if $DEBUG;
1932
1933   $invoice_data{'footer'} =~ s/\n+$//;
1934   $invoice_data{'smallfooter'} =~ s/\n+$//;
1935   $invoice_data{'notes'} =~ s/\n+$//;
1936
1937   $invoice_data{'po_line'} =
1938     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1939       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1940       : '~';
1941
1942   my @filled_in = ();
1943   if ( $format eq 'old' ) {
1944   
1945     my @line_item = ();
1946     my @total_item = ();
1947     while ( @invoice_template ) {
1948       my $line = shift @invoice_template;
1949   
1950       if ( $line =~ /^%%Detail\s*$/ ) {
1951   
1952         while ( ( my $line_item_line = shift @invoice_template )
1953                 !~ /^%%EndDetail\s*$/                            ) {
1954           push @line_item, $line_item_line;
1955         }
1956         foreach my $line_item ( $self->_items ) {
1957         #foreach my $line_item ( $self->_items_pkg ) {
1958           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1959           $invoice_data{'description'} =
1960             _latex_escape($line_item->{'description'});
1961           if ( exists $line_item->{'ext_description'} ) {
1962             $invoice_data{'description'} .=
1963               "\\tabularnewline\n~~".
1964               join( "\\tabularnewline\n~~",
1965                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1966                   );
1967           }
1968           $invoice_data{'amount'} = $line_item->{'amount'};
1969           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1970           push @filled_in,
1971             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1972         }
1973   
1974       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1975   
1976         while ( ( my $total_item_line = shift @invoice_template )
1977                 !~ /^%%EndTotalDetails\s*$/                      ) {
1978           push @total_item, $total_item_line;
1979         }
1980   
1981         my @total_fill = ();
1982   
1983         my $taxtotal = 0;
1984         foreach my $tax ( $self->_items_tax ) {
1985           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1986           $taxtotal += $tax->{'amount'};
1987           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1988           push @total_fill,
1989             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1990                 @total_item;
1991         }
1992
1993         if ( $taxtotal ) {
1994           $invoice_data{'total_item'} = 'Sub-total';
1995           $invoice_data{'total_amount'} =
1996             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1997           unshift @total_fill,
1998             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1999                 @total_item;
2000         }
2001   
2002         $invoice_data{'total_item'} = '\textbf{Total}';
2003         $invoice_data{'total_amount'} =
2004           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2005         push @total_fill,
2006           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2007               @total_item;
2008   
2009         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2010   
2011         # credits
2012         foreach my $credit ( $self->_items_credits ) {
2013           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
2014           #$credittotal
2015           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
2016           push @total_fill, 
2017             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2018                 @total_item;
2019         }
2020   
2021         # payments
2022         foreach my $payment ( $self->_items_payments ) {
2023           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2024           #$paymenttotal
2025           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2026           push @total_fill, 
2027             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2028                 @total_item;
2029         }
2030   
2031         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2032         $invoice_data{'total_amount'} =
2033           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2034         push @total_fill,
2035           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2036               @total_item;
2037   
2038         push @filled_in, @total_fill;
2039   
2040       } else {
2041         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2042         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2043         push @filled_in, $line;
2044       }
2045   
2046     }
2047
2048     sub nounder {
2049       my $var = $1;
2050       $var =~ s/_/\-/g;
2051       $var;
2052     }
2053
2054   } elsif ( $format eq 'Text::Template' ) {
2055
2056     my @detail_items = ();
2057     my @total_items = ();
2058
2059     $invoice_data{'detail_items'} = \@detail_items;
2060     $invoice_data{'total_items'} = \@total_items;
2061   
2062     foreach my $line_item ( $self->_items ) {
2063       my $detail = {
2064         ext_description => [],
2065       };
2066       $detail->{'ref'} = $line_item->{'pkgnum'};
2067       $detail->{'quantity'} = 1;
2068       $detail->{'description'} = _latex_escape($line_item->{'description'});
2069       if ( exists $line_item->{'ext_description'} ) {
2070         @{$detail->{'ext_description'}} = map {
2071           _latex_escape($_);
2072         } @{$line_item->{'ext_description'}};
2073       }
2074       $detail->{'amount'} = $line_item->{'amount'};
2075       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2076   
2077       push @detail_items, $detail;
2078     }
2079   
2080   
2081     my $taxtotal = 0;
2082     foreach my $tax ( $self->_items_tax ) {
2083       my $total = {};
2084       $total->{'total_item'} = _latex_escape($tax->{'description'});
2085       $taxtotal += $tax->{'amount'};
2086       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2087       push @total_items, $total;
2088     }
2089   
2090     if ( $taxtotal ) {
2091       my $total = {};
2092       $total->{'total_item'} = 'Sub-total';
2093       $total->{'total_amount'} =
2094         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2095       unshift @total_items, $total;
2096     }
2097   
2098     {
2099       my $total = {};
2100       $total->{'total_item'} = '\textbf{Total}';
2101       $total->{'total_amount'} =
2102         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2103       push @total_items, $total;
2104     }
2105   
2106     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2107   
2108     # credits
2109     foreach my $credit ( $self->_items_credits ) {
2110       my $total;
2111       $total->{'total_item'} = _latex_escape($credit->{'description'});
2112       #$credittotal
2113       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2114       push @total_items, $total;
2115     }
2116   
2117     # payments
2118     foreach my $payment ( $self->_items_payments ) {
2119       my $total = {};
2120       $total->{'total_item'} = _latex_escape($payment->{'description'});
2121       #$paymenttotal
2122       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2123       push @total_items, $total;
2124     }
2125   
2126     { 
2127       my $total;
2128       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2129       $total->{'total_amount'} =
2130         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2131       push @total_items, $total;
2132     }
2133
2134   } else {
2135     die "guru meditation #54";
2136   }
2137
2138   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2139   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2140                            DIR      => $dir,
2141                            SUFFIX   => '.eps',
2142                            UNLINK   => 0,
2143                          ) or die "can't open temp file: $!\n";
2144
2145   if ($template && $conf->exists("logo_${template}.eps")) {
2146     print $lh $conf->config_binary("logo_${template}.eps")
2147       or die "can't write temp file: $!\n";
2148   }else{
2149     print $lh $conf->config_binary('logo.eps')
2150       or die "can't write temp file: $!\n";
2151   }
2152   close $lh;
2153   $invoice_data{'logo_file'} = $lh->filename;
2154
2155   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2156                            DIR      => $dir,
2157                            SUFFIX   => '.tex',
2158                            UNLINK   => 0,
2159                          ) or die "can't open temp file: $!\n";
2160   if ( $format eq 'old' ) {
2161     print $fh join('', @filled_in );
2162   } elsif ( $format eq 'Text::Template' ) {
2163     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2164   } else {
2165     die "guru meditation #32";
2166   }
2167   close $fh;
2168
2169   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2170   return ($1, $invoice_data{'logo_file'});
2171
2172 }
2173
2174 =item print_ps [ TIME [ , TEMPLATE ] ]
2175
2176 Returns an postscript invoice, as a scalar.
2177
2178 TIME an optional value used to control the printing of overdue messages.  The
2179 default is now.  It isn't the date of the invoice; that's the `_date' field.
2180 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2181 L<Time::Local> and L<Date::Parse> for conversion functions.
2182
2183 =cut
2184
2185 sub print_ps {
2186   my $self = shift;
2187
2188   my ($file, $lfile) = $self->print_latex(@_);
2189
2190   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2191   chdir($dir);
2192
2193   my $sfile = shell_quote $file;
2194
2195   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2196     or die "pslatex $file.tex failed; see $file.log for details?\n";
2197   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2198     or die "pslatex $file.tex failed; see $file.log for details?\n";
2199
2200   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2201     or die "dvips failed";
2202
2203   open(POSTSCRIPT, "<$file.ps")
2204     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2205
2206   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2207   unlink("$lfile");
2208
2209   my $ps = '';
2210   while (<POSTSCRIPT>) {
2211     $ps .= $_;
2212   }
2213
2214   close POSTSCRIPT;
2215
2216   return $ps;
2217
2218 }
2219
2220 =item print_pdf [ TIME [ , TEMPLATE ] ]
2221
2222 Returns an PDF invoice, as a scalar.
2223
2224 TIME an optional value used to control the printing of overdue messages.  The
2225 default is now.  It isn't the date of the invoice; that's the `_date' field.
2226 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2227 L<Time::Local> and L<Date::Parse> for conversion functions.
2228
2229 =cut
2230
2231 sub print_pdf {
2232   my $self = shift;
2233
2234   my ($file, $lfile) = $self->print_latex(@_);
2235
2236   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2237   chdir($dir);
2238
2239   #system('pdflatex', "$file.tex");
2240   #system('pdflatex', "$file.tex");
2241   #! LaTeX Error: Unknown graphics extension: .eps.
2242
2243   my $sfile = shell_quote $file;
2244
2245   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2246     or die "pslatex $file.tex failed; see $file.log for details?\n";
2247   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2248     or die "pslatex $file.tex failed; see $file.log for details?\n";
2249
2250   #system('dvipdf', "$file.dvi", "$file.pdf" );
2251   system(
2252     "dvips -q -t letter -f $sfile.dvi ".
2253     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2254     "     -c save pop -"
2255   ) == 0
2256     or die "dvips | gs failed: $!";
2257
2258   open(PDF, "<$file.pdf")
2259     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2260
2261   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2262   unlink("$lfile");
2263
2264   my $pdf = '';
2265   while (<PDF>) {
2266     $pdf .= $_;
2267   }
2268
2269   close PDF;
2270
2271   return $pdf;
2272
2273 }
2274
2275 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2276
2277 Returns an HTML invoice, as a scalar.
2278
2279 TIME an optional value used to control the printing of overdue messages.  The
2280 default is now.  It isn't the date of the invoice; that's the `_date' field.
2281 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2282 L<Time::Local> and L<Date::Parse> for conversion functions.
2283
2284 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2285 when emailing the invoice as part of a multipart/related MIME email.
2286
2287 =cut
2288
2289 #some falze laziness w/print_text and print_latex (and send_csv)
2290 sub print_html {
2291   my( $self, $today, $template, $cid ) = @_;
2292   $today ||= time;
2293
2294   my $cust_main = $self->cust_main;
2295   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2296     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2297
2298   $template ||= $self->_agent_template;
2299   my $templatefile = 'invoice_html';
2300   my $suffix = length($template) ? "_$template" : '';
2301   $templatefile .= $suffix;
2302   my @html_template = map "$_\n", $conf->config($templatefile)
2303     or die "cannot load config file $templatefile";
2304
2305   my $html_template = new Text::Template(
2306     TYPE   => 'ARRAY',
2307     SOURCE => \@html_template,
2308     DELIMITERS => [ '<%=', '%>' ],
2309   );
2310
2311   $html_template->compile()
2312     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2313
2314   my %invoice_data = (
2315     'custnum'      => $self->custnum,
2316     'invnum'       => $self->invnum,
2317     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2318     'today'        => time2str('%b %o, %Y', $today),
2319     'agent'        => encode_entities($cust_main->agent->agent),
2320     'payname'      => encode_entities($cust_main->payname),
2321     'company'      => encode_entities($cust_main->company),
2322     'address1'     => encode_entities($cust_main->address1),
2323     'address2'     => encode_entities($cust_main->address2),
2324     'city'         => encode_entities($cust_main->city),
2325     'state'        => encode_entities($cust_main->state),
2326     'zip'          => encode_entities($cust_main->zip),
2327     'terms'        => $conf->config('invoice_default_terms')
2328                       || 'Payable upon receipt',
2329     'cid'          => $cid,
2330     'template'     => $template,
2331 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2332   );
2333
2334   if (
2335          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2336       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2337   ) {
2338     $invoice_data{'returnaddress'} =
2339       join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2340   } else {
2341     $invoice_data{'returnaddress'} =
2342       join("\n", map { 
2343                        s/~/&nbsp;/g;
2344                        s/\\\\\*?\s*$/<BR>/;
2345                        s/\\hyphenation\{[\w\s\-]+\}//;
2346                        $_;
2347                      }
2348                      $conf->config_orbase( 'invoice_latexreturnaddress',
2349                                            $template
2350                                          )
2351           );
2352   }
2353
2354   my $countrydefault = $conf->config('countrydefault') || 'US';
2355   if ( $cust_main->country eq $countrydefault ) {
2356     $invoice_data{'country'} = '';
2357   } else {
2358     $invoice_data{'country'} =
2359       encode_entities(code2country($cust_main->country));
2360   }
2361
2362   if (
2363          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2364       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2365   ) {
2366     $invoice_data{'notes'} =
2367       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2368   } else {
2369     $invoice_data{'notes'} = 
2370       join("\n", map { 
2371                        s/%%(.*)$/<!-- $1 -->/;
2372                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2373                        s/\\begin\{enumerate\}/<ol>/;
2374                        s/\\item /  <li>/;
2375                        s/\\end\{enumerate\}/<\/ol>/;
2376                        s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2377                        s/\\\\\*/ /;
2378                        $_;
2379                      } 
2380                      $conf->config_orbase('invoice_latexnotes', $template)
2381           );
2382   }
2383
2384 #  #do variable substitutions in notes
2385 #  $invoice_data{'notes'} =
2386 #    join("\n",
2387 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2388 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2389 #    );
2390
2391   if (
2392          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2393       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2394   ) {
2395    $invoice_data{'footer'} =
2396      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2397   } else {
2398    $invoice_data{'footer'} =
2399        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2400                       $conf->config_orbase('invoice_latexfooter', $template)
2401            );
2402   }
2403
2404   $invoice_data{'po_line'} =
2405     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2406       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2407       : '';
2408
2409   my $money_char = $conf->config('money_char') || '$';
2410
2411   foreach my $line_item ( $self->_items ) {
2412     my $detail = {
2413       ext_description => [],
2414     };
2415     $detail->{'ref'} = $line_item->{'pkgnum'};
2416     $detail->{'description'} = encode_entities($line_item->{'description'});
2417     if ( exists $line_item->{'ext_description'} ) {
2418       @{$detail->{'ext_description'}} = map {
2419         encode_entities($_);
2420       } @{$line_item->{'ext_description'}};
2421     }
2422     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2423     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2424
2425     push @{$invoice_data{'detail_items'}}, $detail;
2426   }
2427
2428
2429   my $taxtotal = 0;
2430   foreach my $tax ( $self->_items_tax ) {
2431     my $total = {};
2432     $total->{'total_item'} = encode_entities($tax->{'description'});
2433     $taxtotal += $tax->{'amount'};
2434     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2435     push @{$invoice_data{'total_items'}}, $total;
2436   }
2437
2438   if ( $taxtotal ) {
2439     my $total = {};
2440     $total->{'total_item'} = 'Sub-total';
2441     $total->{'total_amount'} =
2442       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2443     unshift @{$invoice_data{'total_items'}}, $total;
2444   }
2445
2446   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2447   {
2448     my $total = {};
2449     $total->{'total_item'} = '<b>Total</b>';
2450     $total->{'total_amount'} =
2451       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2452     push @{$invoice_data{'total_items'}}, $total;
2453   }
2454
2455   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2456
2457   # credits
2458   foreach my $credit ( $self->_items_credits ) {
2459     my $total;
2460     $total->{'total_item'} = encode_entities($credit->{'description'});
2461     #$credittotal
2462     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2463     push @{$invoice_data{'total_items'}}, $total;
2464   }
2465
2466   # payments
2467   foreach my $payment ( $self->_items_payments ) {
2468     my $total = {};
2469     $total->{'total_item'} = encode_entities($payment->{'description'});
2470     #$paymenttotal
2471     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2472     push @{$invoice_data{'total_items'}}, $total;
2473   }
2474
2475   { 
2476     my $total;
2477     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2478     $total->{'total_amount'} =
2479       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2480     push @{$invoice_data{'total_items'}}, $total;
2481   }
2482
2483   $html_template->fill_in( HASH => \%invoice_data);
2484 }
2485
2486 # quick subroutine for print_latex
2487 #
2488 # There are ten characters that LaTeX treats as special characters, which
2489 # means that they do not simply typeset themselves: 
2490 #      # $ % & ~ _ ^ \ { }
2491 #
2492 # TeX ignores blanks following an escaped character; if you want a blank (as
2493 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2494
2495 sub _latex_escape {
2496   my $value = shift;
2497   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2498   $value =~ s/([<>])/\$$1\$/g;
2499   $value;
2500 }
2501
2502 #utility methods for print_*
2503
2504 sub balance_due_msg {
2505   my $self = shift;
2506   my $msg = 'Balance Due';
2507   return $msg unless $conf->exists('invoice_default_terms');
2508   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2509     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2510   } elsif ( $conf->config('invoice_default_terms') ) {
2511     $msg .= ' - '. $conf->config('invoice_default_terms');
2512   }
2513   $msg;
2514 }
2515
2516 sub _items {
2517   my $self = shift;
2518   my @display = scalar(@_)
2519                 ? @_
2520                 : qw( _items_previous _items_pkg );
2521                 #: qw( _items_pkg );
2522                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2523   my @b = ();
2524   foreach my $display ( @display ) {
2525     push @b, $self->$display(@_);
2526   }
2527   @b;
2528 }
2529
2530 sub _items_previous {
2531   my $self = shift;
2532   my $cust_main = $self->cust_main;
2533   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2534   my @b = ();
2535   foreach ( @pr_cust_bill ) {
2536     push @b, {
2537       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2538                        ' ('. time2str('%x',$_->_date). ')',
2539       #'pkgpart'     => 'N/A',
2540       'pkgnum'      => 'N/A',
2541       'amount'      => sprintf("%.2f", $_->owed),
2542     };
2543   }
2544   @b;
2545
2546   #{
2547   #    'description'     => 'Previous Balance',
2548   #    #'pkgpart'         => 'N/A',
2549   #    'pkgnum'          => 'N/A',
2550   #    'amount'          => sprintf("%10.2f", $pr_total ),
2551   #    'ext_description' => [ map {
2552   #                                 "Invoice ". $_->invnum.
2553   #                                 " (". time2str("%x",$_->_date). ") ".
2554   #                                 sprintf("%10.2f", $_->owed)
2555   #                         } @pr_cust_bill ],
2556
2557   #};
2558 }
2559
2560 sub _items_pkg {
2561   my $self = shift;
2562   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2563   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2564 }
2565
2566 sub _items_tax {
2567   my $self = shift;
2568   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2569   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2570 }
2571
2572 sub _items_cust_bill_pkg {
2573   my $self = shift;
2574   my $cust_bill_pkg = shift;
2575
2576   my @b = ();
2577   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2578
2579     my $desc = $cust_bill_pkg->desc;
2580
2581     if ( $cust_bill_pkg->pkgnum > 0 ) {
2582
2583       if ( $cust_bill_pkg->setup != 0 ) {
2584         my $description = $desc;
2585         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2586         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2587         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2588         push @b, {
2589           description     => $description,
2590           #pkgpart         => $part_pkg->pkgpart,
2591           pkgnum          => $cust_bill_pkg->pkgnum,
2592           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2593           ext_description => \@d,
2594         };
2595       }
2596
2597       if ( $cust_bill_pkg->recur != 0 ) {
2598         push @b, {
2599           description     => "$desc (" .
2600                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2601                                time2str('%x', $cust_bill_pkg->edate). ')',
2602           #pkgpart         => $part_pkg->pkgpart,
2603           pkgnum          => $cust_bill_pkg->pkgnum,
2604           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2605           ext_description =>
2606             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2607                                                         $cust_bill_pkg->sdate),
2608               $cust_bill_pkg->details,
2609             ],
2610         };
2611       }
2612
2613     } else { #pkgnum tax or one-shot line item (??)
2614
2615       if ( $cust_bill_pkg->setup != 0 ) {
2616         push @b, {
2617           'description' => $desc,
2618           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2619         };
2620       }
2621       if ( $cust_bill_pkg->recur != 0 ) {
2622         push @b, {
2623           'description' => "$desc (".
2624                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2625                            time2str("%x", $cust_bill_pkg->edate). ')',
2626           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2627         };
2628       }
2629
2630     }
2631
2632   }
2633
2634   @b;
2635
2636 }
2637
2638 sub _items_credits {
2639   my $self = shift;
2640
2641   my @b;
2642   #credits
2643   foreach ( $self->cust_credited ) {
2644
2645     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2646
2647     my $reason = $_->cust_credit->reason;
2648     #my $reason = substr($_->cust_credit->reason,0,32);
2649     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2650     $reason = " ($reason) " if $reason;
2651     push @b, {
2652       #'description' => 'Credit ref\#'. $_->crednum.
2653       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2654       #                 $reason,
2655       'description' => 'Credit applied '.
2656                        time2str("%x",$_->cust_credit->_date). $reason,
2657       'amount'      => sprintf("%.2f",$_->amount),
2658     };
2659   }
2660   #foreach ( @cr_cust_credit ) {
2661   #  push @buf,[
2662   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2663   #    $money_char. sprintf("%10.2f",$_->credited)
2664   #  ];
2665   #}
2666
2667   @b;
2668
2669 }
2670
2671 sub _items_payments {
2672   my $self = shift;
2673
2674   my @b;
2675   #get & print payments
2676   foreach ( $self->cust_bill_pay ) {
2677
2678     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2679
2680     push @b, {
2681       'description' => "Payment received ".
2682                        time2str("%x",$_->cust_pay->_date ),
2683       'amount'      => sprintf("%.2f", $_->amount )
2684     };
2685   }
2686
2687   @b;
2688
2689 }
2690
2691
2692 =back
2693
2694 =head1 SUBROUTINES
2695
2696 =over 4
2697
2698 =item reprint
2699
2700 =cut
2701
2702 sub process_reprint {
2703   process_re_X('print', @_);
2704 }
2705
2706 =item reemail
2707
2708 =cut
2709
2710 sub process_reemail {
2711   process_re_X('email', @_);
2712 }
2713
2714 =item refax
2715
2716 =cut
2717
2718 sub process_refax {
2719   process_re_X('fax', @_);
2720 }
2721
2722 use Storable qw(thaw);
2723 use Data::Dumper;
2724 use MIME::Base64;
2725 sub process_re_X {
2726   my( $method, $job ) = ( shift, shift );
2727   warn "process_re_X $method for job $job\n" if $DEBUG;
2728
2729   my $param = thaw(decode_base64(shift));
2730   warn Dumper($param) if $DEBUG;
2731
2732   re_X(
2733     $method,
2734     $job,
2735     %$param,
2736   );
2737
2738 }
2739
2740 sub re_X {
2741   my($method, $job, %param ) = @_;
2742 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2743   if ( $DEBUG ) {
2744     warn "re_X $method for job $job with param:\n".
2745          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2746   }
2747
2748   #some false laziness w/search/cust_bill.html
2749   my $distinct = '';
2750   my $orderby = 'ORDER BY cust_bill._date';
2751
2752   my @where;
2753
2754   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2755     push @where, "cust_bill._date >= $1";
2756   }
2757   if ( $param{'end'} =~ /^(\d+)$/ ) {
2758     push @where, "cust_bill._date < $1";
2759   }
2760   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2761     push @where, "cust_main.agentnum = $1";
2762   }
2763
2764   my $owed =
2765     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2766                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2767              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2768                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2769
2770   push @where, "0 != $owed"
2771     if $param{'open'};
2772
2773   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2774     if $param{'days'};
2775
2776   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2777
2778   my $addl_from = 'left join cust_main using ( custnum )';
2779
2780   if ( $param{'newest_percust'} ) {
2781     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2782     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2783     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2784   }
2785      
2786   my @cust_bill = qsearch( 'cust_bill',
2787                            {},
2788                            "$distinct cust_bill.*",
2789                            $extra_sql,
2790                            '',
2791                            $addl_from
2792                          );
2793
2794   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2795   foreach my $cust_bill ( @cust_bill ) {
2796     $cust_bill->$method();
2797
2798     if ( $job ) { #progressbar foo
2799       $num++;
2800       if ( time - $min_sec > $last ) {
2801         my $error = $job->update_statustext(
2802           int( 100 * $num / scalar(@cust_bill) )
2803         );
2804         die $error if $error;
2805         $last = time;
2806       }
2807     }
2808
2809   }
2810
2811 }
2812
2813 =back
2814
2815 =head1 BUGS
2816
2817 The delete method.
2818
2819 print_text formatting (and some logic :/) is in source, but needs to be
2820 slurped in from a file.  Also number of lines ($=).
2821
2822 =head1 SEE ALSO
2823
2824 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2825 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2826 documentation.
2827
2828 =cut
2829
2830 1;
2831