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