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