invoice service address modifications
[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 Date::Format;
9 use Text::Template 1.20;
10 use File::Temp 0.14;
11 use String::ShellQuote;
12 use HTML::Entities;
13 use Locale::Country;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
18 use FS::cust_main;
19 use FS::cust_bill_pkg;
20 use FS::cust_credit;
21 use FS::cust_pay;
22 use FS::cust_pkg;
23 use FS::cust_credit_bill;
24 use FS::pay_batch;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
27 use FS::cust_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 (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
276
277 =cut
278
279 sub cust_bill_event {
280   my $self = shift;
281   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
282 }
283
284 =item num_cust_bill_event
285
286 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
287
288 =cut
289
290 sub num_cust_bill_event {
291   my $self = shift;
292   my $sql =
293     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
294   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
295   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
296   $sth->fetchrow_arrayref->[0];
297 }
298
299 =item cust_event
300
301 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
302
303 =cut
304
305 #false laziness w/cust_pkg.pm
306 sub cust_event {
307   my $self = shift;
308   qsearch({
309     'table'     => 'cust_event',
310     'addl_from' => 'JOIN part_event USING ( eventpart )',
311     'hashref'   => { 'tablenum' => $self->invnum },
312     'extra_sql' => " AND eventtable = 'cust_bill' ",
313   });
314 }
315
316 =item num_cust_event
317
318 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
319
320 =cut
321
322 #false laziness w/cust_pkg.pm
323 sub num_cust_event {
324   my $self = shift;
325   my $sql =
326     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
327     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
328   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
329   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
330   $sth->fetchrow_arrayref->[0];
331 }
332
333 =item cust_main
334
335 Returns the customer (see L<FS::cust_main>) for this invoice.
336
337 =cut
338
339 sub cust_main {
340   my $self = shift;
341   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
342 }
343
344 =item cust_suspend_if_balance_over AMOUNT
345
346 Suspends the customer associated with this invoice if the total amount owed on
347 this invoice and all older invoices is greater than the specified amount.
348
349 Returns a list: an empty list on success or a list of errors.
350
351 =cut
352
353 sub cust_suspend_if_balance_over {
354   my( $self, $amount ) = ( shift, shift );
355   my $cust_main = $self->cust_main;
356   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
357     return ();
358   } else {
359     $cust_main->suspend(@_);
360   }
361 }
362
363 =item cust_credit
364
365 Depreciated.  See the cust_credited method.
366
367  #Returns a list consisting of the total previous credited (see
368  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
369  #outstanding credits (FS::cust_credit objects).
370
371 =cut
372
373 sub cust_credit {
374   use Carp;
375   croak "FS::cust_bill->cust_credit depreciated; see ".
376         "FS::cust_bill->cust_credit_bill";
377   #my $self = shift;
378   #my $total = 0;
379   #my @cust_credit = sort { $a->_date <=> $b->_date }
380   #  grep { $_->credited != 0 && $_->_date < $self->_date }
381   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
382   #;
383   #foreach (@cust_credit) { $total += $_->credited; }
384   #$total, @cust_credit;
385 }
386
387 =item cust_pay
388
389 Depreciated.  See the cust_bill_pay method.
390
391 #Returns all payments (see L<FS::cust_pay>) for this invoice.
392
393 =cut
394
395 sub cust_pay {
396   use Carp;
397   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
398   #my $self = shift;
399   #sort { $a->_date <=> $b->_date }
400   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
401   #;
402 }
403
404 =item cust_bill_pay
405
406 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
407
408 =cut
409
410 sub cust_bill_pay {
411   my $self = shift;
412   sort { $a->_date <=> $b->_date }
413     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
414 }
415
416 =item cust_credited
417
418 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
419
420 =cut
421
422 sub cust_credited {
423   my $self = shift;
424   sort { $a->_date <=> $b->_date }
425     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
426   ;
427 }
428
429 =item tax
430
431 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
432
433 =cut
434
435 sub tax {
436   my $self = shift;
437   my $total = 0;
438   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
439                                              'pkgnum' => 0 } );
440   foreach (@taxlines) { $total += $_->setup; }
441   $total;
442 }
443
444 =item owed
445
446 Returns the amount owed (still outstanding) on this invoice, which is charged
447 minus all payment applications (see L<FS::cust_bill_pay>) and credit
448 applications (see L<FS::cust_credit_bill>).
449
450 =cut
451
452 sub owed {
453   my $self = shift;
454   my $balance = $self->charged;
455   $balance -= $_->amount foreach ( $self->cust_bill_pay );
456   $balance -= $_->amount foreach ( $self->cust_credited );
457   $balance = sprintf( "%.2f", $balance);
458   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
459   $balance;
460 }
461
462 =item apply_payments_and_credits
463
464 =cut
465
466 sub apply_payments_and_credits {
467   my $self = shift;
468
469   local $SIG{HUP} = 'IGNORE';
470   local $SIG{INT} = 'IGNORE';
471   local $SIG{QUIT} = 'IGNORE';
472   local $SIG{TERM} = 'IGNORE';
473   local $SIG{TSTP} = 'IGNORE';
474   local $SIG{PIPE} = 'IGNORE';
475
476   my $oldAutoCommit = $FS::UID::AutoCommit;
477   local $FS::UID::AutoCommit = 0;
478   my $dbh = dbh;
479
480   $self->select_for_update; #mutex
481
482   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
483   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
484
485   while ( $self->owed > 0 and ( @payments || @credits ) ) {
486
487     my $app = '';
488     if ( @payments && @credits ) {
489
490       #decide which goes first by weight of top (unapplied) line item
491
492       my @open_lineitems = $self->open_cust_bill_pkg;
493
494       my $max_pay_weight =
495         max( map  { $_->part_pkg->pay_weight || 0 }
496              grep { $_ }
497              map  { $_->cust_pkg }
498                   @open_lineitems
499            );
500       my $max_credit_weight =
501         max( map  { $_->part_pkg->credit_weight || 0 }
502              grep { $_ } 
503              map  { $_->cust_pkg }
504                   @open_lineitems
505            );
506
507       #if both are the same... payments first?  it has to be something
508       if ( $max_pay_weight >= $max_credit_weight ) {
509         $app = 'pay';
510       } else {
511         $app = 'credit';
512       }
513     
514     } elsif ( @payments ) {
515       $app = 'pay';
516     } elsif ( @credits ) {
517       $app = 'credit';
518     } else {
519       die "guru meditation #12 and 35";
520     }
521
522     if ( $app eq 'pay' ) {
523
524       my $payment = shift @payments;
525
526       $app = new FS::cust_bill_pay {
527         'paynum'  => $payment->paynum,
528         'amount'  => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
529       };
530
531     } elsif ( $app eq 'credit' ) {
532
533       my $credit = shift @credits;
534
535       $app = new FS::cust_credit_bill {
536         'crednum' => $credit->crednum,
537         'amount'  => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
538       };
539
540     } else {
541       die "guru meditation #12 and 35";
542     }
543
544     $app->invnum( $self->invnum );
545
546     my $error = $app->insert;
547     if ( $error ) {
548       $dbh->rollback if $oldAutoCommit;
549       return "Error inserting ". $app->table. " record: $error";
550     }
551     die $error if $error;
552
553   }
554
555   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
556   ''; #no error
557
558 }
559
560 =item generate_email PARAMHASH
561
562 PARAMHASH can contain the following:
563
564 =over 4
565
566 =item from       => sender address, required
567
568 =item tempate    => alternate template name, optional
569
570 =item print_text => text attachment arrayref, optional
571
572 =item subject    => email subject, optional
573
574 =back
575
576 Returns an argument list to be passed to L<FS::Misc::send_email>.
577
578 =cut
579
580 use MIME::Entity;
581
582 sub generate_email {
583
584   my $self = shift;
585   my %args = @_;
586
587   my $me = '[FS::cust_bill::generate_email]';
588
589   my %return = (
590     'from'      => $args{'from'},
591     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
592   );
593
594   if (ref($args{'to'}) eq 'ARRAY') {
595     $return{'to'} = $args{'to'};
596   } else {
597     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
598                            $self->cust_main->invoicing_list
599                     ];
600   }
601
602   if ( $conf->exists('invoice_html') ) {
603
604     warn "$me creating HTML/text multipart message"
605       if $DEBUG;
606
607     $return{'nobody'} = 1;
608
609     my $alternative = build MIME::Entity
610       'Type'        => 'multipart/alternative',
611       'Encoding'    => '7bit',
612       'Disposition' => 'inline'
613     ;
614
615     my $data;
616     if ( $conf->exists('invoice_email_pdf')
617          and scalar($conf->config('invoice_email_pdf_note')) ) {
618
619       warn "$me using 'invoice_email_pdf_note' in multipart message"
620         if $DEBUG;
621       $data = [ map { $_ . "\n" }
622                     $conf->config('invoice_email_pdf_note')
623               ];
624
625     } else {
626
627       warn "$me not using 'invoice_email_pdf_note' in multipart message"
628         if $DEBUG;
629       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
630         $data = $args{'print_text'};
631       } else {
632         $data = [ $self->print_text('', $args{'template'}) ];
633       }
634
635     }
636
637     $alternative->attach(
638       'Type'        => 'text/plain',
639       #'Encoding'    => 'quoted-printable',
640       'Encoding'    => '7bit',
641       'Data'        => $data,
642       'Disposition' => 'inline',
643     );
644
645     $args{'from'} =~ /\@([\w\.\-]+)/;
646     my $from = $1 || 'example.com';
647     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
648
649     my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
650     my $file;
651     if ( defined($args{'template'}) && length($args{'template'})
652          && -e "$path/logo_". $args{'template'}. ".png"
653        )
654     {
655       $file = "$path/logo_". $args{'template'}. ".png";
656     } else {
657       $file = "$path/logo.png";
658     }
659
660     my $image = build MIME::Entity
661       'Type'       => 'image/png',
662       'Encoding'   => 'base64',
663       'Path'       => $file,
664       'Filename'   => 'logo.png',
665       'Content-ID' => "<$content_id>",
666     ;
667
668     $alternative->attach(
669       'Type'        => 'text/html',
670       'Encoding'    => 'quoted-printable',
671       'Data'        => [ '<html>',
672                          '  <head>',
673                          '    <title>',
674                          '      '. encode_entities($return{'subject'}), 
675                          '    </title>',
676                          '  </head>',
677                          '  <body bgcolor="#e8e8e8">',
678                          $self->print_html('', $args{'template'}, $content_id),
679                          '  </body>',
680                          '</html>',
681                        ],
682       'Disposition' => 'inline',
683       #'Filename'    => 'invoice.pdf',
684     );
685
686     if ( $conf->exists('invoice_email_pdf') ) {
687
688       #attaching pdf too:
689       # multipart/mixed
690       #   multipart/related
691       #     multipart/alternative
692       #       text/plain
693       #       text/html
694       #     image/png
695       #   application/pdf
696
697       my $related = build MIME::Entity 'Type'     => 'multipart/related',
698                                        'Encoding' => '7bit';
699
700       #false laziness w/Misc::send_email
701       $related->head->replace('Content-type',
702         $related->mime_type.
703         '; boundary="'. $related->head->multipart_boundary. '"'.
704         '; type=multipart/alternative'
705       );
706
707       $related->add_part($alternative);
708
709       $related->add_part($image);
710
711       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
712
713       $return{'mimeparts'} = [ $related, $pdf ];
714
715     } else {
716
717       #no other attachment:
718       # multipart/related
719       #   multipart/alternative
720       #     text/plain
721       #     text/html
722       #   image/png
723
724       $return{'content-type'} = 'multipart/related';
725       $return{'mimeparts'} = [ $alternative, $image ];
726       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
727       #$return{'disposition'} = 'inline';
728
729     }
730   
731   } else {
732
733     if ( $conf->exists('invoice_email_pdf') ) {
734       warn "$me creating PDF attachment"
735         if $DEBUG;
736
737       #mime parts arguments a la MIME::Entity->build().
738       $return{'mimeparts'} = [
739         { $self->mimebuild_pdf('', $args{'template'}) }
740       ];
741     }
742   
743     if ( $conf->exists('invoice_email_pdf')
744          and scalar($conf->config('invoice_email_pdf_note')) ) {
745
746       warn "$me using 'invoice_email_pdf_note'"
747         if $DEBUG;
748       $return{'body'} = [ map { $_ . "\n" }
749                               $conf->config('invoice_email_pdf_note')
750                         ];
751
752     } else {
753
754       warn "$me not using 'invoice_email_pdf_note'"
755         if $DEBUG;
756       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
757         $return{'body'} = $args{'print_text'};
758       } else {
759         $return{'body'} = [ $self->print_text('', $args{'template'}) ];
760       }
761
762     }
763
764   }
765
766   %return;
767
768 }
769
770 =item mimebuild_pdf
771
772 Returns a list suitable for passing to MIME::Entity->build(), representing
773 this invoice as PDF attachment.
774
775 =cut
776
777 sub mimebuild_pdf {
778   my $self = shift;
779   (
780     'Type'        => 'application/pdf',
781     'Encoding'    => 'base64',
782     'Data'        => [ $self->print_pdf(@_) ],
783     'Disposition' => 'attachment',
784     'Filename'    => 'invoice.pdf',
785   );
786 }
787
788 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
789
790 Sends this invoice to the destinations configured for this customer: sends
791 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
792
793 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
794
795 AGENTNUM, if specified, means that this invoice will only be sent for customers
796 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
797 single agent) or an arrayref of agentnums.
798
799 INVOICE_FROM, if specified, overrides the default email invoice From: address.
800
801 AMOUNT, if specified, only sends the invoice if the total amount owed on this
802 invoice and all older invoices is greater than the specified amount.
803
804 =cut
805
806 sub queueable_send {
807   my %opt = @_;
808
809   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
810     or die "invalid invoice number: " . $opt{invnum};
811
812   my @args = ( $opt{template}, $opt{agentnum} );
813   push @args, $opt{invoice_from}
814     if exists($opt{invoice_from}) && $opt{invoice_from};
815
816   my $error = $self->send( @args );
817   die $error if $error;
818
819 }
820
821 sub send {
822   my $self = shift;
823   my $template = scalar(@_) ? shift : '';
824   if ( scalar(@_) && $_[0]  ) {
825     my $agentnums = ref($_[0]) ? shift : [ shift ];
826     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
827   }
828
829   my $invoice_from =
830     scalar(@_)
831       ? shift
832       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
833
834   my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
835
836   return ''
837     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
838
839   my @invoicing_list = $self->cust_main->invoicing_list;
840
841   #$self->email_invoice($template, $invoice_from)
842   $self->email($template, $invoice_from)
843     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
844
845   #$self->print_invoice($template)
846   $self->print($template)
847     if grep { $_ eq 'POST' } @invoicing_list; #postal
848
849   $self->fax_invoice($template)
850     if grep { $_ eq 'FAX' } @invoicing_list; #fax
851
852   '';
853
854 }
855
856 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
857
858 Emails this invoice.
859
860 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
861
862 INVOICE_FROM, if specified, overrides the default email invoice From: address.
863
864 =cut
865
866 sub queueable_email {
867   my %opt = @_;
868
869   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
870     or die "invalid invoice number: " . $opt{invnum};
871
872   my @args = ( $opt{template} );
873   push @args, $opt{invoice_from}
874     if exists($opt{invoice_from}) && $opt{invoice_from};
875
876   my $error = $self->email( @args );
877   die $error if $error;
878
879 }
880
881 #sub email_invoice {
882 sub email {
883   my $self = shift;
884   my $template = scalar(@_) ? shift : '';
885   my $invoice_from =
886     scalar(@_)
887       ? shift
888       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
889
890   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
891                             $self->cust_main->invoicing_list;
892
893   #better to notify this person than silence
894   @invoicing_list = ($invoice_from) unless @invoicing_list;
895
896   my $error = send_email(
897     $self->generate_email(
898       'from'       => $invoice_from,
899       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
900       'template'   => $template,
901     )
902   );
903   die "can't email invoice: $error\n" if $error;
904   #die "$error\n" if $error;
905
906 }
907
908 =item lpr_data [ TEMPLATENAME ]
909
910 Returns the postscript or plaintext for this invoice as an arrayref.
911
912 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
913
914 =cut
915
916 sub lpr_data {
917   my( $self, $template) = @_;
918   $conf->exists('invoice_latex')
919     ? [ $self->print_ps('', $template) ]
920     : [ $self->print_text('', $template) ];
921 }
922
923 =item print [ TEMPLATENAME ]
924
925 Prints this invoice.
926
927 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
928
929 =cut
930
931 #sub print_invoice {
932 sub print {
933   my $self = shift;
934   my $template = scalar(@_) ? shift : '';
935
936   do_print $self->lpr_data($template);
937 }
938
939 =item fax_invoice [ TEMPLATENAME ] 
940
941 Faxes this invoice.
942
943 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
944
945 =cut
946
947 sub fax_invoice {
948   my $self = shift;
949   my $template = scalar(@_) ? shift : '';
950
951   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
952     unless $conf->exists('invoice_latex');
953
954   my $dialstring = $self->cust_main->getfield('fax');
955   #Check $dialstring?
956
957   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
958                         'dialstring' => $dialstring,
959                       );
960   die $error if $error;
961
962 }
963
964 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
965
966 Like B<send>, but only sends the invoice if it is the newest open invoice for
967 this customer.
968
969 =cut
970
971 sub send_if_newest {
972   my $self = shift;
973
974   return ''
975     if scalar(
976                grep { $_->owed > 0 } 
977                     qsearch('cust_bill', {
978                       'custnum' => $self->custnum,
979                       #'_date'   => { op=>'>', value=>$self->_date },
980                       'invnum'  => { op=>'>', value=>$self->invnum },
981                     } )
982              );
983     
984   $self->send(@_);
985 }
986
987 =item send_csv OPTION => VALUE, ...
988
989 Sends invoice as a CSV data-file to a remote host with the specified protocol.
990
991 Options are:
992
993 protocol - currently only "ftp"
994 server
995 username
996 password
997 dir
998
999 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1000 and YYMMDDHHMMSS is a timestamp.
1001
1002 See L</print_csv> for a description of the output format.
1003
1004 =cut
1005
1006 sub send_csv {
1007   my($self, %opt) = @_;
1008
1009   #create file(s)
1010
1011   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1012   mkdir $spooldir, 0700 unless -d $spooldir;
1013
1014   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1015   my $file = "$spooldir/$tracctnum.csv";
1016   
1017   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1018
1019   open(CSV, ">$file") or die "can't open $file: $!";
1020   print CSV $header;
1021
1022   print CSV $detail;
1023
1024   close CSV;
1025
1026   my $net;
1027   if ( $opt{protocol} eq 'ftp' ) {
1028     eval "use Net::FTP;";
1029     die $@ if $@;
1030     $net = Net::FTP->new($opt{server}) or die @$;
1031   } else {
1032     die "unknown protocol: $opt{protocol}";
1033   }
1034
1035   $net->login( $opt{username}, $opt{password} )
1036     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1037
1038   $net->binary or die "can't set binary mode";
1039
1040   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1041
1042   $net->put($file) or die "can't put $file: $!";
1043
1044   $net->quit;
1045
1046   unlink $file;
1047
1048 }
1049
1050 =item spool_csv
1051
1052 Spools CSV invoice data.
1053
1054 Options are:
1055
1056 =over 4
1057
1058 =item format - 'default' or 'billco'
1059
1060 =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>).
1061
1062 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1063
1064 =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.
1065
1066 =back
1067
1068 =cut
1069
1070 sub spool_csv {
1071   my($self, %opt) = @_;
1072
1073   my $cust_main = $self->cust_main;
1074
1075   if ( $opt{'dest'} ) {
1076     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1077                              $cust_main->invoicing_list;
1078     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1079                      || ! keys %invoicing_list;
1080   }
1081
1082   if ( $opt{'balanceover'} ) {
1083     return 'N/A'
1084       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1085   }
1086
1087   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1088   mkdir $spooldir, 0700 unless -d $spooldir;
1089
1090   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1091
1092   my $file =
1093     "$spooldir/".
1094     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1095     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1096     '.csv';
1097   
1098   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1099
1100   open(CSV, ">>$file") or die "can't open $file: $!";
1101   flock(CSV, LOCK_EX);
1102   seek(CSV, 0, 2);
1103
1104   print CSV $header;
1105
1106   if ( lc($opt{'format'}) eq 'billco' ) {
1107
1108     flock(CSV, LOCK_UN);
1109     close CSV;
1110
1111     $file =
1112       "$spooldir/".
1113       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1114       '-detail.csv';
1115
1116     open(CSV,">>$file") or die "can't open $file: $!";
1117     flock(CSV, LOCK_EX);
1118     seek(CSV, 0, 2);
1119   }
1120
1121   print CSV $detail;
1122
1123   flock(CSV, LOCK_UN);
1124   close CSV;
1125
1126   return '';
1127
1128 }
1129
1130 =item print_csv OPTION => VALUE, ...
1131
1132 Returns CSV data for this invoice.
1133
1134 Options are:
1135
1136 format - 'default' or 'billco'
1137
1138 Returns a list consisting of two scalars.  The first is a single line of CSV
1139 header information for this invoice.  The second is one or more lines of CSV
1140 detail information for this invoice.
1141
1142 If I<format> is not specified or "default", the fields of the CSV file are as
1143 follows:
1144
1145 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1146
1147 =over 4
1148
1149 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1150
1151 B<record_type> is C<cust_bill> for the initial header line only.  The
1152 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1153 fields are filled in.
1154
1155 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1156 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1157 are filled in.
1158
1159 =item invnum - invoice number
1160
1161 =item custnum - customer number
1162
1163 =item _date - invoice date
1164
1165 =item charged - total invoice amount
1166
1167 =item first - customer first name
1168
1169 =item last - customer first name
1170
1171 =item company - company name
1172
1173 =item address1 - address line 1
1174
1175 =item address2 - address line 1
1176
1177 =item city
1178
1179 =item state
1180
1181 =item zip
1182
1183 =item country
1184
1185 =item pkg - line item description
1186
1187 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1188
1189 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1190
1191 =item sdate - start date for recurring fee
1192
1193 =item edate - end date for recurring fee
1194
1195 =back
1196
1197 If I<format> is "billco", the fields of the header CSV file are as follows:
1198
1199   +-------------------------------------------------------------------+
1200   |                        FORMAT HEADER FILE                         |
1201   |-------------------------------------------------------------------|
1202   | Field | Description                   | Name       | Type | Width |
1203   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1204   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1205   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1206   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1207   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1208   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1209   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1210   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1211   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1212   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1213   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1214   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1215   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1216   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1217   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1218   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1219   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1220   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1221   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1222   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1223   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1224   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1225   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1226   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1227   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1228   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1229   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1230   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1231   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1232   +-------+-------------------------------+------------+------+-------+
1233
1234 If I<format> is "billco", the fields of the detail CSV file are as follows:
1235
1236                                   FORMAT FOR DETAIL FILE
1237         |                            |           |      |
1238   Field | Description                | Name      | Type | Width
1239   1     | N/A-Leave Empty            | RC        | CHAR |     2
1240   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1241   3     | Account Number             | TRACCTNUM | CHAR |    15
1242   4     | Invoice Number             | TRINVOICE | CHAR |    15
1243   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1244   6     | Transaction Detail         | DETAILS   | CHAR |   100
1245   7     | Amount                     | AMT       | NUM* |     9
1246   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1247   9     | Grouping Code              | GROUP     | CHAR |     2
1248   10    | User Defined               | ACCT CODE | CHAR |    15
1249
1250 =cut
1251
1252 sub print_csv {
1253   my($self, %opt) = @_;
1254   
1255   eval "use Text::CSV_XS";
1256   die $@ if $@;
1257
1258   my $cust_main = $self->cust_main;
1259
1260   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1261
1262   if ( lc($opt{'format'}) eq 'billco' ) {
1263
1264     my $taxtotal = 0;
1265     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1266
1267     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1268
1269     my( $previous_balance, @unused ) = $self->previous; #previous balance
1270
1271     my $pmt_cr_applied = 0;
1272     $pmt_cr_applied += $_->{'amount'}
1273       foreach ( $self->_items_payments, $self->_items_credits ) ;
1274
1275     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1276
1277     $csv->combine(
1278       '',                         #  1 | N/A-Leave Empty               CHAR   2
1279       '',                         #  2 | N/A-Leave Empty               CHAR  15
1280       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1281       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1282       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1283       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1284       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1285       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1286       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1287       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1288       '',                         # 10 | Ancillary Billing Information CHAR  30
1289       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1290       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1291
1292       # XXX ?
1293       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1294
1295       # XXX ?
1296       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1297
1298       $previous_balance,          # 15 | Previous Balance              NUM*   9
1299       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1300       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1301       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1302       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1303       '',                         # 20 | 30 Day Aging                  NUM*   9
1304       '',                         # 21 | 60 Day Aging                  NUM*   9
1305       '',                         # 22 | 90 Day Aging                  NUM*   9
1306       'N',                        # 23 | Y/N                           CHAR   1
1307       '',                         # 24 | Remittance automation         CHAR 100
1308       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1309       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1310       '0',                        # 27 | Federal Tax***                NUM*   9
1311       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1312       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1313     );
1314
1315   } else {
1316   
1317     $csv->combine(
1318       'cust_bill',
1319       $self->invnum,
1320       $self->custnum,
1321       time2str("%x", $self->_date),
1322       sprintf("%.2f", $self->charged),
1323       ( map { $cust_main->getfield($_) }
1324           qw( first last company address1 address2 city state zip country ) ),
1325       map { '' } (1..5),
1326     ) or die "can't create csv";
1327   }
1328
1329   my $header = $csv->string. "\n";
1330
1331   my $detail = '';
1332   if ( lc($opt{'format'}) eq 'billco' ) {
1333
1334     my $lineseq = 0;
1335     foreach my $item ( $self->_items_pkg ) {
1336
1337       $csv->combine(
1338         '',                     #  1 | N/A-Leave Empty            CHAR   2
1339         '',                     #  2 | N/A-Leave Empty            CHAR  15
1340         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1341         $self->invnum,          #  4 | Invoice Number             CHAR  15
1342         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1343         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1344         $item->{'amount'},      #  7 | Amount                     NUM*   9
1345         '',                     #  8 | Line Format Control**      CHAR   2
1346         '',                     #  9 | Grouping Code              CHAR   2
1347         '',                     # 10 | User Defined               CHAR  15
1348       );
1349
1350       $detail .= $csv->string. "\n";
1351
1352     }
1353
1354   } else {
1355
1356     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1357
1358       my($pkg, $setup, $recur, $sdate, $edate);
1359       if ( $cust_bill_pkg->pkgnum ) {
1360       
1361         ($pkg, $setup, $recur, $sdate, $edate) = (
1362           $cust_bill_pkg->part_pkg->pkg,
1363           ( $cust_bill_pkg->setup != 0
1364             ? sprintf("%.2f", $cust_bill_pkg->setup )
1365             : '' ),
1366           ( $cust_bill_pkg->recur != 0
1367             ? sprintf("%.2f", $cust_bill_pkg->recur )
1368             : '' ),
1369           ( $cust_bill_pkg->sdate 
1370             ? time2str("%x", $cust_bill_pkg->sdate)
1371             : '' ),
1372           ($cust_bill_pkg->edate 
1373             ?time2str("%x", $cust_bill_pkg->edate)
1374             : '' ),
1375         );
1376   
1377       } else { #pkgnum tax
1378         next unless $cust_bill_pkg->setup != 0;
1379         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1380                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1381                          : 'Tax';
1382         ($pkg, $setup, $recur, $sdate, $edate) =
1383           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1384       }
1385   
1386       $csv->combine(
1387         'cust_bill_pkg',
1388         $self->invnum,
1389         ( map { '' } (1..11) ),
1390         ($pkg, $setup, $recur, $sdate, $edate)
1391       ) or die "can't create csv";
1392
1393       $detail .= $csv->string. "\n";
1394
1395     }
1396
1397   }
1398
1399   ( $header, $detail );
1400
1401 }
1402
1403 =item comp
1404
1405 Pays this invoice with a compliemntary payment.  If there is an error,
1406 returns the error, otherwise returns false.
1407
1408 =cut
1409
1410 sub comp {
1411   my $self = shift;
1412   my $cust_pay = new FS::cust_pay ( {
1413     'invnum'   => $self->invnum,
1414     'paid'     => $self->owed,
1415     '_date'    => '',
1416     'payby'    => 'COMP',
1417     'payinfo'  => $self->cust_main->payinfo,
1418     'paybatch' => '',
1419   } );
1420   $cust_pay->insert;
1421 }
1422
1423 =item realtime_card
1424
1425 Attempts to pay this invoice with a credit card payment via a
1426 Business::OnlinePayment realtime gateway.  See
1427 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1428 for supported processors.
1429
1430 =cut
1431
1432 sub realtime_card {
1433   my $self = shift;
1434   $self->realtime_bop( 'CC', @_ );
1435 }
1436
1437 =item realtime_ach
1438
1439 Attempts to pay this invoice with an electronic check (ACH) payment via a
1440 Business::OnlinePayment realtime gateway.  See
1441 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1442 for supported processors.
1443
1444 =cut
1445
1446 sub realtime_ach {
1447   my $self = shift;
1448   $self->realtime_bop( 'ECHECK', @_ );
1449 }
1450
1451 =item realtime_lec
1452
1453 Attempts to pay this invoice with phone bill (LEC) payment via a
1454 Business::OnlinePayment realtime gateway.  See
1455 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1456 for supported processors.
1457
1458 =cut
1459
1460 sub realtime_lec {
1461   my $self = shift;
1462   $self->realtime_bop( 'LEC', @_ );
1463 }
1464
1465 sub realtime_bop {
1466   my( $self, $method ) = @_;
1467
1468   my $cust_main = $self->cust_main;
1469   my $balance = $cust_main->balance;
1470   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1471   $amount = sprintf("%.2f", $amount);
1472   return "not run (balance $balance)" unless $amount > 0;
1473
1474   my $description = 'Internet Services';
1475   if ( $conf->exists('business-onlinepayment-description') ) {
1476     my $dtempl = $conf->config('business-onlinepayment-description');
1477
1478     my $agent_obj = $cust_main->agent
1479       or die "can't retreive agent for $cust_main (agentnum ".
1480              $cust_main->agentnum. ")";
1481     my $agent = $agent_obj->agent;
1482     my $pkgs = join(', ',
1483       map { $_->part_pkg->pkg }
1484         grep { $_->pkgnum } $self->cust_bill_pkg
1485     );
1486     $description = eval qq("$dtempl");
1487   }
1488
1489   $cust_main->realtime_bop($method, $amount,
1490     'description' => $description,
1491     'invnum'      => $self->invnum,
1492   );
1493
1494 }
1495
1496 =item batch_card OPTION => VALUE...
1497
1498 Adds a payment for this invoice to the pending credit card batch (see
1499 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1500 runs the payment using a realtime gateway.
1501
1502 =cut
1503
1504 sub batch_card {
1505   my ($self, %options) = @_;
1506   my $cust_main = $self->cust_main;
1507
1508   $options{invnum} = $self->invnum;
1509   
1510   $cust_main->batch_card(%options);
1511 }
1512
1513 sub _agent_template {
1514   my $self = shift;
1515   $self->cust_main->agent_template;
1516 }
1517
1518 sub _agent_invoice_from {
1519   my $self = shift;
1520   $self->cust_main->agent_invoice_from;
1521 }
1522
1523 =item print_text [ TIME [ , TEMPLATE ] ]
1524
1525 Returns an text invoice, as a list of lines.
1526
1527 TIME an optional value used to control the printing of overdue messages.  The
1528 default is now.  It isn't the date of the invoice; that's the `_date' field.
1529 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1530 L<Time::Local> and L<Date::Parse> for conversion functions.
1531
1532 =cut
1533
1534 sub print_text {
1535   my( $self, $today, $template ) = @_;
1536
1537   my %params = ( 'format' => 'template' );
1538   $params{'time'} = $today if $today;
1539   $params{'template'} = $template if $template;
1540
1541   $self->print_generic( %params );
1542 }
1543
1544 =item print_latex [ TIME [ , TEMPLATE ] ]
1545
1546 Internal method - returns a filename of a filled-in LaTeX template for this
1547 invoice (Note: add ".tex" to get the actual filename), and a filename of
1548 an associated logo (with the .eps extension included).
1549
1550 See print_ps and print_pdf for methods that return PostScript and PDF output.
1551
1552 TIME an optional value used to control the printing of overdue messages.  The
1553 default is now.  It isn't the date of the invoice; that's the `_date' field.
1554 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1555 L<Time::Local> and L<Date::Parse> for conversion functions.
1556
1557 =cut
1558
1559 sub print_latex {
1560
1561   my( $self, $today, $template ) = @_;
1562
1563   my %params = ( 'format' => 'latex' );
1564   $params{'time'} = $today if $today;
1565   $params{'template'} = $template if $template;
1566
1567   $template ||= $self->_agent_template;
1568
1569   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1570   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1571                            DIR      => $dir,
1572                            SUFFIX   => '.eps',
1573                            UNLINK   => 0,
1574                          ) or die "can't open temp file: $!\n";
1575
1576   if ($template && $conf->exists("logo_${template}.eps")) {
1577     print $lh $conf->config_binary("logo_${template}.eps")
1578       or die "can't write temp file: $!\n";
1579   }else{
1580     print $lh $conf->config_binary('logo.eps')
1581       or die "can't write temp file: $!\n";
1582   }
1583   close $lh;
1584   $params{'logo_file'} = $lh->filename;
1585
1586   my @filled_in = $self->print_generic( %params );
1587   
1588   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1589                            DIR      => $dir,
1590                            SUFFIX   => '.tex',
1591                            UNLINK   => 0,
1592                          ) or die "can't open temp file: $!\n";
1593   print $fh join('', @filled_in );
1594   close $fh;
1595
1596   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1597   return ($1, $params{'logo_file'});
1598
1599 }
1600
1601 =item print_generic OPTIONS_HASH
1602
1603 Internal method - returns a filled-in template for this invoice as a scalar.
1604
1605 See print_ps and print_pdf for methods that return PostScript and PDF output.
1606
1607 Non optional options include 
1608   format - latex, html, template
1609
1610 Optional options include
1611
1612 template - a value used as a suffix for a configuration template
1613
1614 time - a value used to control the printing of overdue messages.  The
1615 default is now.  It isn't the date of the invoice; that's the `_date' field.
1616 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1617 L<Time::Local> and L<Date::Parse> for conversion functions.
1618
1619 cid - 
1620
1621 =cut
1622
1623 sub print_generic {
1624
1625   my( $self, %params ) = @_;
1626   my $today = $params{today} ? $params{today} : time;
1627   warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1628     if $DEBUG;
1629
1630   my $format = $params{format};
1631   die "Unknown format: $format"
1632     unless $format =~ /^(latex|html|template)$/;
1633
1634   my $cust_main = $self->cust_main;
1635   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1636     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1637
1638
1639   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
1640                      'html'     => [ '<%=', '%>' ],
1641                      'template' => [ '{', '}' ],
1642                    );
1643
1644   #create the template
1645   my $template = $params{template} ? $params{template} : $self->_agent_template;
1646   my $templatefile = "invoice_$format";
1647   $templatefile .= "_$template"
1648     if length($template);
1649   my @invoice_template = map "$_\n", $conf->config($templatefile)
1650     or die "cannot load config data $templatefile";
1651
1652   my $old_latex = '';
1653   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1654     #change this to a die when the old code is removed
1655     warn "old-style invoice template $templatefile; ".
1656          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1657     $old_latex = 'true';
1658     @invoice_template = _translate_old_latex_format(@invoice_template);
1659   } 
1660
1661   my $text_template = new Text::Template(
1662     TYPE => 'ARRAY',
1663     SOURCE => \@invoice_template,
1664     DELIMITERS => $delimiters{$format},
1665   );
1666
1667   $text_template->compile()
1668     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1669
1670
1671   # additional substitution could possibly cause breakage in existing templates
1672   my %convert_maps = ( 
1673     'latex' => {
1674                  'notes'         => sub { map "$_", @_ },
1675                  'footer'        => sub { map "$_", @_ },
1676                  'smallfooter'   => sub { map "$_", @_ },
1677                  'returnaddress' => sub { map "$_", @_ },
1678                  'coupon'        => sub { map "$_", @_ },
1679                },
1680     'html'  => {
1681                  'notes' =>
1682                    sub {
1683                      map { 
1684                        s/%%(.*)$/<!-- $1 -->/g;
1685                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1686                        s/\\begin\{enumerate\}/<ol>/g;
1687                        s/\\item /  <li>/g;
1688                        s/\\end\{enumerate\}/<\/ol>/g;
1689                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1690                        s/\\\\\*/<br>/g;
1691                        s/\\dollar ?/\$/g;
1692                        s/\\#/#/g;
1693                        s/~/&nbsp;/g;
1694                        $_;
1695                      }  @_
1696                    },
1697                  'footer' =>
1698                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1699                  'smallfooter' =>
1700                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1701                  'returnaddress' =>
1702                    sub {
1703                      map { 
1704                        s/~/&nbsp;/g;
1705                        s/\\\\\*?\s*$/<BR>/;
1706                        s/\\hyphenation\{[\w\s\-]+}//;
1707                        $_;
1708                      }  @_
1709                    },
1710                  'coupon'        => sub { "" },
1711                },
1712     'template' => {
1713                  'notes' =>
1714                    sub {
1715                      map { 
1716                        s/%%.*$//g;
1717                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1718                        s/\\begin\{enumerate\}//g;
1719                        s/\\item /  * /g;
1720                        s/\\end\{enumerate\}//g;
1721                        s/\\textbf\{(.*)\}/$1/g;
1722                        s/\\\\\*/ /;
1723                        s/\\dollar ?/\$/g;
1724                        $_;
1725                      }  @_
1726                    },
1727                  'footer' =>
1728                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1729                  'smallfooter' =>
1730                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1731                  'returnaddress' =>
1732                    sub {
1733                      map { 
1734                        s/~/ /g;
1735                        s/\\\\\*?\s*$/\n/;             # dubious
1736                        s/\\hyphenation\{[\w\s\-]+}//;
1737                        $_;
1738                      }  @_
1739                    },
1740                  'coupon'        => sub { "" },
1741                },
1742   );
1743
1744
1745   # hashes for differing output formats
1746   my %nbsps = ( 'latex'    => '~',
1747                 'html'     => '',    # '&nbps;' would be nice
1748                 'template' => '',    # not used
1749               );
1750   my $nbsp = $nbsps{$format};
1751
1752   my %escape_functions = ( 'latex'    => \&_latex_escape,
1753                            'html'     => \&encode_entities,
1754                            'template' => sub { shift },
1755                          );
1756   my $escape_function = $escape_functions{$format};
1757
1758   my %date_formats = ( 'latex'    => '%b %o, %Y',
1759                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
1760                        'template' => '%s',
1761                      );
1762   my $date_format = $date_formats{$format};
1763
1764   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
1765                                                },
1766                              'html'     => sub { return '<b>'. shift(). '</b>'
1767                                                },
1768                              'template' => sub { shift },
1769                            );
1770   my $embolden_function = $embolden_functions{$format};
1771
1772
1773   # generate template variables
1774   my $returnaddress;
1775   if (
1776          defined( $conf->config_orbase( "invoice_${format}returnaddress",
1777                                         $template
1778                                       )
1779                 )
1780        && length( $conf->config_orbase( "invoice_${format}returnaddress",
1781                                         $template
1782                                       )
1783                 )
1784   ) {
1785
1786     $returnaddress = join("\n",
1787       $conf->config_orbase("invoice_${format}returnaddress", $template)
1788     );
1789
1790   } elsif ( grep /\S/,
1791             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1792
1793     my $convert_map = $convert_maps{$format}{'returnaddress'};
1794     $returnaddress =
1795       join( "\n",
1796             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1797                                                  $template
1798                                                )
1799                          )
1800           );
1801   } elsif ( grep /\S/, $conf->config('company_address') ) {
1802
1803     my $convert_map = $convert_maps{$format}{'returnaddress'};
1804     $returnaddress = join( "\n", &$convert_map(
1805                                    map { s/( {2,})/'~' x length($1)/eg;
1806                                          s/$/\\\\\*/;
1807                                          $_
1808                                        }
1809                                      ( $conf->config('company_name'),
1810                                        $conf->config('company_address'),
1811                                      )
1812                                  )
1813                      );
1814
1815   } else {
1816
1817     my $warning = "Couldn't find a return address; ".
1818                   "do you need to set the company_address configuration value?";
1819     warn "$warning\n";
1820     $returnaddress = $nbsp;
1821     #$returnaddress = $warning;
1822
1823   }
1824
1825   my %invoice_data = (
1826     'company_name'    => scalar( $conf->config('company_name') ),
1827     'company_address' => join("\n", $conf->config('company_address') ). "\n",
1828     'custnum'         => $self->custnum,
1829     'invnum'          => $self->invnum,
1830     'date'            => time2str($date_format, $self->_date),
1831     'today'           => time2str('%b %o, %Y', $today),
1832     'agent'           => &$escape_function($cust_main->agent->agent),
1833     'agent_custid'    => &$escape_function($cust_main->agent_custid),
1834     'payname'         => &$escape_function($cust_main->payname),
1835     'company'         => &$escape_function($cust_main->company),
1836     'address1'        => &$escape_function($cust_main->address1),
1837     'address2'        => &$escape_function($cust_main->address2),
1838     'city'            => &$escape_function($cust_main->city),
1839     'state'           => &$escape_function($cust_main->state),
1840     'zip'             => &$escape_function($cust_main->zip),
1841     'returnaddress'   => $returnaddress,
1842     #'quantity'        => 1,
1843     'terms'           => $self->terms,
1844     'template'        => $params{'template'},
1845     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
1846     # better hang on to conf_dir for a while
1847     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1848     'page'            => 1,
1849     'total_pages'     => 1,
1850     'current_charges' => sprintf("%.2f", $self->charged),
1851     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
1852     'ship_enable'     => $conf->exists('invoice-ship_address'),
1853     'unitprices'      => $conf->exists('invoice-unitprice'),
1854   );
1855
1856   my $countrydefault = $conf->config('countrydefault') || 'US';
1857   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1858   foreach ( qw( contact company address1 address2 city state zip country fax) ){
1859     my $method = $prefix.$_;
1860     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1861   }
1862   $invoice_data{'ship_country'} = ''
1863     if ( $invoice_data{'ship_country'} eq $countrydefault );
1864   
1865   $invoice_data{'cid'} = $params{'cid'}
1866     if $params{'cid'};
1867
1868   if ( $cust_main->country eq $countrydefault ) {
1869     $invoice_data{'country'} = '';
1870   } else {
1871     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1872   }
1873
1874   my @address = ();
1875   $invoice_data{'address'} = \@address;
1876   push @address,
1877     $cust_main->payname.
1878       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1879         ? " (P.O. #". $cust_main->payinfo. ")"
1880         : ''
1881       )
1882   ;
1883   push @address, $cust_main->company
1884     if $cust_main->company;
1885   push @address, $cust_main->address1;
1886   push @address, $cust_main->address2
1887     if $cust_main->address2;
1888   push @address,
1889     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1890   push @address, $invoice_data{'country'}
1891     if $invoice_data{'country'};
1892   push @address, ''
1893     while (scalar(@address) < 5);
1894
1895   $invoice_data{'logo_file'} = $params{'logo_file'}
1896     if $params{'logo_file'};
1897
1898   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1899 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1900   #my $balance_due = $self->owed + $pr_total - $cr_total;
1901   my $balance_due = $self->owed + $pr_total;
1902   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1903   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1904
1905   #do variable substitution in notes, footer, smallfooter
1906   foreach my $include (qw( notes footer smallfooter coupon )) {
1907
1908     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1909     my @inc_src;
1910
1911     if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1912
1913       @inc_src = $conf->config($inc_file);
1914
1915     } else {
1916
1917       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1918
1919       my $convert_map = $convert_maps{$format}{$include};
1920
1921       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1922                        s/--\@\]/$delimiters{$format}[1]/g;
1923                        $_;
1924                      } 
1925                  &$convert_map( $conf->config($inc_file) );
1926
1927     }
1928
1929     my $inc_tt = new Text::Template (
1930       TYPE       => 'ARRAY',
1931       SOURCE     => [ map "$_\n", @inc_src ],
1932       DELIMITERS => $delimiters{$format},
1933     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1934
1935     unless ( $inc_tt->compile() ) {
1936       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1937       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1938       die $error;
1939     }
1940
1941     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1942
1943     $invoice_data{$include} =~ s/\n+$//
1944       if ($format eq 'latex');
1945   }
1946
1947   $invoice_data{'po_line'} =
1948     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1949       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1950       : $nbsp;
1951
1952   my %money_chars = ( 'latex'    => '',
1953                       'html'     => $conf->config('money_char') || '$',
1954                       'template' => '',
1955                     );
1956   my $money_char = $money_chars{$format};
1957
1958   my %other_money_chars = ( 'latex'    => '\dollar ',
1959                             'html'     => $conf->config('money_char') || '$',
1960                             'template' => '',
1961                           );
1962   my $other_money_char = $other_money_chars{$format};
1963
1964   my @detail_items = ();
1965   my @total_items = ();
1966   my @buf = ();
1967   my @sections = ();
1968
1969   $invoice_data{'detail_items'} = \@detail_items;
1970   $invoice_data{'total_items'} = \@total_items;
1971   $invoice_data{'buf'} = \@buf;
1972   $invoice_data{'sections'} = \@sections;
1973   
1974   my $previous_section = { 'description' => 'Previous Charges',
1975                            'subtotal'    => $other_money_char.
1976                                             sprintf('%.2f', $pr_total),
1977                          };
1978
1979   my $taxtotal = 0;
1980   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
1981                       'subtotal'    => $taxtotal }; # adjusted below
1982
1983   my $adjusttotal = 0;
1984   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
1985                          'subtotal'    => 0 }; # adjusted below
1986
1987   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
1988   if ( $multisection ) {
1989     push @sections, $self->_items_sections;
1990   }else{
1991     push @sections, { 'description' => '', 'subtotal' => '' };
1992   }
1993
1994   foreach my $line_item ( $conf->exists('disable_previous_balance') 
1995                             ? ()
1996                             : $self->_items_previous
1997                         )
1998   {
1999     my $detail = {
2000       ext_description => [],
2001     };
2002     $detail->{'ref'} = $line_item->{'pkgnum'};
2003     $detail->{'quantity'} = 1;
2004     $detail->{'section'} = $previous_section;
2005     $detail->{'description'} = &$escape_function($line_item->{'description'});
2006     if ( exists $line_item->{'ext_description'} ) {
2007       @{$detail->{'ext_description'}} = map {
2008         &$escape_function($_);
2009       } @{$line_item->{'ext_description'}};
2010     }
2011     $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2012                           $line_item->{'amount'};
2013     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2014   
2015     push @detail_items, $detail;
2016     push @buf, [ $detail->{'description'},
2017                  $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2018                ];
2019   }
2020   
2021   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2022     push @buf, ['','-----------'];
2023     push @buf, [ 'Total Previous Balance',
2024                  $money_char. sprintf("%10.2f", $pr_total) ];
2025     push @buf, ['',''];
2026   }
2027
2028   foreach my $section (@sections) {
2029
2030     $section->{'subtotal'} = $other_money_char.
2031                              sprintf('%.2f', $section->{'subtotal'})
2032       if $multisection;
2033
2034     if ( $section->{'description'} ) {
2035       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2036                    [ '', '' ],
2037                  );
2038     }
2039
2040     my %options = ();
2041     $options{'section'} = $section if $multisection;
2042     $options{'format'} = $format;
2043     $options{'escape_function'} = $escape_function;
2044
2045     foreach my $line_item ( $self->_items_pkg(%options) ) {
2046       my $detail = {
2047         ext_description => [],
2048       };
2049       $detail->{'ref'} = $line_item->{'pkgnum'};
2050       $detail->{'quantity'} = $line_item->{'quantity'};
2051       $detail->{'section'} = $section;
2052       $detail->{'description'} = &$escape_function($line_item->{'description'});
2053       if ( exists $line_item->{'ext_description'} ) {
2054         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2055       }
2056       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2057                               $line_item->{'amount'};
2058       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2059                                  $line_item->{'unit_amount'};
2060       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2061   
2062       push @detail_items, $detail;
2063       push @buf, ( [ $detail->{'description'},
2064                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2065                    ],
2066                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2067                  );
2068     }
2069
2070     if ( $section->{'description'} ) {
2071       push @buf, ( ['','-----------'],
2072                    [ $section->{'description'}. ' sub-total',
2073                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2074                    ],
2075                    [ '', '' ],
2076                    [ '', '' ],
2077                  );
2078     }
2079   
2080   }
2081   
2082   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2083     unshift @sections, $previous_section if $pr_total;
2084   }
2085
2086   foreach my $tax ( $self->_items_tax ) {
2087     my $total = {};
2088     $total->{'total_item'} = &$escape_function($tax->{'description'});
2089     $taxtotal += $tax->{'amount'};
2090     $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2091     if ( $multisection ) {
2092       my $money = $old_latex ? '' : $money_char;
2093       push @detail_items, {
2094         ext_description => [],
2095         ref          => '',
2096         quantity     => '',
2097         description  => &$escape_function($tax->{'description'}),
2098         amount       => $money. $tax->{'amount'},
2099         product_code => '',
2100         section      => $tax_section,
2101       };
2102     }else{
2103       push @total_items, $total;
2104     }
2105     push @buf,[ $total->{'total_item'},
2106                 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2107               ];
2108
2109   }
2110   
2111   if ( $taxtotal ) {
2112     my $total = {};
2113     $total->{'total_item'} = 'Sub-total';
2114     $total->{'total_amount'} =
2115       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2116
2117     if ( $multisection ) {
2118       $tax_section->{'subtotal'} = $other_money_char.
2119                                    sprintf('%.2f', $taxtotal);
2120       $tax_section->{'pretotal'} = 'New charges sub-total '.
2121                                    $total->{'total_amount'};
2122       push @sections, $tax_section if $taxtotal;
2123     }else{
2124       unshift @total_items, $total;
2125     }
2126   }
2127   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2128   
2129   push @buf,['','-----------'];
2130   push @buf,[( $conf->exists('disable_previous_balance') 
2131                ? 'Total Charges'
2132                : 'Total New Charges'
2133              ),
2134              $money_char. sprintf("%10.2f",$self->charged) ];
2135   push @buf,['',''];
2136
2137   {
2138     my $total = {};
2139     $total->{'total_item'} = &$embolden_function('Total');
2140     $total->{'total_amount'} =
2141       &$embolden_function(
2142         $other_money_char.
2143         sprintf( '%.2f',
2144                  $self->charged + ( $conf->exists('disable_previous_balance')
2145                                     ? 0
2146                                     : $pr_total
2147                                   )
2148                )
2149       );
2150     if ( $multisection ) {
2151       $adjust_section->{'pretotal'} = 'New charges total '.
2152                                       $total->{'total_amount'};
2153     }else{
2154       push @total_items, $total;
2155     }
2156     push @buf,['','-----------'];
2157     push @buf,['Total Charges',
2158                $money_char.
2159                sprintf( '%10.2f', $self->charged +
2160                                     ( $conf->exists('disable_previous_balance')
2161                                         ? 0
2162                                         : $pr_total
2163                                     )
2164                       )
2165               ];
2166     push @buf,['',''];
2167   }
2168   
2169   unless ( $conf->exists('disable_previous_balance') ) {
2170     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2171   
2172     # credits
2173     my $credittotal = 0;
2174     foreach my $credit ( $self->_items_credits ) {
2175       my $total;
2176       $total->{'total_item'} = &$escape_function($credit->{'description'});
2177       $credittotal += $credit->{'amount'};
2178       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2179       $adjusttotal += $credit->{'amount'};
2180       if ( $multisection ) {
2181         my $money = $old_latex ? '' : $money_char;
2182         push @detail_items, {
2183           ext_description => [],
2184           ref          => '',
2185           quantity     => '',
2186           description  => &$escape_function($credit->{'description'}),
2187           amount       => $money. $credit->{'amount'},
2188           product_code => '',
2189           section      => $adjust_section,
2190         };
2191       }else{
2192         push @total_items, $total;
2193       }
2194     }
2195     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2196   
2197     # credits (again)
2198     foreach ( $self->cust_credited ) {
2199   
2200       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2201
2202       my $reason = substr($_->cust_credit->reason,0,32);
2203       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2204       $reason = " ($reason) " if $reason;
2205       push @buf,[
2206         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
2207         $money_char. sprintf("%10.2f",$_->amount)
2208       ];
2209     }
2210
2211     # payments
2212     my $paymenttotal = 0;
2213     foreach my $payment ( $self->_items_payments ) {
2214       my $total = {};
2215       $total->{'total_item'} = &$escape_function($payment->{'description'});
2216       $paymenttotal += $payment->{'amount'};
2217       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2218       $adjusttotal += $payment->{'amount'};
2219       if ( $multisection ) {
2220         my $money = $old_latex ? '' : $money_char;
2221         push @detail_items, {
2222           ext_description => [],
2223           ref          => '',
2224           quantity     => '',
2225           description  => &$escape_function($payment->{'description'}),
2226           amount       => $money. $payment->{'amount'},
2227           product_code => '',
2228           section      => $adjust_section,
2229         };
2230       }else{
2231         push @total_items, $total;
2232       }
2233       push @buf, [ $payment->{'description'},
2234                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2235                  ];
2236     }
2237     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2238   
2239     if ( $multisection ) {
2240       $adjust_section->{'subtotal'} = $other_money_char.
2241                                       sprintf('%.2f', $adjusttotal);
2242       push @sections, $adjust_section;
2243     }
2244
2245     { 
2246       my $total;
2247       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2248       $total->{'total_amount'} =
2249         &$embolden_function(
2250           $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2251         );
2252       if ( $multisection ) {
2253         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2254                                          $total->{'total_amount'};
2255       }else{
2256         push @total_items, $total;
2257       }
2258       push @buf,['','-----------'];
2259       push @buf,[$self->balance_due_msg, $money_char. 
2260         sprintf("%10.2f", $balance_due ) ];
2261     }
2262   }
2263
2264   $invoice_lines = 0;
2265   my $wasfunc = 0;
2266   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2267     /invoice_lines\((\d*)\)/;
2268     $invoice_lines += $1 || scalar(@buf);
2269     $wasfunc=1;
2270   }
2271   die "no invoice_lines() functions in template?"
2272     if ( $format eq 'template' && !$wasfunc );
2273
2274   if ($format eq 'template') {
2275
2276     if ( $invoice_lines ) {
2277       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2278       $invoice_data{'total_pages'}++
2279         if scalar(@buf) % $invoice_lines;
2280     }
2281
2282     #setup subroutine for the template
2283     sub FS::cust_bill::_template::invoice_lines {
2284       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2285       map { 
2286         scalar(@FS::cust_bill::_template::buf)
2287           ? shift @FS::cust_bill::_template::buf
2288           : [ '', '' ];
2289       }
2290       ( 1 .. $lines );
2291     }
2292
2293     my $lines;
2294     my @collect;
2295     while (@buf) {
2296       push @collect, split("\n",
2297         $text_template->fill_in( HASH => \%invoice_data,
2298                                  PACKAGE => 'FS::cust_bill::_template'
2299                                )
2300       );
2301       $FS::cust_bill::_template::page++;
2302     }
2303     map "$_\n", @collect;
2304   }else{
2305     warn "filling in template for invoice ". $self->invnum. "\n"
2306       if $DEBUG;
2307     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2308       if $DEBUG > 1;
2309
2310     $text_template->fill_in(HASH => \%invoice_data);
2311   }
2312 }
2313
2314 =item print_ps [ TIME [ , TEMPLATE ] ]
2315
2316 Returns an postscript invoice, as a scalar.
2317
2318 TIME an optional value used to control the printing of overdue messages.  The
2319 default is now.  It isn't the date of the invoice; that's the `_date' field.
2320 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2321 L<Time::Local> and L<Date::Parse> for conversion functions.
2322
2323 =cut
2324
2325 sub print_ps {
2326   my $self = shift;
2327
2328   my ($file, $lfile) = $self->print_latex(@_);
2329   my $ps = generate_ps($file);
2330   unlink($lfile);
2331
2332   $ps;
2333 }
2334
2335 =item print_pdf [ TIME [ , TEMPLATE ] ]
2336
2337 Returns an PDF invoice, as a scalar.
2338
2339 TIME an optional value used to control the printing of overdue messages.  The
2340 default is now.  It isn't the date of the invoice; that's the `_date' field.
2341 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2342 L<Time::Local> and L<Date::Parse> for conversion functions.
2343
2344 =cut
2345
2346 sub print_pdf {
2347   my $self = shift;
2348
2349   my ($file, $lfile) = $self->print_latex(@_);
2350   my $pdf = generate_pdf($file);
2351   unlink($lfile);
2352
2353   $pdf;
2354 }
2355
2356 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2357
2358 Returns an HTML invoice, as a scalar.
2359
2360 TIME an optional value used to control the printing of overdue messages.  The
2361 default is now.  It isn't the date of the invoice; that's the `_date' field.
2362 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2363 L<Time::Local> and L<Date::Parse> for conversion functions.
2364
2365 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2366 when emailing the invoice as part of a multipart/related MIME email.
2367
2368 =cut
2369
2370 sub print_html {
2371   my( $self, $today, $template, $cid ) = @_;
2372
2373   my %params = ( 'format' => 'html' );
2374   $params{'time'} = $today if $today;
2375   $params{'template'} = $template if $template;
2376   $params{'cid'} = $cid if $cid;
2377
2378   $self->print_generic( %params );
2379 }
2380
2381 # quick subroutine for print_latex
2382 #
2383 # There are ten characters that LaTeX treats as special characters, which
2384 # means that they do not simply typeset themselves: 
2385 #      # $ % & ~ _ ^ \ { }
2386 #
2387 # TeX ignores blanks following an escaped character; if you want a blank (as
2388 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2389
2390 sub _latex_escape {
2391   my $value = shift;
2392   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2393   $value =~ s/([<>])/\$$1\$/g;
2394   $value;
2395 }
2396
2397 #utility methods for print_*
2398
2399 sub _translate_old_latex_format {
2400   warn "_translate_old_latex_format called\n"
2401     if $DEBUG; 
2402
2403   my @template = ();
2404   while ( @_ ) {
2405     my $line = shift;
2406   
2407     if ( $line =~ /^%%Detail\s*$/ ) {
2408   
2409       push @template, q![@--!,
2410                       q!  foreach my $_tr_line (@detail_items) {!,
2411                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2412                       q!      $_tr_line->{'description'} .= !, 
2413                       q!        "\\tabularnewline\n~~".!,
2414                       q!        join( "\\tabularnewline\n~~",!,
2415                       q!          @{$_tr_line->{'ext_description'}}!,
2416                       q!        );!,
2417                       q!    }!;
2418
2419       while ( ( my $line_item_line = shift )
2420               !~ /^%%EndDetail\s*$/                            ) {
2421         $line_item_line =~ s/'/\\'/g;    # nice LTS
2422         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2423         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2424         push @template, "    \$OUT .= '$line_item_line';";
2425       }
2426   
2427       push @template, '}',
2428                       '--@]';
2429
2430     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2431
2432       push @template, '[@--',
2433                       '  foreach my $_tr_line (@total_items) {';
2434
2435       while ( ( my $total_item_line = shift )
2436               !~ /^%%EndTotalDetails\s*$/                      ) {
2437         $total_item_line =~ s/'/\\'/g;    # nice LTS
2438         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2439         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2440         push @template, "    \$OUT .= '$total_item_line';";
2441       }
2442
2443       push @template, '}',
2444                       '--@]';
2445
2446     } else {
2447       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2448       push @template, $line;  
2449     }
2450   
2451   }
2452
2453   if ($DEBUG) {
2454     warn "$_\n" foreach @template;
2455   }
2456
2457   (@template);
2458 }
2459
2460 sub terms {
2461   my $self = shift;
2462
2463   #check for an invoice- specific override (eventually)
2464   
2465   #check for a customer- specific override
2466   return $self->cust_main->invoice_terms
2467     if $self->cust_main->invoice_terms;
2468
2469   #use configured default or default default
2470   $conf->config('invoice_default_terms') || 'Payable upon receipt';
2471 }
2472
2473 sub due_date {
2474   my $self = shift;
2475   my $duedate = '';
2476   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2477     $duedate = $self->_date() + ( $1 * 86400 );
2478   }
2479   $duedate;
2480 }
2481
2482 sub due_date2str {
2483   my $self = shift;
2484   $self->due_date ? time2str(shift, $self->due_date) : '';
2485 }
2486
2487 sub balance_due_msg {
2488   my $self = shift;
2489   my $msg = 'Balance Due';
2490   return $msg unless $self->terms;
2491   if ( $self->due_date ) {
2492     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2493   } elsif ( $self->terms ) {
2494     $msg .= ' - '. $self->terms;
2495   }
2496   $msg;
2497 }
2498
2499 sub balance_due_date {
2500   my $self = shift;
2501   my $duedate = '';
2502   if (    $conf->exists('invoice_default_terms') 
2503        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2504     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2505   }
2506   $duedate;
2507 }
2508
2509 =item invnum_date_pretty
2510
2511 Returns a string with the invoice number and date, for example:
2512 "Invoice #54 (3/20/2008)"
2513
2514 =cut
2515
2516 sub invnum_date_pretty {
2517   my $self = shift;
2518   'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2519 }
2520
2521 sub _items_sections {
2522   my $self = shift;
2523
2524   my %s = ();
2525   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2526
2527     if ( $cust_bill_pkg->pkgnum > 0 ) {
2528
2529       my $desc = $cust_bill_pkg->part_pkg->categoryname;
2530
2531       $s{$desc} += $cust_bill_pkg->setup
2532         if ( $cust_bill_pkg->setup != 0 );
2533
2534       $s{$desc} += $cust_bill_pkg->recur
2535         if ( $cust_bill_pkg->recur != 0 );
2536
2537     }
2538
2539   }
2540
2541   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2542
2543 }
2544
2545 sub _items {
2546   my $self = shift;
2547
2548   #my @display = scalar(@_)
2549   #              ? @_
2550   #              : qw( _items_previous _items_pkg );
2551   #              #: qw( _items_pkg );
2552   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2553   my @display = qw( _items_previous _items_pkg );
2554
2555   my @b = ();
2556   foreach my $display ( @display ) {
2557     push @b, $self->$display(@_);
2558   }
2559   @b;
2560 }
2561
2562 sub _items_previous {
2563   my $self = shift;
2564   my $cust_main = $self->cust_main;
2565   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2566   my @b = ();
2567   foreach ( @pr_cust_bill ) {
2568     push @b, {
2569       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2570                        ' ('. time2str('%x',$_->_date). ')',
2571       #'pkgpart'     => 'N/A',
2572       'pkgnum'      => 'N/A',
2573       'amount'      => sprintf("%.2f", $_->owed),
2574     };
2575   }
2576   @b;
2577
2578   #{
2579   #    'description'     => 'Previous Balance',
2580   #    #'pkgpart'         => 'N/A',
2581   #    'pkgnum'          => 'N/A',
2582   #    'amount'          => sprintf("%10.2f", $pr_total ),
2583   #    'ext_description' => [ map {
2584   #                                 "Invoice ". $_->invnum.
2585   #                                 " (". time2str("%x",$_->_date). ") ".
2586   #                                 sprintf("%10.2f", $_->owed)
2587   #                         } @pr_cust_bill ],
2588
2589   #};
2590 }
2591
2592 sub _items_pkg {
2593   my $self = shift;
2594   my %options = @_;
2595   my $section = delete $options{'section'};
2596   my @cust_bill_pkg =
2597     grep { $_->pkgnum &&
2598            ( defined($section)
2599                ? $_->part_pkg->categoryname eq $section->{'description'}
2600                : 1
2601            )
2602          } $self->cust_bill_pkg;
2603   $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2604 }
2605
2606 sub _taxsort {
2607   return 0 unless $a cmp $b;
2608   return -1 if $b eq 'Tax';
2609   return 1 if $a eq 'Tax';
2610   return -1 if $b eq 'Other surcharges';
2611   return 1 if $a eq 'Other surcharges';
2612   $a cmp $b;
2613 }
2614
2615 sub _items_tax {
2616   my $self = shift;
2617   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2618   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2619 }
2620
2621 sub _items_cust_bill_pkg {
2622   my $self = shift;
2623   my $cust_bill_pkg = shift;
2624   my %opt = @_;
2625
2626   my $format = $opt{format} || '';
2627   my $escape_function = $opt{escape_function} || sub { shift };
2628
2629   my @b = ();
2630   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2631
2632     my $cust_pkg = $cust_bill_pkg->cust_pkg;
2633
2634     my $desc = $cust_bill_pkg->desc;
2635
2636     my %details_opt = ( 'format'          => $format,
2637                         'escape_function' => $escape_function,
2638                       );
2639
2640     if ( $cust_bill_pkg->pkgnum > 0 ) {
2641
2642       if ( $cust_bill_pkg->setup != 0 ) {
2643
2644         my $description = $desc;
2645         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2646
2647         my @d = map &{$escape_function}($_),
2648                        $cust_pkg->h_labels_short($self->_date);
2649         push @d, $cust_bill_pkg->details(%details_opt)
2650           if $cust_bill_pkg->recur == 0;
2651
2652         push @b, {
2653           description     => $description,
2654           #pkgpart         => $part_pkg->pkgpart,
2655           pkgnum          => $cust_bill_pkg->pkgnum,
2656           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2657           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2658           quantity        => $cust_bill_pkg->quantity,
2659           ext_description => \@d,
2660         };
2661       }
2662
2663       if ( $cust_bill_pkg->recur != 0 ) {
2664
2665         my $description = $desc;
2666         unless ( $conf->exists('disable_line_item_date_ranges') ) {
2667           $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2668                    " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2669         }
2670
2671         #at least until cust_bill_pkg has "past" ranges in addition to
2672         #the "future" sdate/edate ones... see #3032
2673         my @d = map &{$escape_function}($_),
2674                     $cust_pkg->h_labels_short($self->_date);
2675                                               #$cust_bill_pkg->edate,
2676                                               #$cust_bill_pkg->sdate),
2677         push @d, $cust_bill_pkg->details(%details_opt);
2678
2679         push @b, {
2680           description     => $description,
2681           #pkgpart         => $part_pkg->pkgpart,
2682           pkgnum          => $cust_bill_pkg->pkgnum,
2683           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2684           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2685           quantity        => $cust_bill_pkg->quantity,
2686           ext_description => \@d,
2687         };
2688
2689       }
2690
2691     } else { #pkgnum tax or one-shot line item (??)
2692
2693       if ( $cust_bill_pkg->setup != 0 ) {
2694         push @b, {
2695           'description' => $desc,
2696           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2697         };
2698       }
2699       if ( $cust_bill_pkg->recur != 0 ) {
2700         push @b, {
2701           'description' => "$desc (".
2702                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2703                            time2str("%x", $cust_bill_pkg->edate). ')',
2704           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2705         };
2706       }
2707
2708     }
2709
2710   }
2711
2712   @b;
2713
2714 }
2715
2716 sub _items_credits {
2717   my $self = shift;
2718
2719   my @b;
2720   #credits
2721   foreach ( $self->cust_credited ) {
2722
2723     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2724
2725     my $reason = $_->cust_credit->reason;
2726     #my $reason = substr($_->cust_credit->reason,0,32);
2727     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2728     $reason = " ($reason) " if $reason;
2729     push @b, {
2730       #'description' => 'Credit ref\#'. $_->crednum.
2731       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2732       #                 $reason,
2733       'description' => 'Credit applied '.
2734                        time2str("%x",$_->cust_credit->_date). $reason,
2735       'amount'      => sprintf("%.2f",$_->amount),
2736     };
2737   }
2738   #foreach ( @cr_cust_credit ) {
2739   #  push @buf,[
2740   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2741   #    $money_char. sprintf("%10.2f",$_->credited)
2742   #  ];
2743   #}
2744
2745   @b;
2746
2747 }
2748
2749 sub _items_payments {
2750   my $self = shift;
2751
2752   my @b;
2753   #get & print payments
2754   foreach ( $self->cust_bill_pay ) {
2755
2756     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2757
2758     push @b, {
2759       'description' => "Payment received ".
2760                        time2str("%x",$_->cust_pay->_date ),
2761       'amount'      => sprintf("%.2f", $_->amount )
2762     };
2763   }
2764
2765   @b;
2766
2767 }
2768
2769
2770 =back
2771
2772 =head1 SUBROUTINES
2773
2774 =over 4
2775
2776 =item reprint
2777
2778 =cut
2779
2780 sub process_reprint {
2781   process_re_X('print', @_);
2782 }
2783
2784 =item reemail
2785
2786 =cut
2787
2788 sub process_reemail {
2789   process_re_X('email', @_);
2790 }
2791
2792 =item refax
2793
2794 =cut
2795
2796 sub process_refax {
2797   process_re_X('fax', @_);
2798 }
2799
2800 use Storable qw(thaw);
2801 use Data::Dumper;
2802 use MIME::Base64;
2803 sub process_re_X {
2804   my( $method, $job ) = ( shift, shift );
2805   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2806
2807   my $param = thaw(decode_base64(shift));
2808   warn Dumper($param) if $DEBUG;
2809
2810   re_X(
2811     $method,
2812     $job,
2813     %$param,
2814   );
2815
2816 }
2817
2818 sub re_X {
2819   my($method, $job, %param ) = @_;
2820   if ( $DEBUG ) {
2821     warn "re_X $method for job $job with param:\n".
2822          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2823   }
2824
2825   #some false laziness w/search/cust_bill.html
2826   my $distinct = '';
2827   my $orderby = 'ORDER BY cust_bill._date';
2828
2829   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2830
2831   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2832      
2833   my @cust_bill = qsearch( {
2834     #'select'    => "cust_bill.*",
2835     'table'     => 'cust_bill',
2836     'addl_from' => $addl_from,
2837     'hashref'   => {},
2838     'extra_sql' => $extra_sql,
2839     'order_by'  => $orderby,
2840     'debug' => 1,
2841   } );
2842
2843   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2844     if $DEBUG;
2845
2846   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2847   foreach my $cust_bill ( @cust_bill ) {
2848     $cust_bill->$method();
2849
2850     if ( $job ) { #progressbar foo
2851       $num++;
2852       if ( time - $min_sec > $last ) {
2853         my $error = $job->update_statustext(
2854           int( 100 * $num / scalar(@cust_bill) )
2855         );
2856         die $error if $error;
2857         $last = time;
2858       }
2859     }
2860
2861   }
2862
2863 }
2864
2865 =back
2866
2867 =head1 CLASS METHODS
2868
2869 =over 4
2870
2871 =item owed_sql
2872
2873 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2874
2875 =cut
2876
2877 sub owed_sql {
2878   my $class = shift;
2879   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2880 }
2881
2882 =item net_sql
2883
2884 Returns an SQL fragment to retreive the net amount (charged minus credited).
2885
2886 =cut
2887
2888 sub net_sql {
2889   my $class = shift;
2890   'charged - '. $class->credited_sql;
2891 }
2892
2893 =item paid_sql
2894
2895 Returns an SQL fragment to retreive the amount paid against this invoice.
2896
2897 =cut
2898
2899 sub paid_sql {
2900   #my $class = shift;
2901   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2902        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
2903 }
2904
2905 =item credited_sql
2906
2907 Returns an SQL fragment to retreive the amount credited against this invoice.
2908
2909 =cut
2910
2911 sub credited_sql {
2912   #my $class = shift;
2913   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2914        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
2915 }
2916
2917 =item search_sql HASHREF
2918
2919 Class method which returns an SQL WHERE fragment to search for parameters
2920 specified in HASHREF.  Valid parameters are
2921
2922 =over 4
2923
2924 =item begin
2925
2926 Epoch date (UNIX timestamp) setting a lower bound for _date values
2927
2928 =item end
2929
2930 Epoch date (UNIX timestamp) setting an upper bound for _date values
2931
2932 =item invnum_min
2933
2934 =item invnum_max
2935
2936 =item agentnum
2937
2938 =item owed
2939
2940 =item net
2941
2942 =item days
2943
2944 =item newest_percust
2945
2946 =back
2947
2948 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2949
2950 =cut
2951
2952 sub search_sql {
2953   my($class, $param) = @_;
2954   if ( $DEBUG ) {
2955     warn "$me search_sql called with params: \n".
2956          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
2957   }
2958
2959   my @search = ();
2960
2961   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2962     push @search, "cust_bill._date >= $1";
2963   }
2964   if ( $param->{'end'} =~ /^(\d+)$/ ) {
2965     push @search, "cust_bill._date < $1";
2966   }
2967   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2968     push @search, "cust_bill.invnum >= $1";
2969   }
2970   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2971     push @search, "cust_bill.invnum <= $1";
2972   }
2973   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2974     push @search, "cust_main.agentnum = $1";
2975   }
2976
2977   push @search, '0 != '. FS::cust_bill->owed_sql
2978     if $param->{'open'};
2979
2980   push @search, '0 != '. FS::cust_bill->net_sql
2981     if $param->{'net'};
2982
2983   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2984     if $param->{'days'};
2985
2986   if ( $param->{'newest_percust'} ) {
2987
2988     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2989     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2990
2991     my @newest_where = map { my $x = $_;
2992                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
2993                              $x;
2994                            }
2995                            grep ! /^cust_main./, @search;
2996     my $newest_where = scalar(@newest_where)
2997                          ? ' AND '. join(' AND ', @newest_where)
2998                          : '';
2999
3000
3001     push @search, "cust_bill._date = (
3002       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3003         WHERE newest_cust_bill.custnum = cust_bill.custnum
3004           $newest_where
3005     )";
3006
3007   }
3008
3009   my $curuser = $FS::CurrentUser::CurrentUser;
3010   if ( $curuser->username eq 'fs_queue'
3011        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3012     my $username = $1;
3013     my $newuser = qsearchs('access_user', {
3014       'username' => $username,
3015       'disabled' => '',
3016     } );
3017     if ( $newuser ) {
3018       $curuser = $newuser;
3019     } else {
3020       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3021     }
3022   }
3023
3024   push @search, $curuser->agentnums_sql;
3025
3026   join(' AND ', @search );
3027
3028 }
3029
3030 =back
3031
3032 =head1 BUGS
3033
3034 The delete method.
3035
3036 =head1 SEE ALSO
3037
3038 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3039 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3040 documentation.
3041
3042 =cut
3043
3044 1;
3045