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