remove duplicate cust_bill_pkg creation RT#3919
[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 unsquelch_cdr - overrides any per customer cdr squelching when true
1635
1636 =cut
1637
1638 sub print_generic {
1639
1640   my( $self, %params ) = @_;
1641   my $today = $params{today} ? $params{today} : time;
1642   warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1643     if $DEBUG;
1644
1645   my $format = $params{format};
1646   die "Unknown format: $format"
1647     unless $format =~ /^(latex|html|template)$/;
1648
1649   my $cust_main = $self->cust_main;
1650   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1651     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1652
1653
1654   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
1655                      'html'     => [ '<%=', '%>' ],
1656                      'template' => [ '{', '}' ],
1657                    );
1658
1659   #create the template
1660   my $template = $params{template} ? $params{template} : $self->_agent_template;
1661   my $templatefile = "invoice_$format";
1662   $templatefile .= "_$template"
1663     if length($template);
1664   my @invoice_template = map "$_\n", $conf->config($templatefile)
1665     or die "cannot load config data $templatefile";
1666
1667   my $old_latex = '';
1668   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1669     #change this to a die when the old code is removed
1670     warn "old-style invoice template $templatefile; ".
1671          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1672     $old_latex = 'true';
1673     @invoice_template = _translate_old_latex_format(@invoice_template);
1674   } 
1675
1676   my $text_template = new Text::Template(
1677     TYPE => 'ARRAY',
1678     SOURCE => \@invoice_template,
1679     DELIMITERS => $delimiters{$format},
1680   );
1681
1682   $text_template->compile()
1683     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1684
1685
1686   # additional substitution could possibly cause breakage in existing templates
1687   my %convert_maps = ( 
1688     'latex' => {
1689                  'notes'         => sub { map "$_", @_ },
1690                  'footer'        => sub { map "$_", @_ },
1691                  'smallfooter'   => sub { map "$_", @_ },
1692                  'returnaddress' => sub { map "$_", @_ },
1693                  'coupon'        => sub { map "$_", @_ },
1694                },
1695     'html'  => {
1696                  'notes' =>
1697                    sub {
1698                      map { 
1699                        s/%%(.*)$/<!-- $1 -->/g;
1700                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1701                        s/\\begin\{enumerate\}/<ol>/g;
1702                        s/\\item /  <li>/g;
1703                        s/\\end\{enumerate\}/<\/ol>/g;
1704                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1705                        s/\\\\\*/<br>/g;
1706                        s/\\dollar ?/\$/g;
1707                        s/\\#/#/g;
1708                        s/~/&nbsp;/g;
1709                        $_;
1710                      }  @_
1711                    },
1712                  'footer' =>
1713                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1714                  'smallfooter' =>
1715                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1716                  'returnaddress' =>
1717                    sub {
1718                      map { 
1719                        s/~/&nbsp;/g;
1720                        s/\\\\\*?\s*$/<BR>/;
1721                        s/\\hyphenation\{[\w\s\-]+}//;
1722                        $_;
1723                      }  @_
1724                    },
1725                  'coupon'        => sub { "" },
1726                },
1727     'template' => {
1728                  'notes' =>
1729                    sub {
1730                      map { 
1731                        s/%%.*$//g;
1732                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1733                        s/\\begin\{enumerate\}//g;
1734                        s/\\item /  * /g;
1735                        s/\\end\{enumerate\}//g;
1736                        s/\\textbf\{(.*)\}/$1/g;
1737                        s/\\\\\*/ /;
1738                        s/\\dollar ?/\$/g;
1739                        $_;
1740                      }  @_
1741                    },
1742                  'footer' =>
1743                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1744                  'smallfooter' =>
1745                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1746                  'returnaddress' =>
1747                    sub {
1748                      map { 
1749                        s/~/ /g;
1750                        s/\\\\\*?\s*$/\n/;             # dubious
1751                        s/\\hyphenation\{[\w\s\-]+}//;
1752                        $_;
1753                      }  @_
1754                    },
1755                  'coupon'        => sub { "" },
1756                },
1757   );
1758
1759
1760   # hashes for differing output formats
1761   my %nbsps = ( 'latex'    => '~',
1762                 'html'     => '',    # '&nbps;' would be nice
1763                 'template' => '',    # not used
1764               );
1765   my $nbsp = $nbsps{$format};
1766
1767   my %escape_functions = ( 'latex'    => \&_latex_escape,
1768                            'html'     => \&encode_entities,
1769                            'template' => sub { shift },
1770                          );
1771   my $escape_function = $escape_functions{$format};
1772
1773   my %date_formats = ( 'latex'    => '%b %o, %Y',
1774                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
1775                        'template' => '%s',
1776                      );
1777   my $date_format = $date_formats{$format};
1778
1779   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
1780                                                },
1781                              'html'     => sub { return '<b>'. shift(). '</b>'
1782                                                },
1783                              'template' => sub { shift },
1784                            );
1785   my $embolden_function = $embolden_functions{$format};
1786
1787
1788   # generate template variables
1789   my $returnaddress;
1790   if (
1791          defined( $conf->config_orbase( "invoice_${format}returnaddress",
1792                                         $template
1793                                       )
1794                 )
1795        && length( $conf->config_orbase( "invoice_${format}returnaddress",
1796                                         $template
1797                                       )
1798                 )
1799   ) {
1800
1801     $returnaddress = join("\n",
1802       $conf->config_orbase("invoice_${format}returnaddress", $template)
1803     );
1804
1805   } elsif ( grep /\S/,
1806             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1807
1808     my $convert_map = $convert_maps{$format}{'returnaddress'};
1809     $returnaddress =
1810       join( "\n",
1811             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1812                                                  $template
1813                                                )
1814                          )
1815           );
1816   } elsif ( grep /\S/, $conf->config('company_address') ) {
1817
1818     my $convert_map = $convert_maps{$format}{'returnaddress'};
1819     $returnaddress = join( "\n", &$convert_map(
1820                                    map { s/( {2,})/'~' x length($1)/eg;
1821                                          s/$/\\\\\*/;
1822                                          $_
1823                                        }
1824                                      ( $conf->config('company_name'),
1825                                        $conf->config('company_address'),
1826                                      )
1827                                  )
1828                      );
1829
1830   } else {
1831
1832     my $warning = "Couldn't find a return address; ".
1833                   "do you need to set the company_address configuration value?";
1834     warn "$warning\n";
1835     $returnaddress = $nbsp;
1836     #$returnaddress = $warning;
1837
1838   }
1839
1840   my %invoice_data = (
1841     'company_name'    => scalar( $conf->config('company_name') ),
1842     'company_address' => join("\n", $conf->config('company_address') ). "\n",
1843     'custnum'         => $self->custnum,
1844     'invnum'          => $self->invnum,
1845     'date'            => time2str($date_format, $self->_date),
1846     'today'           => time2str('%b %o, %Y', $today),
1847     'agent'           => &$escape_function($cust_main->agent->agent),
1848     'agent_custid'    => &$escape_function($cust_main->agent_custid),
1849     'payname'         => &$escape_function($cust_main->payname),
1850     'company'         => &$escape_function($cust_main->company),
1851     'address1'        => &$escape_function($cust_main->address1),
1852     'address2'        => &$escape_function($cust_main->address2),
1853     'city'            => &$escape_function($cust_main->city),
1854     'state'           => &$escape_function($cust_main->state),
1855     'zip'             => &$escape_function($cust_main->zip),
1856     'returnaddress'   => $returnaddress,
1857     #'quantity'        => 1,
1858     'terms'           => $self->terms,
1859     'template'        => $params{'template'},
1860     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
1861     # better hang on to conf_dir for a while
1862     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1863     'page'            => 1,
1864     'total_pages'     => 1,
1865     'current_charges' => sprintf("%.2f", $self->charged),
1866     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
1867     'ship_enable'     => $conf->exists('invoice-ship_address'),
1868     'unitprices'      => $conf->exists('invoice-unitprice'),
1869   );
1870
1871   my $countrydefault = $conf->config('countrydefault') || 'US';
1872   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1873   foreach ( qw( contact company address1 address2 city state zip country fax) ){
1874     my $method = $prefix.$_;
1875     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1876   }
1877   $invoice_data{'ship_country'} = ''
1878     if ( $invoice_data{'ship_country'} eq $countrydefault );
1879   
1880   $invoice_data{'cid'} = $params{'cid'}
1881     if $params{'cid'};
1882
1883   if ( $cust_main->country eq $countrydefault ) {
1884     $invoice_data{'country'} = '';
1885   } else {
1886     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1887   }
1888
1889   my @address = ();
1890   $invoice_data{'address'} = \@address;
1891   push @address,
1892     $cust_main->payname.
1893       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1894         ? " (P.O. #". $cust_main->payinfo. ")"
1895         : ''
1896       )
1897   ;
1898   push @address, $cust_main->company
1899     if $cust_main->company;
1900   push @address, $cust_main->address1;
1901   push @address, $cust_main->address2
1902     if $cust_main->address2;
1903   push @address,
1904     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1905   push @address, $invoice_data{'country'}
1906     if $invoice_data{'country'};
1907   push @address, ''
1908     while (scalar(@address) < 5);
1909
1910   $invoice_data{'logo_file'} = $params{'logo_file'}
1911     if $params{'logo_file'};
1912
1913   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1914 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1915   #my $balance_due = $self->owed + $pr_total - $cr_total;
1916   my $balance_due = $self->owed + $pr_total;
1917   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1918   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1919
1920   #do variable substitution in notes, footer, smallfooter
1921   foreach my $include (qw( notes footer smallfooter coupon )) {
1922
1923     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1924     my @inc_src;
1925
1926     if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1927
1928       @inc_src = $conf->config($inc_file);
1929
1930     } else {
1931
1932       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1933
1934       my $convert_map = $convert_maps{$format}{$include};
1935
1936       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1937                        s/--\@\]/$delimiters{$format}[1]/g;
1938                        $_;
1939                      } 
1940                  &$convert_map( $conf->config($inc_file) );
1941
1942     }
1943
1944     my $inc_tt = new Text::Template (
1945       TYPE       => 'ARRAY',
1946       SOURCE     => [ map "$_\n", @inc_src ],
1947       DELIMITERS => $delimiters{$format},
1948     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1949
1950     unless ( $inc_tt->compile() ) {
1951       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1952       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1953       die $error;
1954     }
1955
1956     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1957
1958     $invoice_data{$include} =~ s/\n+$//
1959       if ($format eq 'latex');
1960   }
1961
1962   $invoice_data{'po_line'} =
1963     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1964       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1965       : $nbsp;
1966
1967   my %money_chars = ( 'latex'    => '',
1968                       'html'     => $conf->config('money_char') || '$',
1969                       'template' => '',
1970                     );
1971   my $money_char = $money_chars{$format};
1972
1973   my %other_money_chars = ( 'latex'    => '\dollar ',
1974                             'html'     => $conf->config('money_char') || '$',
1975                             'template' => '',
1976                           );
1977   my $other_money_char = $other_money_chars{$format};
1978
1979   my @detail_items = ();
1980   my @total_items = ();
1981   my @buf = ();
1982   my @sections = ();
1983
1984   $invoice_data{'detail_items'} = \@detail_items;
1985   $invoice_data{'total_items'} = \@total_items;
1986   $invoice_data{'buf'} = \@buf;
1987   $invoice_data{'sections'} = \@sections;
1988   
1989   my $previous_section = { 'description' => 'Previous Charges',
1990                            'subtotal'    => $other_money_char.
1991                                             sprintf('%.2f', $pr_total),
1992                          };
1993
1994   my $taxtotal = 0;
1995   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
1996                       'subtotal'    => $taxtotal }; # adjusted below
1997
1998   my $adjusttotal = 0;
1999   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2000                          'subtotal'    => 0 }; # adjusted below
2001
2002   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2003   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2004   my $late_sections = [];
2005   if ( $multisection ) {
2006     push @sections, $self->_items_sections( $late_sections );
2007   }else{
2008     push @sections, { 'description' => '', 'subtotal' => '' };
2009   }
2010
2011   foreach my $line_item ( $conf->exists('disable_previous_balance') 
2012                             ? ()
2013                             : $self->_items_previous
2014                         )
2015   {
2016     my $detail = {
2017       ext_description => [],
2018     };
2019     $detail->{'ref'} = $line_item->{'pkgnum'};
2020     $detail->{'quantity'} = 1;
2021     $detail->{'section'} = $previous_section;
2022     $detail->{'description'} = &$escape_function($line_item->{'description'});
2023     if ( exists $line_item->{'ext_description'} ) {
2024       @{$detail->{'ext_description'}} = map {
2025         &$escape_function($_);
2026       } @{$line_item->{'ext_description'}};
2027     }
2028     $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2029                           $line_item->{'amount'};
2030     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2031   
2032     push @detail_items, $detail;
2033     push @buf, [ $detail->{'description'},
2034                  $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2035                ];
2036   }
2037   
2038   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2039     push @buf, ['','-----------'];
2040     push @buf, [ 'Total Previous Balance',
2041                  $money_char. sprintf("%10.2f", $pr_total) ];
2042     push @buf, ['',''];
2043   }
2044
2045   foreach my $section (@sections, @$late_sections) {
2046
2047     $section->{'subtotal'} = $other_money_char.
2048                              sprintf('%.2f', $section->{'subtotal'})
2049       if $multisection;
2050
2051     if ( $section->{'description'} ) {
2052       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2053                    [ '', '' ],
2054                  );
2055     }
2056
2057     my %options = ();
2058     $options{'section'} = $section if $multisection;
2059     $options{'format'} = $format;
2060     $options{'escape_function'} = $escape_function;
2061     $options{'format_function'} = sub { () } unless $unsquelched;
2062     $options{'unsquelched'} = $unsquelched;
2063
2064     foreach my $line_item ( $self->_items_pkg(%options) ) {
2065       my $detail = {
2066         ext_description => [],
2067       };
2068       $detail->{'ref'} = $line_item->{'pkgnum'};
2069       $detail->{'quantity'} = $line_item->{'quantity'};
2070       $detail->{'section'} = $section;
2071       $detail->{'description'} = &$escape_function($line_item->{'description'});
2072       if ( exists $line_item->{'ext_description'} ) {
2073         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2074       }
2075       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2076                               $line_item->{'amount'};
2077       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2078                                  $line_item->{'unit_amount'};
2079       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2080   
2081       push @detail_items, $detail;
2082       push @buf, ( [ $detail->{'description'},
2083                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2084                    ],
2085                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2086                  );
2087     }
2088
2089     if ( $section->{'description'} ) {
2090       push @buf, ( ['','-----------'],
2091                    [ $section->{'description'}. ' sub-total',
2092                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2093                    ],
2094                    [ '', '' ],
2095                    [ '', '' ],
2096                  );
2097     }
2098   
2099   }
2100   
2101   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2102     unshift @sections, $previous_section if $pr_total;
2103   }
2104
2105   foreach my $tax ( $self->_items_tax ) {
2106     my $total = {};
2107     $total->{'total_item'} = &$escape_function($tax->{'description'});
2108     $taxtotal += $tax->{'amount'};
2109     $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2110     if ( $multisection ) {
2111       my $money = $old_latex ? '' : $money_char;
2112       push @detail_items, {
2113         ext_description => [],
2114         ref          => '',
2115         quantity     => '',
2116         description  => &$escape_function($tax->{'description'}),
2117         amount       => $money. $tax->{'amount'},
2118         product_code => '',
2119         section      => $tax_section,
2120       };
2121     }else{
2122       push @total_items, $total;
2123     }
2124     push @buf,[ $total->{'total_item'},
2125                 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2126               ];
2127
2128   }
2129   
2130   if ( $taxtotal ) {
2131     my $total = {};
2132     $total->{'total_item'} = 'Sub-total';
2133     $total->{'total_amount'} =
2134       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2135
2136     if ( $multisection ) {
2137       $tax_section->{'subtotal'} = $other_money_char.
2138                                    sprintf('%.2f', $taxtotal);
2139       $tax_section->{'pretotal'} = 'New charges sub-total '.
2140                                    $total->{'total_amount'};
2141       push @sections, $tax_section if $taxtotal;
2142     }else{
2143       unshift @total_items, $total;
2144     }
2145   }
2146   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2147   
2148   push @buf,['','-----------'];
2149   push @buf,[( $conf->exists('disable_previous_balance') 
2150                ? 'Total Charges'
2151                : 'Total New Charges'
2152              ),
2153              $money_char. sprintf("%10.2f",$self->charged) ];
2154   push @buf,['',''];
2155
2156   {
2157     my $total = {};
2158     $total->{'total_item'} = &$embolden_function('Total');
2159     $total->{'total_amount'} =
2160       &$embolden_function(
2161         $other_money_char.
2162         sprintf( '%.2f',
2163                  $self->charged + ( $conf->exists('disable_previous_balance')
2164                                     ? 0
2165                                     : $pr_total
2166                                   )
2167                )
2168       );
2169     if ( $multisection ) {
2170       $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2171                                       sprintf('%.2f', $self->charged );
2172     }else{
2173       push @total_items, $total;
2174     }
2175     push @buf,['','-----------'];
2176     push @buf,['Total Charges',
2177                $money_char.
2178                sprintf( '%10.2f', $self->charged +
2179                                     ( $conf->exists('disable_previous_balance')
2180                                         ? 0
2181                                         : $pr_total
2182                                     )
2183                       )
2184               ];
2185     push @buf,['',''];
2186   }
2187   
2188   unless ( $conf->exists('disable_previous_balance') ) {
2189     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2190   
2191     # credits
2192     my $credittotal = 0;
2193     foreach my $credit ( $self->_items_credits ) {
2194       my $total;
2195       $total->{'total_item'} = &$escape_function($credit->{'description'});
2196       $credittotal += $credit->{'amount'};
2197       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2198       $adjusttotal += $credit->{'amount'};
2199       if ( $multisection ) {
2200         my $money = $old_latex ? '' : $money_char;
2201         push @detail_items, {
2202           ext_description => [],
2203           ref          => '',
2204           quantity     => '',
2205           description  => &$escape_function($credit->{'description'}),
2206           amount       => $money. $credit->{'amount'},
2207           product_code => '',
2208           section      => $adjust_section,
2209         };
2210       }else{
2211         push @total_items, $total;
2212       }
2213     }
2214     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2215   
2216     # credits (again)
2217     foreach ( $self->cust_credited ) {
2218   
2219       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2220
2221       my $reason = substr($_->cust_credit->reason,0,32);
2222       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2223       $reason = " ($reason) " if $reason;
2224       push @buf,[
2225         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
2226         $money_char. sprintf("%10.2f",$_->amount)
2227       ];
2228     }
2229
2230     # payments
2231     my $paymenttotal = 0;
2232     foreach my $payment ( $self->_items_payments ) {
2233       my $total = {};
2234       $total->{'total_item'} = &$escape_function($payment->{'description'});
2235       $paymenttotal += $payment->{'amount'};
2236       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2237       $adjusttotal += $payment->{'amount'};
2238       if ( $multisection ) {
2239         my $money = $old_latex ? '' : $money_char;
2240         push @detail_items, {
2241           ext_description => [],
2242           ref          => '',
2243           quantity     => '',
2244           description  => &$escape_function($payment->{'description'}),
2245           amount       => $money. $payment->{'amount'},
2246           product_code => '',
2247           section      => $adjust_section,
2248         };
2249       }else{
2250         push @total_items, $total;
2251       }
2252       push @buf, [ $payment->{'description'},
2253                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2254                  ];
2255     }
2256     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2257   
2258     if ( $multisection ) {
2259       $adjust_section->{'subtotal'} = $other_money_char.
2260                                       sprintf('%.2f', $adjusttotal);
2261       push @sections, $adjust_section;
2262     }
2263
2264     { 
2265       my $total;
2266       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2267       $total->{'total_amount'} =
2268         &$embolden_function(
2269           $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2270         );
2271       if ( $multisection ) {
2272         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2273                                          $total->{'total_amount'};
2274       }else{
2275         push @total_items, $total;
2276       }
2277       push @buf,['','-----------'];
2278       push @buf,[$self->balance_due_msg, $money_char. 
2279         sprintf("%10.2f", $balance_due ) ];
2280     }
2281   }
2282
2283   if ( $multisection ) {
2284     push @sections, @$late_sections
2285       if $unsquelched;
2286   }
2287
2288   $invoice_lines = 0;
2289   my $wasfunc = 0;
2290   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2291     /invoice_lines\((\d*)\)/;
2292     $invoice_lines += $1 || scalar(@buf);
2293     $wasfunc=1;
2294   }
2295   die "no invoice_lines() functions in template?"
2296     if ( $format eq 'template' && !$wasfunc );
2297
2298   if ($format eq 'template') {
2299
2300     if ( $invoice_lines ) {
2301       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2302       $invoice_data{'total_pages'}++
2303         if scalar(@buf) % $invoice_lines;
2304     }
2305
2306     #setup subroutine for the template
2307     sub FS::cust_bill::_template::invoice_lines {
2308       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2309       map { 
2310         scalar(@FS::cust_bill::_template::buf)
2311           ? shift @FS::cust_bill::_template::buf
2312           : [ '', '' ];
2313       }
2314       ( 1 .. $lines );
2315     }
2316
2317     my $lines;
2318     my @collect;
2319     while (@buf) {
2320       push @collect, split("\n",
2321         $text_template->fill_in( HASH => \%invoice_data,
2322                                  PACKAGE => 'FS::cust_bill::_template'
2323                                )
2324       );
2325       $FS::cust_bill::_template::page++;
2326     }
2327     map "$_\n", @collect;
2328   }else{
2329     warn "filling in template for invoice ". $self->invnum. "\n"
2330       if $DEBUG;
2331     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2332       if $DEBUG > 1;
2333
2334     $text_template->fill_in(HASH => \%invoice_data);
2335   }
2336 }
2337
2338 =item print_ps [ TIME [ , TEMPLATE ] ]
2339
2340 Returns an postscript invoice, as a scalar.
2341
2342 TIME an optional value used to control the printing of overdue messages.  The
2343 default is now.  It isn't the date of the invoice; that's the `_date' field.
2344 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2345 L<Time::Local> and L<Date::Parse> for conversion functions.
2346
2347 =cut
2348
2349 sub print_ps {
2350   my $self = shift;
2351
2352   my ($file, $lfile) = $self->print_latex(@_);
2353   my $ps = generate_ps($file);
2354   unlink($lfile);
2355
2356   $ps;
2357 }
2358
2359 =item print_pdf [ TIME [ , TEMPLATE ] ]
2360
2361 Returns an PDF invoice, as a scalar.
2362
2363 TIME an optional value used to control the printing of overdue messages.  The
2364 default is now.  It isn't the date of the invoice; that's the `_date' field.
2365 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2366 L<Time::Local> and L<Date::Parse> for conversion functions.
2367
2368 =cut
2369
2370 sub print_pdf {
2371   my $self = shift;
2372
2373   my ($file, $lfile) = $self->print_latex(@_);
2374   my $pdf = generate_pdf($file);
2375   unlink($lfile);
2376
2377   $pdf;
2378 }
2379
2380 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2381
2382 Returns an HTML invoice, as a scalar.
2383
2384 TIME an optional value used to control the printing of overdue messages.  The
2385 default is now.  It isn't the date of the invoice; that's the `_date' field.
2386 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2387 L<Time::Local> and L<Date::Parse> for conversion functions.
2388
2389 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2390 when emailing the invoice as part of a multipart/related MIME email.
2391
2392 =cut
2393
2394 sub print_html {
2395   my( $self, $today, $template, $cid ) = @_;
2396
2397   my %params = ( 'format' => 'html' );
2398   $params{'time'} = $today if $today;
2399   $params{'template'} = $template if $template;
2400   $params{'cid'} = $cid if $cid;
2401
2402   $self->print_generic( %params );
2403 }
2404
2405 # quick subroutine for print_latex
2406 #
2407 # There are ten characters that LaTeX treats as special characters, which
2408 # means that they do not simply typeset themselves: 
2409 #      # $ % & ~ _ ^ \ { }
2410 #
2411 # TeX ignores blanks following an escaped character; if you want a blank (as
2412 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2413
2414 sub _latex_escape {
2415   my $value = shift;
2416   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2417   $value =~ s/([<>])/\$$1\$/g;
2418   $value;
2419 }
2420
2421 #utility methods for print_*
2422
2423 sub _translate_old_latex_format {
2424   warn "_translate_old_latex_format called\n"
2425     if $DEBUG; 
2426
2427   my @template = ();
2428   while ( @_ ) {
2429     my $line = shift;
2430   
2431     if ( $line =~ /^%%Detail\s*$/ ) {
2432   
2433       push @template, q![@--!,
2434                       q!  foreach my $_tr_line (@detail_items) {!,
2435                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2436                       q!      $_tr_line->{'description'} .= !, 
2437                       q!        "\\tabularnewline\n~~".!,
2438                       q!        join( "\\tabularnewline\n~~",!,
2439                       q!          @{$_tr_line->{'ext_description'}}!,
2440                       q!        );!,
2441                       q!    }!;
2442
2443       while ( ( my $line_item_line = shift )
2444               !~ /^%%EndDetail\s*$/                            ) {
2445         $line_item_line =~ s/'/\\'/g;    # nice LTS
2446         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2447         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2448         push @template, "    \$OUT .= '$line_item_line';";
2449       }
2450   
2451       push @template, '}',
2452                       '--@]';
2453
2454     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2455
2456       push @template, '[@--',
2457                       '  foreach my $_tr_line (@total_items) {';
2458
2459       while ( ( my $total_item_line = shift )
2460               !~ /^%%EndTotalDetails\s*$/                      ) {
2461         $total_item_line =~ s/'/\\'/g;    # nice LTS
2462         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2463         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2464         push @template, "    \$OUT .= '$total_item_line';";
2465       }
2466
2467       push @template, '}',
2468                       '--@]';
2469
2470     } else {
2471       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2472       push @template, $line;  
2473     }
2474   
2475   }
2476
2477   if ($DEBUG) {
2478     warn "$_\n" foreach @template;
2479   }
2480
2481   (@template);
2482 }
2483
2484 sub terms {
2485   my $self = shift;
2486
2487   #check for an invoice- specific override (eventually)
2488   
2489   #check for a customer- specific override
2490   return $self->cust_main->invoice_terms
2491     if $self->cust_main->invoice_terms;
2492
2493   #use configured default or default default
2494   $conf->config('invoice_default_terms') || 'Payable upon receipt';
2495 }
2496
2497 sub due_date {
2498   my $self = shift;
2499   my $duedate = '';
2500   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2501     $duedate = $self->_date() + ( $1 * 86400 );
2502   }
2503   $duedate;
2504 }
2505
2506 sub due_date2str {
2507   my $self = shift;
2508   $self->due_date ? time2str(shift, $self->due_date) : '';
2509 }
2510
2511 sub balance_due_msg {
2512   my $self = shift;
2513   my $msg = 'Balance Due';
2514   return $msg unless $self->terms;
2515   if ( $self->due_date ) {
2516     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2517   } elsif ( $self->terms ) {
2518     $msg .= ' - '. $self->terms;
2519   }
2520   $msg;
2521 }
2522
2523 sub balance_due_date {
2524   my $self = shift;
2525   my $duedate = '';
2526   if (    $conf->exists('invoice_default_terms') 
2527        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2528     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2529   }
2530   $duedate;
2531 }
2532
2533 =item invnum_date_pretty
2534
2535 Returns a string with the invoice number and date, for example:
2536 "Invoice #54 (3/20/2008)"
2537
2538 =cut
2539
2540 sub invnum_date_pretty {
2541   my $self = shift;
2542   'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2543 }
2544
2545 sub _items_sections {
2546   my $self = shift;
2547   my $late = shift;
2548
2549   my %s = ();
2550   my %l = ();
2551
2552   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2553   {
2554
2555     if ( $cust_bill_pkg->pkgnum > 0 ) {
2556
2557       my $desc = $cust_bill_pkg->section;
2558       my $dup_desc = $cust_bill_pkg->duplicate_section;
2559
2560       if ($cust_bill_pkg->duplicate) {
2561         $s{$dup_desc} += $cust_bill_pkg->setup
2562           if ( $cust_bill_pkg->setup != 0 );
2563
2564         $s{$dup_desc} += $cust_bill_pkg->recur
2565           if ( $cust_bill_pkg->recur != 0 );
2566       }
2567
2568       if ( $cust_bill_pkg->post_total ) {
2569         $l{$desc} += $cust_bill_pkg->setup
2570           if ( $cust_bill_pkg->setup != 0 );
2571
2572         $l{$desc} += $cust_bill_pkg->recur
2573           if ( $cust_bill_pkg->recur != 0 );
2574
2575       } else {
2576         $s{$desc} += $cust_bill_pkg->setup
2577           if ( $cust_bill_pkg->setup != 0 );
2578
2579         $s{$desc} += $cust_bill_pkg->recur
2580           if ( $cust_bill_pkg->recur != 0 );
2581       }
2582
2583     }
2584
2585   }
2586
2587   push @$late, map { { 'description' => $_,
2588                        'subtotal'    => $l{$_},
2589                        'post_total'  => 1,
2590                    } } sort keys %l;
2591
2592   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2593
2594 }
2595
2596 sub _items {
2597   my $self = shift;
2598
2599   #my @display = scalar(@_)
2600   #              ? @_
2601   #              : qw( _items_previous _items_pkg );
2602   #              #: qw( _items_pkg );
2603   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2604   my @display = qw( _items_previous _items_pkg );
2605
2606   my @b = ();
2607   foreach my $display ( @display ) {
2608     push @b, $self->$display(@_);
2609   }
2610   @b;
2611 }
2612
2613 sub _items_previous {
2614   my $self = shift;
2615   my $cust_main = $self->cust_main;
2616   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2617   my @b = ();
2618   foreach ( @pr_cust_bill ) {
2619     push @b, {
2620       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2621                        ' ('. time2str('%x',$_->_date). ')',
2622       #'pkgpart'     => 'N/A',
2623       'pkgnum'      => 'N/A',
2624       'amount'      => sprintf("%.2f", $_->owed),
2625     };
2626   }
2627   @b;
2628
2629   #{
2630   #    'description'     => 'Previous Balance',
2631   #    #'pkgpart'         => 'N/A',
2632   #    'pkgnum'          => 'N/A',
2633   #    'amount'          => sprintf("%10.2f", $pr_total ),
2634   #    'ext_description' => [ map {
2635   #                                 "Invoice ". $_->invnum.
2636   #                                 " (". time2str("%x",$_->_date). ") ".
2637   #                                 sprintf("%10.2f", $_->owed)
2638   #                         } @pr_cust_bill ],
2639
2640   #};
2641 }
2642
2643 sub _items_pkg {
2644   my $self = shift;
2645   my %options = @_;
2646   my $section = $options{'section'};
2647   my $desc = $section->{'description'};
2648   my @cust_bill_pkg =
2649     grep { $_->pkgnum &&
2650            ( defined($section)
2651                ? ( $_->section eq $desc || $_->duplicate_section eq $desc )
2652                : 1
2653            )
2654          } $self->cust_bill_pkg;
2655   $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2656 }
2657
2658 sub _taxsort {
2659   return 0 unless $a cmp $b;
2660   return -1 if $b eq 'Tax';
2661   return 1 if $a eq 'Tax';
2662   return -1 if $b eq 'Other surcharges';
2663   return 1 if $a eq 'Other surcharges';
2664   $a cmp $b;
2665 }
2666
2667 sub _items_tax {
2668   my $self = shift;
2669   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2670   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2671 }
2672
2673 sub _items_cust_bill_pkg {
2674   my $self = shift;
2675   my $cust_bill_pkg = shift;
2676   my %opt = @_;
2677
2678   my $format = $opt{format} || '';
2679   my $escape_function = $opt{escape_function} || sub { shift };
2680   my $format_function = $opt{format_function} || '';
2681   my $unsquelched = $opt{unsquelched} || '';
2682
2683   my @b = ();
2684   my $last_pkgnum = '';
2685   foreach my $cust_bill_pkg ( grep { $unsquelched ? 1 : ! $_->separate_cdr }
2686                               @$cust_bill_pkg
2687                             )
2688   {
2689
2690     my $cust_pkg = $cust_bill_pkg->cust_pkg;
2691
2692     my $desc = $cust_bill_pkg->desc;
2693
2694     my %details_opt = ( 'format'          => $format,
2695                         'escape_function' => $escape_function,
2696                         'format_function' => $format_function,
2697                       );
2698
2699     if ( $cust_bill_pkg->pkgnum > 0 ) {
2700
2701       if ( $cust_bill_pkg->setup != 0 ) {
2702
2703         my $description = $desc;
2704         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2705
2706         my @d = map &{$escape_function}($_),
2707                        $cust_pkg->h_labels_short($self->_date);
2708         push @d, $cust_bill_pkg->details(%details_opt)
2709           if $cust_bill_pkg->recur == 0;
2710
2711         push @b, {
2712           description     => $description,
2713           #pkgpart         => $part_pkg->pkgpart,
2714           pkgnum          => $cust_bill_pkg->pkgnum,
2715           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2716           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2717           quantity        => $cust_bill_pkg->quantity,
2718           ext_description => \@d,
2719         };
2720
2721         $last_pkgnum = '';
2722
2723       }
2724
2725       if ( $cust_bill_pkg->recur != 0 ) {
2726
2727         my $is_summary =
2728           ( $cust_bill_pkg->duplicate && 
2729             $opt{section}->{description} ne $cust_bill_pkg->section
2730           );
2731         my $description = $is_summary ? "Usage charges" : $desc;
2732
2733         unless ( $conf->exists('disable_line_item_date_ranges') ) {
2734           $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2735                           " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2736         }
2737
2738         #at least until cust_bill_pkg has "past" ranges in addition to
2739         #the "future" sdate/edate ones... see #3032
2740         my @d = ();
2741         push @d, map &{$escape_function}($_),
2742                        $cust_pkg->h_labels_short($self->_date)
2743                                               #$cust_bill_pkg->edate,
2744                                               #$cust_bill_pkg->sdate),
2745           unless ($cust_bill_pkg->pkgnum eq $last_pkgnum);
2746
2747         @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2748         push @d, $cust_bill_pkg->details(%details_opt)
2749           unless $is_summary;
2750
2751         if ($cust_bill_pkg->pkgnum eq $last_pkgnum) {
2752
2753           $b[$#b]->{amount} =
2754             sprintf("%.2f", $b[$#b]->{amount} + $cust_bill_pkg->recur);
2755           push @{$b[$#b]->{ext_description}}, @d;
2756
2757         }else{
2758
2759           push @b, {
2760             description     => $description,
2761             #pkgpart         => $part_pkg->pkgpart,
2762             pkgnum          => $cust_bill_pkg->pkgnum,
2763             amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2764             unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2765             quantity        => $cust_bill_pkg->quantity,
2766             ext_description => \@d,
2767           };
2768
2769         }
2770
2771         if ($conf->exists('separate_usage') && $cust_bill_pkg->type ne 'U') {
2772           $last_pkgnum = '';
2773         }else{
2774           $last_pkgnum = $cust_bill_pkg->pkgnum;
2775         }
2776       }
2777
2778     } else { #pkgnum tax or one-shot line item (??)
2779
2780       if ( $cust_bill_pkg->setup != 0 ) {
2781         push @b, {
2782           'description' => $desc,
2783           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2784         };
2785       }
2786       if ( $cust_bill_pkg->recur != 0 ) {
2787         push @b, {
2788           'description' => "$desc (".
2789                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2790                            time2str("%x", $cust_bill_pkg->edate). ')',
2791           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2792         };
2793       }
2794
2795       $last_pkgnum = '';
2796
2797     }
2798
2799   }
2800
2801   @b;
2802
2803 }
2804
2805 sub _items_credits {
2806   my $self = shift;
2807
2808   my @b;
2809   #credits
2810   foreach ( $self->cust_credited ) {
2811
2812     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2813
2814     my $reason = $_->cust_credit->reason;
2815     #my $reason = substr($_->cust_credit->reason,0,32);
2816     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2817     $reason = " ($reason) " if $reason;
2818     push @b, {
2819       #'description' => 'Credit ref\#'. $_->crednum.
2820       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2821       #                 $reason,
2822       'description' => 'Credit applied '.
2823                        time2str("%x",$_->cust_credit->_date). $reason,
2824       'amount'      => sprintf("%.2f",$_->amount),
2825     };
2826   }
2827   #foreach ( @cr_cust_credit ) {
2828   #  push @buf,[
2829   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2830   #    $money_char. sprintf("%10.2f",$_->credited)
2831   #  ];
2832   #}
2833
2834   @b;
2835
2836 }
2837
2838 sub _items_payments {
2839   my $self = shift;
2840
2841   my @b;
2842   #get & print payments
2843   foreach ( $self->cust_bill_pay ) {
2844
2845     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2846
2847     push @b, {
2848       'description' => "Payment received ".
2849                        time2str("%x",$_->cust_pay->_date ),
2850       'amount'      => sprintf("%.2f", $_->amount )
2851     };
2852   }
2853
2854   @b;
2855
2856 }
2857
2858
2859 =back
2860
2861 =head1 SUBROUTINES
2862
2863 =over 4
2864
2865 =item reprint
2866
2867 =cut
2868
2869 sub process_reprint {
2870   process_re_X('print', @_);
2871 }
2872
2873 =item reemail
2874
2875 =cut
2876
2877 sub process_reemail {
2878   process_re_X('email', @_);
2879 }
2880
2881 =item refax
2882
2883 =cut
2884
2885 sub process_refax {
2886   process_re_X('fax', @_);
2887 }
2888
2889 use Storable qw(thaw);
2890 use Data::Dumper;
2891 use MIME::Base64;
2892 sub process_re_X {
2893   my( $method, $job ) = ( shift, shift );
2894   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2895
2896   my $param = thaw(decode_base64(shift));
2897   warn Dumper($param) if $DEBUG;
2898
2899   re_X(
2900     $method,
2901     $job,
2902     %$param,
2903   );
2904
2905 }
2906
2907 sub re_X {
2908   my($method, $job, %param ) = @_;
2909   if ( $DEBUG ) {
2910     warn "re_X $method for job $job with param:\n".
2911          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2912   }
2913
2914   #some false laziness w/search/cust_bill.html
2915   my $distinct = '';
2916   my $orderby = 'ORDER BY cust_bill._date';
2917
2918   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2919
2920   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2921      
2922   my @cust_bill = qsearch( {
2923     #'select'    => "cust_bill.*",
2924     'table'     => 'cust_bill',
2925     'addl_from' => $addl_from,
2926     'hashref'   => {},
2927     'extra_sql' => $extra_sql,
2928     'order_by'  => $orderby,
2929     'debug' => 1,
2930   } );
2931
2932   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2933     if $DEBUG;
2934
2935   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2936   foreach my $cust_bill ( @cust_bill ) {
2937     $cust_bill->$method();
2938
2939     if ( $job ) { #progressbar foo
2940       $num++;
2941       if ( time - $min_sec > $last ) {
2942         my $error = $job->update_statustext(
2943           int( 100 * $num / scalar(@cust_bill) )
2944         );
2945         die $error if $error;
2946         $last = time;
2947       }
2948     }
2949
2950   }
2951
2952 }
2953
2954 =back
2955
2956 =head1 CLASS METHODS
2957
2958 =over 4
2959
2960 =item owed_sql
2961
2962 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2963
2964 =cut
2965
2966 sub owed_sql {
2967   my $class = shift;
2968   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2969 }
2970
2971 =item net_sql
2972
2973 Returns an SQL fragment to retreive the net amount (charged minus credited).
2974
2975 =cut
2976
2977 sub net_sql {
2978   my $class = shift;
2979   'charged - '. $class->credited_sql;
2980 }
2981
2982 =item paid_sql
2983
2984 Returns an SQL fragment to retreive the amount paid against this invoice.
2985
2986 =cut
2987
2988 sub paid_sql {
2989   #my $class = shift;
2990   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2991        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
2992 }
2993
2994 =item credited_sql
2995
2996 Returns an SQL fragment to retreive the amount credited against this invoice.
2997
2998 =cut
2999
3000 sub credited_sql {
3001   #my $class = shift;
3002   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3003        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
3004 }
3005
3006 =item search_sql HASHREF
3007
3008 Class method which returns an SQL WHERE fragment to search for parameters
3009 specified in HASHREF.  Valid parameters are
3010
3011 =over 4
3012
3013 =item begin
3014
3015 Epoch date (UNIX timestamp) setting a lower bound for _date values
3016
3017 =item end
3018
3019 Epoch date (UNIX timestamp) setting an upper bound for _date values
3020
3021 =item invnum_min
3022
3023 =item invnum_max
3024
3025 =item agentnum
3026
3027 =item owed
3028
3029 =item net
3030
3031 =item days
3032
3033 =item newest_percust
3034
3035 =back
3036
3037 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3038
3039 =cut
3040
3041 sub search_sql {
3042   my($class, $param) = @_;
3043   if ( $DEBUG ) {
3044     warn "$me search_sql called with params: \n".
3045          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3046   }
3047
3048   my @search = ();
3049
3050   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3051     push @search, "cust_bill._date >= $1";
3052   }
3053   if ( $param->{'end'} =~ /^(\d+)$/ ) {
3054     push @search, "cust_bill._date < $1";
3055   }
3056   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3057     push @search, "cust_bill.invnum >= $1";
3058   }
3059   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3060     push @search, "cust_bill.invnum <= $1";
3061   }
3062   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3063     push @search, "cust_main.agentnum = $1";
3064   }
3065
3066   push @search, '0 != '. FS::cust_bill->owed_sql
3067     if $param->{'open'};
3068
3069   push @search, '0 != '. FS::cust_bill->net_sql
3070     if $param->{'net'};
3071
3072   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3073     if $param->{'days'};
3074
3075   if ( $param->{'newest_percust'} ) {
3076
3077     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3078     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3079
3080     my @newest_where = map { my $x = $_;
3081                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3082                              $x;
3083                            }
3084                            grep ! /^cust_main./, @search;
3085     my $newest_where = scalar(@newest_where)
3086                          ? ' AND '. join(' AND ', @newest_where)
3087                          : '';
3088
3089
3090     push @search, "cust_bill._date = (
3091       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3092         WHERE newest_cust_bill.custnum = cust_bill.custnum
3093           $newest_where
3094     )";
3095
3096   }
3097
3098   my $curuser = $FS::CurrentUser::CurrentUser;
3099   if ( $curuser->username eq 'fs_queue'
3100        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3101     my $username = $1;
3102     my $newuser = qsearchs('access_user', {
3103       'username' => $username,
3104       'disabled' => '',
3105     } );
3106     if ( $newuser ) {
3107       $curuser = $newuser;
3108     } else {
3109       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3110     }
3111   }
3112
3113   push @search, $curuser->agentnums_sql;
3114
3115   join(' AND ', @search );
3116
3117 }
3118
3119 =back
3120
3121 =head1 BUGS
3122
3123 The delete method.
3124
3125 =head1 SEE ALSO
3126
3127 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3128 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3129 documentation.
3130
3131 =cut
3132
3133 1;
3134