add billco respooling, not re-FTPing, RT#3971
[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 ftp_invoice [ TEMPLATENAME ] 
978
979 Sends this invoice data via FTP.
980
981 TEMPLATENAME is unused?
982
983 =cut
984
985 sub ftp_invoice {
986   my $self = shift;
987   my $template = scalar(@_) ? shift : '';
988
989   $self->send_csv(
990     'protocol'   => 'ftp',
991     'server'     => $conf->config('cust_bill-ftpserver'),
992     'username'   => $conf->config('cust_bill-ftpusername'),
993     'password'   => $conf->config('cust_bill-ftppassword'),
994     'dir'        => $conf->config('cust_bill-ftpdir'),
995     'format'     => $conf->config('cust_bill-ftpformat'),
996   );
997 }
998
999 =item spool_invoice [ TEMPLATENAME ] 
1000
1001 Spools this invoice data (see L<FS::spool_csv>)
1002
1003 TEMPLATENAME is unused?
1004
1005 =cut
1006
1007 sub spool_invoice {
1008   my $self = shift;
1009   my $template = scalar(@_) ? shift : '';
1010
1011   $self->spool_csv(
1012     'format'       => $conf->config('cust_bill-spoolformat'),
1013     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1014   );
1015 }
1016
1017 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1018
1019 Like B<send>, but only sends the invoice if it is the newest open invoice for
1020 this customer.
1021
1022 =cut
1023
1024 sub send_if_newest {
1025   my $self = shift;
1026
1027   return ''
1028     if scalar(
1029                grep { $_->owed > 0 } 
1030                     qsearch('cust_bill', {
1031                       'custnum' => $self->custnum,
1032                       #'_date'   => { op=>'>', value=>$self->_date },
1033                       'invnum'  => { op=>'>', value=>$self->invnum },
1034                     } )
1035              );
1036     
1037   $self->send(@_);
1038 }
1039
1040 =item send_csv OPTION => VALUE, ...
1041
1042 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1043
1044 Options are:
1045
1046 protocol - currently only "ftp"
1047 server
1048 username
1049 password
1050 dir
1051
1052 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1053 and YYMMDDHHMMSS is a timestamp.
1054
1055 See L</print_csv> for a description of the output format.
1056
1057 =cut
1058
1059 sub send_csv {
1060   my($self, %opt) = @_;
1061
1062   #create file(s)
1063
1064   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1065   mkdir $spooldir, 0700 unless -d $spooldir;
1066
1067   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1068   my $file = "$spooldir/$tracctnum.csv";
1069   
1070   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1071
1072   open(CSV, ">$file") or die "can't open $file: $!";
1073   print CSV $header;
1074
1075   print CSV $detail;
1076
1077   close CSV;
1078
1079   my $net;
1080   if ( $opt{protocol} eq 'ftp' ) {
1081     eval "use Net::FTP;";
1082     die $@ if $@;
1083     $net = Net::FTP->new($opt{server}) or die @$;
1084   } else {
1085     die "unknown protocol: $opt{protocol}";
1086   }
1087
1088   $net->login( $opt{username}, $opt{password} )
1089     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1090
1091   $net->binary or die "can't set binary mode";
1092
1093   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1094
1095   $net->put($file) or die "can't put $file: $!";
1096
1097   $net->quit;
1098
1099   unlink $file;
1100
1101 }
1102
1103 =item spool_csv
1104
1105 Spools CSV invoice data.
1106
1107 Options are:
1108
1109 =over 4
1110
1111 =item format - 'default' or 'billco'
1112
1113 =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>).
1114
1115 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1116
1117 =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.
1118
1119 =back
1120
1121 =cut
1122
1123 sub spool_csv {
1124   my($self, %opt) = @_;
1125
1126   my $cust_main = $self->cust_main;
1127
1128   if ( $opt{'dest'} ) {
1129     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1130                              $cust_main->invoicing_list;
1131     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1132                      || ! keys %invoicing_list;
1133   }
1134
1135   if ( $opt{'balanceover'} ) {
1136     return 'N/A'
1137       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1138   }
1139
1140   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1141   mkdir $spooldir, 0700 unless -d $spooldir;
1142
1143   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1144
1145   my $file =
1146     "$spooldir/".
1147     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1148     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1149     '.csv';
1150   
1151   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1152
1153   open(CSV, ">>$file") or die "can't open $file: $!";
1154   flock(CSV, LOCK_EX);
1155   seek(CSV, 0, 2);
1156
1157   print CSV $header;
1158
1159   if ( lc($opt{'format'}) eq 'billco' ) {
1160
1161     flock(CSV, LOCK_UN);
1162     close CSV;
1163
1164     $file =
1165       "$spooldir/".
1166       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1167       '-detail.csv';
1168
1169     open(CSV,">>$file") or die "can't open $file: $!";
1170     flock(CSV, LOCK_EX);
1171     seek(CSV, 0, 2);
1172   }
1173
1174   print CSV $detail;
1175
1176   flock(CSV, LOCK_UN);
1177   close CSV;
1178
1179   return '';
1180
1181 }
1182
1183 =item print_csv OPTION => VALUE, ...
1184
1185 Returns CSV data for this invoice.
1186
1187 Options are:
1188
1189 format - 'default' or 'billco'
1190
1191 Returns a list consisting of two scalars.  The first is a single line of CSV
1192 header information for this invoice.  The second is one or more lines of CSV
1193 detail information for this invoice.
1194
1195 If I<format> is not specified or "default", the fields of the CSV file are as
1196 follows:
1197
1198 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1199
1200 =over 4
1201
1202 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1203
1204 B<record_type> is C<cust_bill> for the initial header line only.  The
1205 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1206 fields are filled in.
1207
1208 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1209 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1210 are filled in.
1211
1212 =item invnum - invoice number
1213
1214 =item custnum - customer number
1215
1216 =item _date - invoice date
1217
1218 =item charged - total invoice amount
1219
1220 =item first - customer first name
1221
1222 =item last - customer first name
1223
1224 =item company - company name
1225
1226 =item address1 - address line 1
1227
1228 =item address2 - address line 1
1229
1230 =item city
1231
1232 =item state
1233
1234 =item zip
1235
1236 =item country
1237
1238 =item pkg - line item description
1239
1240 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1241
1242 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1243
1244 =item sdate - start date for recurring fee
1245
1246 =item edate - end date for recurring fee
1247
1248 =back
1249
1250 If I<format> is "billco", the fields of the header CSV file are as follows:
1251
1252   +-------------------------------------------------------------------+
1253   |                        FORMAT HEADER FILE                         |
1254   |-------------------------------------------------------------------|
1255   | Field | Description                   | Name       | Type | Width |
1256   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1257   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1258   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1259   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1260   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1261   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1262   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1263   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1264   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1265   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1266   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1267   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1268   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1269   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1270   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1271   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1272   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1273   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1274   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1275   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1276   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1277   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1278   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1279   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1280   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1281   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1282   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1283   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1284   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1285   +-------+-------------------------------+------------+------+-------+
1286
1287 If I<format> is "billco", the fields of the detail CSV file are as follows:
1288
1289                                   FORMAT FOR DETAIL FILE
1290         |                            |           |      |
1291   Field | Description                | Name      | Type | Width
1292   1     | N/A-Leave Empty            | RC        | CHAR |     2
1293   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1294   3     | Account Number             | TRACCTNUM | CHAR |    15
1295   4     | Invoice Number             | TRINVOICE | CHAR |    15
1296   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1297   6     | Transaction Detail         | DETAILS   | CHAR |   100
1298   7     | Amount                     | AMT       | NUM* |     9
1299   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1300   9     | Grouping Code              | GROUP     | CHAR |     2
1301   10    | User Defined               | ACCT CODE | CHAR |    15
1302
1303 =cut
1304
1305 sub print_csv {
1306   my($self, %opt) = @_;
1307   
1308   eval "use Text::CSV_XS";
1309   die $@ if $@;
1310
1311   my $cust_main = $self->cust_main;
1312
1313   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1314
1315   if ( lc($opt{'format'}) eq 'billco' ) {
1316
1317     my $taxtotal = 0;
1318     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1319
1320     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1321
1322     my( $previous_balance, @unused ) = $self->previous; #previous balance
1323
1324     my $pmt_cr_applied = 0;
1325     $pmt_cr_applied += $_->{'amount'}
1326       foreach ( $self->_items_payments, $self->_items_credits ) ;
1327
1328     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1329
1330     $csv->combine(
1331       '',                         #  1 | N/A-Leave Empty               CHAR   2
1332       '',                         #  2 | N/A-Leave Empty               CHAR  15
1333       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1334       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1335       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1336       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1337       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1338       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1339       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1340       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1341       '',                         # 10 | Ancillary Billing Information CHAR  30
1342       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1343       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1344
1345       # XXX ?
1346       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1347
1348       # XXX ?
1349       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1350
1351       $previous_balance,          # 15 | Previous Balance              NUM*   9
1352       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1353       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1354       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1355       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1356       '',                         # 20 | 30 Day Aging                  NUM*   9
1357       '',                         # 21 | 60 Day Aging                  NUM*   9
1358       '',                         # 22 | 90 Day Aging                  NUM*   9
1359       'N',                        # 23 | Y/N                           CHAR   1
1360       '',                         # 24 | Remittance automation         CHAR 100
1361       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1362       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1363       '0',                        # 27 | Federal Tax***                NUM*   9
1364       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1365       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1366     );
1367
1368   } else {
1369   
1370     $csv->combine(
1371       'cust_bill',
1372       $self->invnum,
1373       $self->custnum,
1374       time2str("%x", $self->_date),
1375       sprintf("%.2f", $self->charged),
1376       ( map { $cust_main->getfield($_) }
1377           qw( first last company address1 address2 city state zip country ) ),
1378       map { '' } (1..5),
1379     ) or die "can't create csv";
1380   }
1381
1382   my $header = $csv->string. "\n";
1383
1384   my $detail = '';
1385   if ( lc($opt{'format'}) eq 'billco' ) {
1386
1387     my $lineseq = 0;
1388     foreach my $item ( $self->_items_pkg ) {
1389
1390       $csv->combine(
1391         '',                     #  1 | N/A-Leave Empty            CHAR   2
1392         '',                     #  2 | N/A-Leave Empty            CHAR  15
1393         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1394         $self->invnum,          #  4 | Invoice Number             CHAR  15
1395         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1396         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1397         $item->{'amount'},      #  7 | Amount                     NUM*   9
1398         '',                     #  8 | Line Format Control**      CHAR   2
1399         '',                     #  9 | Grouping Code              CHAR   2
1400         '',                     # 10 | User Defined               CHAR  15
1401       );
1402
1403       $detail .= $csv->string. "\n";
1404
1405     }
1406
1407   } else {
1408
1409     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1410
1411       my($pkg, $setup, $recur, $sdate, $edate);
1412       if ( $cust_bill_pkg->pkgnum ) {
1413       
1414         ($pkg, $setup, $recur, $sdate, $edate) = (
1415           $cust_bill_pkg->part_pkg->pkg,
1416           ( $cust_bill_pkg->setup != 0
1417             ? sprintf("%.2f", $cust_bill_pkg->setup )
1418             : '' ),
1419           ( $cust_bill_pkg->recur != 0
1420             ? sprintf("%.2f", $cust_bill_pkg->recur )
1421             : '' ),
1422           ( $cust_bill_pkg->sdate 
1423             ? time2str("%x", $cust_bill_pkg->sdate)
1424             : '' ),
1425           ($cust_bill_pkg->edate 
1426             ?time2str("%x", $cust_bill_pkg->edate)
1427             : '' ),
1428         );
1429   
1430       } else { #pkgnum tax
1431         next unless $cust_bill_pkg->setup != 0;
1432         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1433                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1434                          : 'Tax';
1435         ($pkg, $setup, $recur, $sdate, $edate) =
1436           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1437       }
1438   
1439       $csv->combine(
1440         'cust_bill_pkg',
1441         $self->invnum,
1442         ( map { '' } (1..11) ),
1443         ($pkg, $setup, $recur, $sdate, $edate)
1444       ) or die "can't create csv";
1445
1446       $detail .= $csv->string. "\n";
1447
1448     }
1449
1450   }
1451
1452   ( $header, $detail );
1453
1454 }
1455
1456 =item comp
1457
1458 Pays this invoice with a compliemntary payment.  If there is an error,
1459 returns the error, otherwise returns false.
1460
1461 =cut
1462
1463 sub comp {
1464   my $self = shift;
1465   my $cust_pay = new FS::cust_pay ( {
1466     'invnum'   => $self->invnum,
1467     'paid'     => $self->owed,
1468     '_date'    => '',
1469     'payby'    => 'COMP',
1470     'payinfo'  => $self->cust_main->payinfo,
1471     'paybatch' => '',
1472   } );
1473   $cust_pay->insert;
1474 }
1475
1476 =item realtime_card
1477
1478 Attempts to pay this invoice with a credit card payment via a
1479 Business::OnlinePayment realtime gateway.  See
1480 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1481 for supported processors.
1482
1483 =cut
1484
1485 sub realtime_card {
1486   my $self = shift;
1487   $self->realtime_bop( 'CC', @_ );
1488 }
1489
1490 =item realtime_ach
1491
1492 Attempts to pay this invoice with an electronic check (ACH) payment via a
1493 Business::OnlinePayment realtime gateway.  See
1494 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1495 for supported processors.
1496
1497 =cut
1498
1499 sub realtime_ach {
1500   my $self = shift;
1501   $self->realtime_bop( 'ECHECK', @_ );
1502 }
1503
1504 =item realtime_lec
1505
1506 Attempts to pay this invoice with phone bill (LEC) payment via a
1507 Business::OnlinePayment realtime gateway.  See
1508 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1509 for supported processors.
1510
1511 =cut
1512
1513 sub realtime_lec {
1514   my $self = shift;
1515   $self->realtime_bop( 'LEC', @_ );
1516 }
1517
1518 sub realtime_bop {
1519   my( $self, $method ) = @_;
1520
1521   my $cust_main = $self->cust_main;
1522   my $balance = $cust_main->balance;
1523   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1524   $amount = sprintf("%.2f", $amount);
1525   return "not run (balance $balance)" unless $amount > 0;
1526
1527   my $description = 'Internet Services';
1528   if ( $conf->exists('business-onlinepayment-description') ) {
1529     my $dtempl = $conf->config('business-onlinepayment-description');
1530
1531     my $agent_obj = $cust_main->agent
1532       or die "can't retreive agent for $cust_main (agentnum ".
1533              $cust_main->agentnum. ")";
1534     my $agent = $agent_obj->agent;
1535     my $pkgs = join(', ',
1536       map { $_->part_pkg->pkg }
1537         grep { $_->pkgnum } $self->cust_bill_pkg
1538     );
1539     $description = eval qq("$dtempl");
1540   }
1541
1542   $cust_main->realtime_bop($method, $amount,
1543     'description' => $description,
1544     'invnum'      => $self->invnum,
1545   );
1546
1547 }
1548
1549 =item batch_card OPTION => VALUE...
1550
1551 Adds a payment for this invoice to the pending credit card batch (see
1552 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1553 runs the payment using a realtime gateway.
1554
1555 =cut
1556
1557 sub batch_card {
1558   my ($self, %options) = @_;
1559   my $cust_main = $self->cust_main;
1560
1561   $options{invnum} = $self->invnum;
1562   
1563   $cust_main->batch_card(%options);
1564 }
1565
1566 sub _agent_template {
1567   my $self = shift;
1568   $self->cust_main->agent_template;
1569 }
1570
1571 sub _agent_invoice_from {
1572   my $self = shift;
1573   $self->cust_main->agent_invoice_from;
1574 }
1575
1576 =item print_text [ TIME [ , TEMPLATE ] ]
1577
1578 Returns an text invoice, as a list of lines.
1579
1580 TIME an optional value used to control the printing of overdue messages.  The
1581 default is now.  It isn't the date of the invoice; that's the `_date' field.
1582 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1583 L<Time::Local> and L<Date::Parse> for conversion functions.
1584
1585 =cut
1586
1587 sub print_text {
1588   my( $self, $today, $template ) = @_;
1589
1590   my %params = ( 'format' => 'template' );
1591   $params{'time'} = $today if $today;
1592   $params{'template'} = $template if $template;
1593
1594   $self->print_generic( %params );
1595 }
1596
1597 =item print_latex [ TIME [ , TEMPLATE ] ]
1598
1599 Internal method - returns a filename of a filled-in LaTeX template for this
1600 invoice (Note: add ".tex" to get the actual filename), and a filename of
1601 an associated logo (with the .eps extension included).
1602
1603 See print_ps and print_pdf for methods that return PostScript and PDF output.
1604
1605 TIME an optional value used to control the printing of overdue messages.  The
1606 default is now.  It isn't the date of the invoice; that's the `_date' field.
1607 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1608 L<Time::Local> and L<Date::Parse> for conversion functions.
1609
1610 =cut
1611
1612 sub print_latex {
1613
1614   my( $self, $today, $template ) = @_;
1615
1616   my %params = ( 'format' => 'latex' );
1617   $params{'time'} = $today if $today;
1618   $params{'template'} = $template if $template;
1619
1620   $template ||= $self->_agent_template;
1621
1622   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1623   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1624                            DIR      => $dir,
1625                            SUFFIX   => '.eps',
1626                            UNLINK   => 0,
1627                          ) or die "can't open temp file: $!\n";
1628
1629   if ($template && $conf->exists("logo_${template}.eps")) {
1630     print $lh $conf->config_binary("logo_${template}.eps")
1631       or die "can't write temp file: $!\n";
1632   }else{
1633     print $lh $conf->config_binary('logo.eps')
1634       or die "can't write temp file: $!\n";
1635   }
1636   close $lh;
1637   $params{'logo_file'} = $lh->filename;
1638
1639   my @filled_in = $self->print_generic( %params );
1640   
1641   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1642                            DIR      => $dir,
1643                            SUFFIX   => '.tex',
1644                            UNLINK   => 0,
1645                          ) or die "can't open temp file: $!\n";
1646   print $fh join('', @filled_in );
1647   close $fh;
1648
1649   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1650   return ($1, $params{'logo_file'});
1651
1652 }
1653
1654 =item print_generic OPTIONS_HASH
1655
1656 Internal method - returns a filled-in template for this invoice as a scalar.
1657
1658 See print_ps and print_pdf for methods that return PostScript and PDF output.
1659
1660 Non optional options include 
1661   format - latex, html, template
1662
1663 Optional options include
1664
1665 template - a value used as a suffix for a configuration template
1666
1667 time - a value used to control the printing of overdue messages.  The
1668 default is now.  It isn't the date of the invoice; that's the `_date' field.
1669 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1670 L<Time::Local> and L<Date::Parse> for conversion functions.
1671
1672 cid - 
1673
1674 unsquelch_cdr - overrides any per customer cdr squelching when true
1675
1676 =cut
1677
1678 sub print_generic {
1679
1680   my( $self, %params ) = @_;
1681   my $today = $params{today} ? $params{today} : time;
1682   warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1683     if $DEBUG;
1684
1685   my $format = $params{format};
1686   die "Unknown format: $format"
1687     unless $format =~ /^(latex|html|template)$/;
1688
1689   my $cust_main = $self->cust_main;
1690   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1691     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1692
1693
1694   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
1695                      'html'     => [ '<%=', '%>' ],
1696                      'template' => [ '{', '}' ],
1697                    );
1698
1699   #create the template
1700   my $template = $params{template} ? $params{template} : $self->_agent_template;
1701   my $templatefile = "invoice_$format";
1702   $templatefile .= "_$template"
1703     if length($template);
1704   my @invoice_template = map "$_\n", $conf->config($templatefile)
1705     or die "cannot load config data $templatefile";
1706
1707   my $old_latex = '';
1708   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1709     #change this to a die when the old code is removed
1710     warn "old-style invoice template $templatefile; ".
1711          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1712     $old_latex = 'true';
1713     @invoice_template = _translate_old_latex_format(@invoice_template);
1714   } 
1715
1716   my $text_template = new Text::Template(
1717     TYPE => 'ARRAY',
1718     SOURCE => \@invoice_template,
1719     DELIMITERS => $delimiters{$format},
1720   );
1721
1722   $text_template->compile()
1723     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1724
1725
1726   # additional substitution could possibly cause breakage in existing templates
1727   my %convert_maps = ( 
1728     'latex' => {
1729                  'notes'         => sub { map "$_", @_ },
1730                  'footer'        => sub { map "$_", @_ },
1731                  'smallfooter'   => sub { map "$_", @_ },
1732                  'returnaddress' => sub { map "$_", @_ },
1733                  'coupon'        => sub { map "$_", @_ },
1734                },
1735     'html'  => {
1736                  'notes' =>
1737                    sub {
1738                      map { 
1739                        s/%%(.*)$/<!-- $1 -->/g;
1740                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1741                        s/\\begin\{enumerate\}/<ol>/g;
1742                        s/\\item /  <li>/g;
1743                        s/\\end\{enumerate\}/<\/ol>/g;
1744                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1745                        s/\\\\\*/<br>/g;
1746                        s/\\dollar ?/\$/g;
1747                        s/\\#/#/g;
1748                        s/~/&nbsp;/g;
1749                        $_;
1750                      }  @_
1751                    },
1752                  'footer' =>
1753                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1754                  'smallfooter' =>
1755                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1756                  'returnaddress' =>
1757                    sub {
1758                      map { 
1759                        s/~/&nbsp;/g;
1760                        s/\\\\\*?\s*$/<BR>/;
1761                        s/\\hyphenation\{[\w\s\-]+}//;
1762                        $_;
1763                      }  @_
1764                    },
1765                  'coupon'        => sub { "" },
1766                },
1767     'template' => {
1768                  'notes' =>
1769                    sub {
1770                      map { 
1771                        s/%%.*$//g;
1772                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1773                        s/\\begin\{enumerate\}//g;
1774                        s/\\item /  * /g;
1775                        s/\\end\{enumerate\}//g;
1776                        s/\\textbf\{(.*)\}/$1/g;
1777                        s/\\\\\*/ /;
1778                        s/\\dollar ?/\$/g;
1779                        $_;
1780                      }  @_
1781                    },
1782                  'footer' =>
1783                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1784                  'smallfooter' =>
1785                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1786                  'returnaddress' =>
1787                    sub {
1788                      map { 
1789                        s/~/ /g;
1790                        s/\\\\\*?\s*$/\n/;             # dubious
1791                        s/\\hyphenation\{[\w\s\-]+}//;
1792                        $_;
1793                      }  @_
1794                    },
1795                  'coupon'        => sub { "" },
1796                },
1797   );
1798
1799
1800   # hashes for differing output formats
1801   my %nbsps = ( 'latex'    => '~',
1802                 'html'     => '',    # '&nbps;' would be nice
1803                 'template' => '',    # not used
1804               );
1805   my $nbsp = $nbsps{$format};
1806
1807   my %escape_functions = ( 'latex'    => \&_latex_escape,
1808                            'html'     => \&encode_entities,
1809                            'template' => sub { shift },
1810                          );
1811   my $escape_function = $escape_functions{$format};
1812
1813   my %date_formats = ( 'latex'    => '%b %o, %Y',
1814                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
1815                        'template' => '%s',
1816                      );
1817   my $date_format = $date_formats{$format};
1818
1819   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
1820                                                },
1821                              'html'     => sub { return '<b>'. shift(). '</b>'
1822                                                },
1823                              'template' => sub { shift },
1824                            );
1825   my $embolden_function = $embolden_functions{$format};
1826
1827
1828   # generate template variables
1829   my $returnaddress;
1830   if (
1831          defined( $conf->config_orbase( "invoice_${format}returnaddress",
1832                                         $template
1833                                       )
1834                 )
1835        && length( $conf->config_orbase( "invoice_${format}returnaddress",
1836                                         $template
1837                                       )
1838                 )
1839   ) {
1840
1841     $returnaddress = join("\n",
1842       $conf->config_orbase("invoice_${format}returnaddress", $template)
1843     );
1844
1845   } elsif ( grep /\S/,
1846             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1847
1848     my $convert_map = $convert_maps{$format}{'returnaddress'};
1849     $returnaddress =
1850       join( "\n",
1851             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1852                                                  $template
1853                                                )
1854                          )
1855           );
1856   } elsif ( grep /\S/, $conf->config('company_address') ) {
1857
1858     my $convert_map = $convert_maps{$format}{'returnaddress'};
1859     $returnaddress = join( "\n", &$convert_map(
1860                                    map { s/( {2,})/'~' x length($1)/eg;
1861                                          s/$/\\\\\*/;
1862                                          $_
1863                                        }
1864                                      ( $conf->config('company_name'),
1865                                        $conf->config('company_address'),
1866                                      )
1867                                  )
1868                      );
1869
1870   } else {
1871
1872     my $warning = "Couldn't find a return address; ".
1873                   "do you need to set the company_address configuration value?";
1874     warn "$warning\n";
1875     $returnaddress = $nbsp;
1876     #$returnaddress = $warning;
1877
1878   }
1879
1880   my %invoice_data = (
1881     'company_name'    => scalar( $conf->config('company_name') ),
1882     'company_address' => join("\n", $conf->config('company_address') ). "\n",
1883     'custnum'         => $self->custnum,
1884     'invnum'          => $self->invnum,
1885     'date'            => time2str($date_format, $self->_date),
1886     'today'           => time2str('%b %o, %Y', $today),
1887     'agent'           => &$escape_function($cust_main->agent->agent),
1888     'agent_custid'    => &$escape_function($cust_main->agent_custid),
1889     'payname'         => &$escape_function($cust_main->payname),
1890     'company'         => &$escape_function($cust_main->company),
1891     'address1'        => &$escape_function($cust_main->address1),
1892     'address2'        => &$escape_function($cust_main->address2),
1893     'city'            => &$escape_function($cust_main->city),
1894     'state'           => &$escape_function($cust_main->state),
1895     'zip'             => &$escape_function($cust_main->zip),
1896     'returnaddress'   => $returnaddress,
1897     #'quantity'        => 1,
1898     'terms'           => $self->terms,
1899     'template'        => $params{'template'},
1900     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
1901     # better hang on to conf_dir for a while
1902     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1903     'page'            => 1,
1904     'total_pages'     => 1,
1905     'current_charges' => sprintf("%.2f", $self->charged),
1906     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
1907     'ship_enable'     => $conf->exists('invoice-ship_address'),
1908     'unitprices'      => $conf->exists('invoice-unitprice'),
1909   );
1910
1911   my $countrydefault = $conf->config('countrydefault') || 'US';
1912   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1913   foreach ( qw( contact company address1 address2 city state zip country fax) ){
1914     my $method = $prefix.$_;
1915     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1916   }
1917   $invoice_data{'ship_country'} = ''
1918     if ( $invoice_data{'ship_country'} eq $countrydefault );
1919   
1920   $invoice_data{'cid'} = $params{'cid'}
1921     if $params{'cid'};
1922
1923   if ( $cust_main->country eq $countrydefault ) {
1924     $invoice_data{'country'} = '';
1925   } else {
1926     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1927   }
1928
1929   my @address = ();
1930   $invoice_data{'address'} = \@address;
1931   push @address,
1932     $cust_main->payname.
1933       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1934         ? " (P.O. #". $cust_main->payinfo. ")"
1935         : ''
1936       )
1937   ;
1938   push @address, $cust_main->company
1939     if $cust_main->company;
1940   push @address, $cust_main->address1;
1941   push @address, $cust_main->address2
1942     if $cust_main->address2;
1943   push @address,
1944     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1945   push @address, $invoice_data{'country'}
1946     if $invoice_data{'country'};
1947   push @address, ''
1948     while (scalar(@address) < 5);
1949
1950   $invoice_data{'logo_file'} = $params{'logo_file'}
1951     if $params{'logo_file'};
1952
1953   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1954 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1955   #my $balance_due = $self->owed + $pr_total - $cr_total;
1956   my $balance_due = $self->owed + $pr_total;
1957   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1958   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1959
1960   #do variable substitution in notes, footer, smallfooter
1961   foreach my $include (qw( notes footer smallfooter coupon )) {
1962
1963     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1964     my @inc_src;
1965
1966     if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1967
1968       @inc_src = $conf->config($inc_file);
1969
1970     } else {
1971
1972       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1973
1974       my $convert_map = $convert_maps{$format}{$include};
1975
1976       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1977                        s/--\@\]/$delimiters{$format}[1]/g;
1978                        $_;
1979                      } 
1980                  &$convert_map( $conf->config($inc_file) );
1981
1982     }
1983
1984     my $inc_tt = new Text::Template (
1985       TYPE       => 'ARRAY',
1986       SOURCE     => [ map "$_\n", @inc_src ],
1987       DELIMITERS => $delimiters{$format},
1988     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1989
1990     unless ( $inc_tt->compile() ) {
1991       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1992       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1993       die $error;
1994     }
1995
1996     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1997
1998     $invoice_data{$include} =~ s/\n+$//
1999       if ($format eq 'latex');
2000   }
2001
2002   $invoice_data{'po_line'} =
2003     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2004       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2005       : $nbsp;
2006
2007   my %money_chars = ( 'latex'    => '',
2008                       'html'     => $conf->config('money_char') || '$',
2009                       'template' => '',
2010                     );
2011   my $money_char = $money_chars{$format};
2012
2013   my %other_money_chars = ( 'latex'    => '\dollar ',
2014                             'html'     => $conf->config('money_char') || '$',
2015                             'template' => '',
2016                           );
2017   my $other_money_char = $other_money_chars{$format};
2018
2019   my @detail_items = ();
2020   my @total_items = ();
2021   my @buf = ();
2022   my @sections = ();
2023
2024   $invoice_data{'detail_items'} = \@detail_items;
2025   $invoice_data{'total_items'} = \@total_items;
2026   $invoice_data{'buf'} = \@buf;
2027   $invoice_data{'sections'} = \@sections;
2028   
2029   my $previous_section = { 'description' => 'Previous Charges',
2030                            'subtotal'    => $other_money_char.
2031                                             sprintf('%.2f', $pr_total),
2032                          };
2033
2034   my $taxtotal = 0;
2035   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2036                       'subtotal'    => $taxtotal }; # adjusted below
2037
2038   my $adjusttotal = 0;
2039   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2040                          'subtotal'    => 0 }; # adjusted below
2041
2042   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2043   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2044   my $late_sections = [];
2045   if ( $multisection ) {
2046     push @sections, $self->_items_sections( $late_sections );
2047   }else{
2048     push @sections, { 'description' => '', 'subtotal' => '' };
2049   }
2050
2051   foreach my $line_item ( $conf->exists('disable_previous_balance') 
2052                             ? ()
2053                             : $self->_items_previous
2054                         )
2055   {
2056     my $detail = {
2057       ext_description => [],
2058     };
2059     $detail->{'ref'} = $line_item->{'pkgnum'};
2060     $detail->{'quantity'} = 1;
2061     $detail->{'section'} = $previous_section;
2062     $detail->{'description'} = &$escape_function($line_item->{'description'});
2063     if ( exists $line_item->{'ext_description'} ) {
2064       @{$detail->{'ext_description'}} = map {
2065         &$escape_function($_);
2066       } @{$line_item->{'ext_description'}};
2067     }
2068     $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2069                           $line_item->{'amount'};
2070     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2071   
2072     push @detail_items, $detail;
2073     push @buf, [ $detail->{'description'},
2074                  $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2075                ];
2076   }
2077   
2078   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2079     push @buf, ['','-----------'];
2080     push @buf, [ 'Total Previous Balance',
2081                  $money_char. sprintf("%10.2f", $pr_total) ];
2082     push @buf, ['',''];
2083   }
2084
2085   foreach my $section (@sections, @$late_sections) {
2086
2087     $section->{'subtotal'} = $other_money_char.
2088                              sprintf('%.2f', $section->{'subtotal'})
2089       if $multisection;
2090
2091     if ( $section->{'description'} ) {
2092       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2093                    [ '', '' ],
2094                  );
2095     }
2096
2097     my %options = ();
2098     $options{'section'} = $section if $multisection;
2099     $options{'format'} = $format;
2100     $options{'escape_function'} = $escape_function;
2101     $options{'format_function'} = sub { () } unless $unsquelched;
2102     $options{'unsquelched'} = $unsquelched;
2103
2104     foreach my $line_item ( $self->_items_pkg(%options) ) {
2105       my $detail = {
2106         ext_description => [],
2107       };
2108       $detail->{'ref'} = $line_item->{'pkgnum'};
2109       $detail->{'quantity'} = $line_item->{'quantity'};
2110       $detail->{'section'} = $section;
2111       $detail->{'description'} = &$escape_function($line_item->{'description'});
2112       if ( exists $line_item->{'ext_description'} ) {
2113         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2114       }
2115       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2116                               $line_item->{'amount'};
2117       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2118                                  $line_item->{'unit_amount'};
2119       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2120   
2121       push @detail_items, $detail;
2122       push @buf, ( [ $detail->{'description'},
2123                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2124                    ],
2125                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2126                  );
2127     }
2128
2129     if ( $section->{'description'} ) {
2130       push @buf, ( ['','-----------'],
2131                    [ $section->{'description'}. ' sub-total',
2132                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2133                    ],
2134                    [ '', '' ],
2135                    [ '', '' ],
2136                  );
2137     }
2138   
2139   }
2140   
2141   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2142     unshift @sections, $previous_section if $pr_total;
2143   }
2144
2145   foreach my $tax ( $self->_items_tax ) {
2146     my $total = {};
2147     $total->{'total_item'} = &$escape_function($tax->{'description'});
2148     $taxtotal += $tax->{'amount'};
2149     $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2150     if ( $multisection ) {
2151       my $money = $old_latex ? '' : $money_char;
2152       push @detail_items, {
2153         ext_description => [],
2154         ref          => '',
2155         quantity     => '',
2156         description  => &$escape_function($tax->{'description'}),
2157         amount       => $money. $tax->{'amount'},
2158         product_code => '',
2159         section      => $tax_section,
2160       };
2161     }else{
2162       push @total_items, $total;
2163     }
2164     push @buf,[ $total->{'total_item'},
2165                 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2166               ];
2167
2168   }
2169   
2170   if ( $taxtotal ) {
2171     my $total = {};
2172     $total->{'total_item'} = 'Sub-total';
2173     $total->{'total_amount'} =
2174       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2175
2176     if ( $multisection ) {
2177       $tax_section->{'subtotal'} = $other_money_char.
2178                                    sprintf('%.2f', $taxtotal);
2179       $tax_section->{'pretotal'} = 'New charges sub-total '.
2180                                    $total->{'total_amount'};
2181       push @sections, $tax_section if $taxtotal;
2182     }else{
2183       unshift @total_items, $total;
2184     }
2185   }
2186   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2187   
2188   push @buf,['','-----------'];
2189   push @buf,[( $conf->exists('disable_previous_balance') 
2190                ? 'Total Charges'
2191                : 'Total New Charges'
2192              ),
2193              $money_char. sprintf("%10.2f",$self->charged) ];
2194   push @buf,['',''];
2195
2196   {
2197     my $total = {};
2198     $total->{'total_item'} = &$embolden_function('Total');
2199     $total->{'total_amount'} =
2200       &$embolden_function(
2201         $other_money_char.
2202         sprintf( '%.2f',
2203                  $self->charged + ( $conf->exists('disable_previous_balance')
2204                                     ? 0
2205                                     : $pr_total
2206                                   )
2207                )
2208       );
2209     if ( $multisection ) {
2210       $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2211                                       sprintf('%.2f', $self->charged );
2212     }else{
2213       push @total_items, $total;
2214     }
2215     push @buf,['','-----------'];
2216     push @buf,['Total Charges',
2217                $money_char.
2218                sprintf( '%10.2f', $self->charged +
2219                                     ( $conf->exists('disable_previous_balance')
2220                                         ? 0
2221                                         : $pr_total
2222                                     )
2223                       )
2224               ];
2225     push @buf,['',''];
2226   }
2227   
2228   unless ( $conf->exists('disable_previous_balance') ) {
2229     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2230   
2231     # credits
2232     my $credittotal = 0;
2233     foreach my $credit ( $self->_items_credits ) {
2234       my $total;
2235       $total->{'total_item'} = &$escape_function($credit->{'description'});
2236       $credittotal += $credit->{'amount'};
2237       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2238       $adjusttotal += $credit->{'amount'};
2239       if ( $multisection ) {
2240         my $money = $old_latex ? '' : $money_char;
2241         push @detail_items, {
2242           ext_description => [],
2243           ref          => '',
2244           quantity     => '',
2245           description  => &$escape_function($credit->{'description'}),
2246           amount       => $money. $credit->{'amount'},
2247           product_code => '',
2248           section      => $adjust_section,
2249         };
2250       }else{
2251         push @total_items, $total;
2252       }
2253     }
2254     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2255   
2256     # credits (again)
2257     foreach ( $self->cust_credited ) {
2258   
2259       #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2260
2261       my $reason = substr($_->cust_credit->reason,0,32);
2262       $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2263       $reason = " ($reason) " if $reason;
2264       push @buf,[
2265         "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
2266         $money_char. sprintf("%10.2f",$_->amount)
2267       ];
2268     }
2269
2270     # payments
2271     my $paymenttotal = 0;
2272     foreach my $payment ( $self->_items_payments ) {
2273       my $total = {};
2274       $total->{'total_item'} = &$escape_function($payment->{'description'});
2275       $paymenttotal += $payment->{'amount'};
2276       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2277       $adjusttotal += $payment->{'amount'};
2278       if ( $multisection ) {
2279         my $money = $old_latex ? '' : $money_char;
2280         push @detail_items, {
2281           ext_description => [],
2282           ref          => '',
2283           quantity     => '',
2284           description  => &$escape_function($payment->{'description'}),
2285           amount       => $money. $payment->{'amount'},
2286           product_code => '',
2287           section      => $adjust_section,
2288         };
2289       }else{
2290         push @total_items, $total;
2291       }
2292       push @buf, [ $payment->{'description'},
2293                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2294                  ];
2295     }
2296     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2297   
2298     if ( $multisection ) {
2299       $adjust_section->{'subtotal'} = $other_money_char.
2300                                       sprintf('%.2f', $adjusttotal);
2301       push @sections, $adjust_section;
2302     }
2303
2304     { 
2305       my $total;
2306       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2307       $total->{'total_amount'} =
2308         &$embolden_function(
2309           $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2310         );
2311       if ( $multisection ) {
2312         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2313                                          $total->{'total_amount'};
2314       }else{
2315         push @total_items, $total;
2316       }
2317       push @buf,['','-----------'];
2318       push @buf,[$self->balance_due_msg, $money_char. 
2319         sprintf("%10.2f", $balance_due ) ];
2320     }
2321   }
2322
2323   if ( $multisection ) {
2324     push @sections, @$late_sections
2325       if $unsquelched;
2326   }
2327
2328   $invoice_lines = 0;
2329   my $wasfunc = 0;
2330   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2331     /invoice_lines\((\d*)\)/;
2332     $invoice_lines += $1 || scalar(@buf);
2333     $wasfunc=1;
2334   }
2335   die "no invoice_lines() functions in template?"
2336     if ( $format eq 'template' && !$wasfunc );
2337
2338   if ($format eq 'template') {
2339
2340     if ( $invoice_lines ) {
2341       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2342       $invoice_data{'total_pages'}++
2343         if scalar(@buf) % $invoice_lines;
2344     }
2345
2346     #setup subroutine for the template
2347     sub FS::cust_bill::_template::invoice_lines {
2348       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2349       map { 
2350         scalar(@FS::cust_bill::_template::buf)
2351           ? shift @FS::cust_bill::_template::buf
2352           : [ '', '' ];
2353       }
2354       ( 1 .. $lines );
2355     }
2356
2357     my $lines;
2358     my @collect;
2359     while (@buf) {
2360       push @collect, split("\n",
2361         $text_template->fill_in( HASH => \%invoice_data,
2362                                  PACKAGE => 'FS::cust_bill::_template'
2363                                )
2364       );
2365       $FS::cust_bill::_template::page++;
2366     }
2367     map "$_\n", @collect;
2368   }else{
2369     warn "filling in template for invoice ". $self->invnum. "\n"
2370       if $DEBUG;
2371     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2372       if $DEBUG > 1;
2373
2374     $text_template->fill_in(HASH => \%invoice_data);
2375   }
2376 }
2377
2378 =item print_ps [ TIME [ , TEMPLATE ] ]
2379
2380 Returns an postscript invoice, as a scalar.
2381
2382 TIME an optional value used to control the printing of overdue messages.  The
2383 default is now.  It isn't the date of the invoice; that's the `_date' field.
2384 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2385 L<Time::Local> and L<Date::Parse> for conversion functions.
2386
2387 =cut
2388
2389 sub print_ps {
2390   my $self = shift;
2391
2392   my ($file, $lfile) = $self->print_latex(@_);
2393   my $ps = generate_ps($file);
2394   unlink($lfile);
2395
2396   $ps;
2397 }
2398
2399 =item print_pdf [ TIME [ , TEMPLATE ] ]
2400
2401 Returns an PDF invoice, as a scalar.
2402
2403 TIME an optional value used to control the printing of overdue messages.  The
2404 default is now.  It isn't the date of the invoice; that's the `_date' field.
2405 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2406 L<Time::Local> and L<Date::Parse> for conversion functions.
2407
2408 =cut
2409
2410 sub print_pdf {
2411   my $self = shift;
2412
2413   my ($file, $lfile) = $self->print_latex(@_);
2414   my $pdf = generate_pdf($file);
2415   unlink($lfile);
2416
2417   $pdf;
2418 }
2419
2420 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2421
2422 Returns an HTML invoice, as a scalar.
2423
2424 TIME an optional value used to control the printing of overdue messages.  The
2425 default is now.  It isn't the date of the invoice; that's the `_date' field.
2426 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2427 L<Time::Local> and L<Date::Parse> for conversion functions.
2428
2429 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2430 when emailing the invoice as part of a multipart/related MIME email.
2431
2432 =cut
2433
2434 sub print_html {
2435   my $self = shift;
2436   my %params;
2437   if ( ref $_[0]  ) {
2438     %params = %{ shift() }; 
2439   }else{
2440     $params{'time'} = shift;
2441     $params{'template'} = shift;
2442     $params{'cid'} = shift;
2443   }
2444
2445   $params{'format'} = 'html';
2446
2447   $self->print_generic( %params );
2448 }
2449
2450 # quick subroutine for print_latex
2451 #
2452 # There are ten characters that LaTeX treats as special characters, which
2453 # means that they do not simply typeset themselves: 
2454 #      # $ % & ~ _ ^ \ { }
2455 #
2456 # TeX ignores blanks following an escaped character; if you want a blank (as
2457 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2458
2459 sub _latex_escape {
2460   my $value = shift;
2461   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2462   $value =~ s/([<>])/\$$1\$/g;
2463   $value;
2464 }
2465
2466 #utility methods for print_*
2467
2468 sub _translate_old_latex_format {
2469   warn "_translate_old_latex_format called\n"
2470     if $DEBUG; 
2471
2472   my @template = ();
2473   while ( @_ ) {
2474     my $line = shift;
2475   
2476     if ( $line =~ /^%%Detail\s*$/ ) {
2477   
2478       push @template, q![@--!,
2479                       q!  foreach my $_tr_line (@detail_items) {!,
2480                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2481                       q!      $_tr_line->{'description'} .= !, 
2482                       q!        "\\tabularnewline\n~~".!,
2483                       q!        join( "\\tabularnewline\n~~",!,
2484                       q!          @{$_tr_line->{'ext_description'}}!,
2485                       q!        );!,
2486                       q!    }!;
2487
2488       while ( ( my $line_item_line = shift )
2489               !~ /^%%EndDetail\s*$/                            ) {
2490         $line_item_line =~ s/'/\\'/g;    # nice LTS
2491         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2492         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2493         push @template, "    \$OUT .= '$line_item_line';";
2494       }
2495   
2496       push @template, '}',
2497                       '--@]';
2498
2499     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2500
2501       push @template, '[@--',
2502                       '  foreach my $_tr_line (@total_items) {';
2503
2504       while ( ( my $total_item_line = shift )
2505               !~ /^%%EndTotalDetails\s*$/                      ) {
2506         $total_item_line =~ s/'/\\'/g;    # nice LTS
2507         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2508         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2509         push @template, "    \$OUT .= '$total_item_line';";
2510       }
2511
2512       push @template, '}',
2513                       '--@]';
2514
2515     } else {
2516       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2517       push @template, $line;  
2518     }
2519   
2520   }
2521
2522   if ($DEBUG) {
2523     warn "$_\n" foreach @template;
2524   }
2525
2526   (@template);
2527 }
2528
2529 sub terms {
2530   my $self = shift;
2531
2532   #check for an invoice- specific override (eventually)
2533   
2534   #check for a customer- specific override
2535   return $self->cust_main->invoice_terms
2536     if $self->cust_main->invoice_terms;
2537
2538   #use configured default or default default
2539   $conf->config('invoice_default_terms') || 'Payable upon receipt';
2540 }
2541
2542 sub due_date {
2543   my $self = shift;
2544   my $duedate = '';
2545   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2546     $duedate = $self->_date() + ( $1 * 86400 );
2547   }
2548   $duedate;
2549 }
2550
2551 sub due_date2str {
2552   my $self = shift;
2553   $self->due_date ? time2str(shift, $self->due_date) : '';
2554 }
2555
2556 sub balance_due_msg {
2557   my $self = shift;
2558   my $msg = 'Balance Due';
2559   return $msg unless $self->terms;
2560   if ( $self->due_date ) {
2561     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2562   } elsif ( $self->terms ) {
2563     $msg .= ' - '. $self->terms;
2564   }
2565   $msg;
2566 }
2567
2568 sub balance_due_date {
2569   my $self = shift;
2570   my $duedate = '';
2571   if (    $conf->exists('invoice_default_terms') 
2572        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2573     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2574   }
2575   $duedate;
2576 }
2577
2578 =item invnum_date_pretty
2579
2580 Returns a string with the invoice number and date, for example:
2581 "Invoice #54 (3/20/2008)"
2582
2583 =cut
2584
2585 sub invnum_date_pretty {
2586   my $self = shift;
2587   'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2588 }
2589
2590 sub _items_sections {
2591   my $self = shift;
2592   my $late = shift;
2593
2594   my %s = ();
2595   my %l = ();
2596
2597   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2598   {
2599
2600     if ( $cust_bill_pkg->pkgnum > 0 ) {
2601
2602       my $desc = $cust_bill_pkg->section;
2603       my $dup_desc = $cust_bill_pkg->duplicate_section;
2604
2605       if ($cust_bill_pkg->duplicate) {
2606         $s{$dup_desc} += $cust_bill_pkg->setup
2607           if ( $cust_bill_pkg->setup != 0 );
2608
2609         $s{$dup_desc} += $cust_bill_pkg->recur
2610           if ( $cust_bill_pkg->recur != 0 );
2611       }
2612
2613       if ( $cust_bill_pkg->post_total ) {
2614         $l{$desc} += $cust_bill_pkg->setup
2615           if ( $cust_bill_pkg->setup != 0 );
2616
2617         $l{$desc} += $cust_bill_pkg->recur
2618           if ( $cust_bill_pkg->recur != 0 );
2619
2620       } else {
2621         $s{$desc} += $cust_bill_pkg->setup
2622           if ( $cust_bill_pkg->setup != 0 );
2623
2624         $s{$desc} += $cust_bill_pkg->recur
2625           if ( $cust_bill_pkg->recur != 0 );
2626       }
2627
2628     }
2629
2630   }
2631
2632   push @$late, map { { 'description' => $_,
2633                        'subtotal'    => $l{$_},
2634                        'post_total'  => 1,
2635                    } } sort keys %l;
2636
2637   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2638
2639 }
2640
2641 sub _items {
2642   my $self = shift;
2643
2644   #my @display = scalar(@_)
2645   #              ? @_
2646   #              : qw( _items_previous _items_pkg );
2647   #              #: qw( _items_pkg );
2648   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2649   my @display = qw( _items_previous _items_pkg );
2650
2651   my @b = ();
2652   foreach my $display ( @display ) {
2653     push @b, $self->$display(@_);
2654   }
2655   @b;
2656 }
2657
2658 sub _items_previous {
2659   my $self = shift;
2660   my $cust_main = $self->cust_main;
2661   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2662   my @b = ();
2663   foreach ( @pr_cust_bill ) {
2664     push @b, {
2665       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2666                        ' ('. time2str('%x',$_->_date). ')',
2667       #'pkgpart'     => 'N/A',
2668       'pkgnum'      => 'N/A',
2669       'amount'      => sprintf("%.2f", $_->owed),
2670     };
2671   }
2672   @b;
2673
2674   #{
2675   #    'description'     => 'Previous Balance',
2676   #    #'pkgpart'         => 'N/A',
2677   #    'pkgnum'          => 'N/A',
2678   #    'amount'          => sprintf("%10.2f", $pr_total ),
2679   #    'ext_description' => [ map {
2680   #                                 "Invoice ". $_->invnum.
2681   #                                 " (". time2str("%x",$_->_date). ") ".
2682   #                                 sprintf("%10.2f", $_->owed)
2683   #                         } @pr_cust_bill ],
2684
2685   #};
2686 }
2687
2688 sub _items_pkg {
2689   my $self = shift;
2690   my %options = @_;
2691   my $section = $options{'section'};
2692   my $desc = $section->{'description'};
2693   my @cust_bill_pkg =
2694     grep { $_->pkgnum &&
2695            ( defined($section)
2696                ? ( $_->section eq $desc || $_->duplicate_section eq $desc )
2697                : 1
2698            )
2699          } $self->cust_bill_pkg;
2700   $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2701 }
2702
2703 sub _taxsort {
2704   return 0 unless $a cmp $b;
2705   return -1 if $b eq 'Tax';
2706   return 1 if $a eq 'Tax';
2707   return -1 if $b eq 'Other surcharges';
2708   return 1 if $a eq 'Other surcharges';
2709   $a cmp $b;
2710 }
2711
2712 sub _items_tax {
2713   my $self = shift;
2714   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2715   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2716 }
2717
2718 sub _items_cust_bill_pkg {
2719   my $self = shift;
2720   my $cust_bill_pkg = shift;
2721   my %opt = @_;
2722
2723   my $format = $opt{format} || '';
2724   my $escape_function = $opt{escape_function} || sub { shift };
2725   my $format_function = $opt{format_function} || '';
2726   my $unsquelched = $opt{unsquelched} || '';
2727
2728   my @b = ();
2729   my $last_pkgnum = '';
2730   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2731   {
2732
2733     my $cust_pkg = $cust_bill_pkg->cust_pkg;
2734
2735     my $desc = $cust_bill_pkg->desc;
2736
2737     my %details_opt = ( 'format'          => $format,
2738                         'escape_function' => $escape_function,
2739                         'format_function' => $format_function,
2740                       );
2741
2742     if ( $cust_bill_pkg->pkgnum > 0 ) {
2743
2744       if ( $cust_bill_pkg->setup != 0 ) {
2745
2746         my $description = $desc;
2747         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2748
2749         my @d = map &{$escape_function}($_),
2750                        $cust_pkg->h_labels_short($self->_date);
2751         push @d, $cust_bill_pkg->details(%details_opt)
2752           if $cust_bill_pkg->recur == 0;
2753
2754         push @b, {
2755           description     => $description,
2756           #pkgpart         => $part_pkg->pkgpart,
2757           pkgnum          => $cust_bill_pkg->pkgnum,
2758           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2759           unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2760           quantity        => $cust_bill_pkg->quantity,
2761           ext_description => \@d,
2762         };
2763
2764         $last_pkgnum = '';
2765
2766       }
2767
2768       if ( $cust_bill_pkg->recur != 0 ) {
2769
2770         my $is_summary =
2771           ( $cust_bill_pkg->duplicate && 
2772             $opt{section}->{description} ne $cust_bill_pkg->section
2773           );
2774         my $description = $is_summary ? "Usage charges" : $desc;
2775
2776         unless ( $conf->exists('disable_line_item_date_ranges') ) {
2777           $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2778                           " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2779         }
2780
2781         #at least until cust_bill_pkg has "past" ranges in addition to
2782         #the "future" sdate/edate ones... see #3032
2783         my @d = ();
2784         push @d, map &{$escape_function}($_),
2785                        $cust_pkg->h_labels_short($self->_date)
2786                                               #$cust_bill_pkg->edate,
2787                                               #$cust_bill_pkg->sdate),
2788           unless ($cust_bill_pkg->pkgnum eq $last_pkgnum);
2789
2790         @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2791         push @d, $cust_bill_pkg->details(%details_opt)
2792           unless $is_summary;
2793
2794         if ($cust_bill_pkg->pkgnum eq $last_pkgnum) {
2795
2796           $b[$#b]->{amount} =
2797             sprintf("%.2f", $b[$#b]->{amount} + $cust_bill_pkg->recur);
2798           push @{$b[$#b]->{ext_description}}, @d;
2799
2800         }else{
2801
2802           push @b, {
2803             description     => $description,
2804             #pkgpart         => $part_pkg->pkgpart,
2805             pkgnum          => $cust_bill_pkg->pkgnum,
2806             amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2807             unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2808             quantity        => $cust_bill_pkg->quantity,
2809             ext_description => \@d,
2810           };
2811
2812         }
2813
2814         if ($conf->exists('separate_usage') && $cust_bill_pkg->type ne 'U') {
2815           $last_pkgnum = '';
2816         }else{
2817           $last_pkgnum = $cust_bill_pkg->pkgnum;
2818         }
2819       }
2820
2821     } else { #pkgnum tax or one-shot line item (??)
2822
2823       if ( $cust_bill_pkg->setup != 0 ) {
2824         push @b, {
2825           'description' => $desc,
2826           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2827         };
2828       }
2829       if ( $cust_bill_pkg->recur != 0 ) {
2830         push @b, {
2831           'description' => "$desc (".
2832                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2833                            time2str("%x", $cust_bill_pkg->edate). ')',
2834           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2835         };
2836       }
2837
2838       $last_pkgnum = '';
2839
2840     }
2841
2842   }
2843
2844   @b;
2845
2846 }
2847
2848 sub _items_credits {
2849   my $self = shift;
2850
2851   my @b;
2852   #credits
2853   foreach ( $self->cust_credited ) {
2854
2855     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2856
2857     my $reason = $_->cust_credit->reason;
2858     #my $reason = substr($_->cust_credit->reason,0,32);
2859     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2860     $reason = " ($reason) " if $reason;
2861     push @b, {
2862       #'description' => 'Credit ref\#'. $_->crednum.
2863       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2864       #                 $reason,
2865       'description' => 'Credit applied '.
2866                        time2str("%x",$_->cust_credit->_date). $reason,
2867       'amount'      => sprintf("%.2f",$_->amount),
2868     };
2869   }
2870   #foreach ( @cr_cust_credit ) {
2871   #  push @buf,[
2872   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2873   #    $money_char. sprintf("%10.2f",$_->credited)
2874   #  ];
2875   #}
2876
2877   @b;
2878
2879 }
2880
2881 sub _items_payments {
2882   my $self = shift;
2883
2884   my @b;
2885   #get & print payments
2886   foreach ( $self->cust_bill_pay ) {
2887
2888     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2889
2890     push @b, {
2891       'description' => "Payment received ".
2892                        time2str("%x",$_->cust_pay->_date ),
2893       'amount'      => sprintf("%.2f", $_->amount )
2894     };
2895   }
2896
2897   @b;
2898
2899 }
2900
2901
2902 =back
2903
2904 =head1 SUBROUTINES
2905
2906 =over 4
2907
2908 =item process_reprint
2909
2910 =cut
2911
2912 sub process_reprint {
2913   process_re_X('print', @_);
2914 }
2915
2916 =item process_reemail
2917
2918 =cut
2919
2920 sub process_reemail {
2921   process_re_X('email', @_);
2922 }
2923
2924 =item process_refax
2925
2926 =cut
2927
2928 sub process_refax {
2929   process_re_X('fax', @_);
2930 }
2931
2932 =item process_reftp
2933
2934 =cut
2935
2936 sub process_reftp {
2937   process_re_X('ftp', @_);
2938 }
2939
2940 =item respool
2941
2942 =cut
2943
2944 sub process_respool {
2945   process_re_X('spool', @_);
2946 }
2947
2948 use Storable qw(thaw);
2949 use Data::Dumper;
2950 use MIME::Base64;
2951 sub process_re_X {
2952   my( $method, $job ) = ( shift, shift );
2953   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2954
2955   my $param = thaw(decode_base64(shift));
2956   warn Dumper($param) if $DEBUG;
2957
2958   re_X(
2959     $method,
2960     $job,
2961     %$param,
2962   );
2963
2964 }
2965
2966 sub re_X {
2967   my($method, $job, %param ) = @_;
2968   if ( $DEBUG ) {
2969     warn "re_X $method for job $job with param:\n".
2970          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2971   }
2972
2973   #some false laziness w/search/cust_bill.html
2974   my $distinct = '';
2975   my $orderby = 'ORDER BY cust_bill._date';
2976
2977   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2978
2979   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2980      
2981   my @cust_bill = qsearch( {
2982     #'select'    => "cust_bill.*",
2983     'table'     => 'cust_bill',
2984     'addl_from' => $addl_from,
2985     'hashref'   => {},
2986     'extra_sql' => $extra_sql,
2987     'order_by'  => $orderby,
2988     'debug' => 1,
2989   } );
2990
2991   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2992
2993   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2994     if $DEBUG;
2995
2996   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2997   foreach my $cust_bill ( @cust_bill ) {
2998     $cust_bill->$method();
2999
3000     if ( $job ) { #progressbar foo
3001       $num++;
3002       if ( time - $min_sec > $last ) {
3003         my $error = $job->update_statustext(
3004           int( 100 * $num / scalar(@cust_bill) )
3005         );
3006         die $error if $error;
3007         $last = time;
3008       }
3009     }
3010
3011   }
3012
3013 }
3014
3015 =back
3016
3017 =head1 CLASS METHODS
3018
3019 =over 4
3020
3021 =item owed_sql
3022
3023 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3024
3025 =cut
3026
3027 sub owed_sql {
3028   my $class = shift;
3029   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3030 }
3031
3032 =item net_sql
3033
3034 Returns an SQL fragment to retreive the net amount (charged minus credited).
3035
3036 =cut
3037
3038 sub net_sql {
3039   my $class = shift;
3040   'charged - '. $class->credited_sql;
3041 }
3042
3043 =item paid_sql
3044
3045 Returns an SQL fragment to retreive the amount paid against this invoice.
3046
3047 =cut
3048
3049 sub paid_sql {
3050   #my $class = shift;
3051   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3052        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
3053 }
3054
3055 =item credited_sql
3056
3057 Returns an SQL fragment to retreive the amount credited against this invoice.
3058
3059 =cut
3060
3061 sub credited_sql {
3062   #my $class = shift;
3063   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3064        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
3065 }
3066
3067 =item search_sql HASHREF
3068
3069 Class method which returns an SQL WHERE fragment to search for parameters
3070 specified in HASHREF.  Valid parameters are
3071
3072 =over 4
3073
3074 =item begin
3075
3076 Epoch date (UNIX timestamp) setting a lower bound for _date values
3077
3078 =item end
3079
3080 Epoch date (UNIX timestamp) setting an upper bound for _date values
3081
3082 =item invnum_min
3083
3084 =item invnum_max
3085
3086 =item agentnum
3087
3088 =item owed
3089
3090 =item net
3091
3092 =item days
3093
3094 =item newest_percust
3095
3096 =back
3097
3098 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3099
3100 =cut
3101
3102 sub search_sql {
3103   my($class, $param) = @_;
3104   if ( $DEBUG ) {
3105     warn "$me search_sql called with params: \n".
3106          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3107   }
3108
3109   my @search = ();
3110
3111   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3112     push @search, "cust_bill._date >= $1";
3113   }
3114   if ( $param->{'end'} =~ /^(\d+)$/ ) {
3115     push @search, "cust_bill._date < $1";
3116   }
3117   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3118     push @search, "cust_bill.invnum >= $1";
3119   }
3120   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3121     push @search, "cust_bill.invnum <= $1";
3122   }
3123   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3124     push @search, "cust_main.agentnum = $1";
3125   }
3126
3127   push @search, '0 != '. FS::cust_bill->owed_sql
3128     if $param->{'open'};
3129
3130   push @search, '0 != '. FS::cust_bill->net_sql
3131     if $param->{'net'};
3132
3133   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3134     if $param->{'days'};
3135
3136   if ( $param->{'newest_percust'} ) {
3137
3138     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3139     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3140
3141     my @newest_where = map { my $x = $_;
3142                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3143                              $x;
3144                            }
3145                            grep ! /^cust_main./, @search;
3146     my $newest_where = scalar(@newest_where)
3147                          ? ' AND '. join(' AND ', @newest_where)
3148                          : '';
3149
3150
3151     push @search, "cust_bill._date = (
3152       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3153         WHERE newest_cust_bill.custnum = cust_bill.custnum
3154           $newest_where
3155     )";
3156
3157   }
3158
3159   my $curuser = $FS::CurrentUser::CurrentUser;
3160   if ( $curuser->username eq 'fs_queue'
3161        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3162     my $username = $1;
3163     my $newuser = qsearchs('access_user', {
3164       'username' => $username,
3165       'disabled' => '',
3166     } );
3167     if ( $newuser ) {
3168       $curuser = $newuser;
3169     } else {
3170       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3171     }
3172   }
3173
3174   push @search, $curuser->agentnums_sql;
3175
3176   join(' AND ', @search );
3177
3178 }
3179
3180 =back
3181
3182 =head1 BUGS
3183
3184 The delete method.
3185
3186 =head1 SEE ALSO
3187
3188 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3189 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3190 documentation.
3191
3192 =cut
3193
3194 1;
3195