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