typo
[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 $date_format $rdate_format );
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 Storable qw( freeze thaw );
15 use FS::UID qw( datasrc );
16 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
17 use FS::Record qw( qsearch qsearchs dbh );
18 use FS::cust_main_Mixin;
19 use FS::cust_main;
20 use FS::cust_statement;
21 use FS::cust_bill_pkg;
22 use FS::cust_bill_pkg_display;
23 use FS::cust_bill_pkg_detail;
24 use FS::cust_credit;
25 use FS::cust_pay;
26 use FS::cust_pkg;
27 use FS::cust_credit_bill;
28 use FS::pay_batch;
29 use FS::cust_pay_batch;
30 use FS::cust_bill_event;
31 use FS::cust_event;
32 use FS::part_pkg;
33 use FS::cust_bill_pay;
34 use FS::cust_bill_pay_batch;
35 use FS::part_bill_event;
36 use FS::payby;
37
38 @ISA = qw( FS::cust_main_Mixin FS::Record );
39
40 $DEBUG = 0;
41 $me = '[FS::cust_bill]';
42
43 #ask FS::UID to run this stuff for us later
44 FS::UID->install_callback( sub { 
45   $conf = new FS::Conf;
46   $money_char   = $conf->config('money_char')  || '$';  
47   $date_format  = $conf->config('date_format') || '%x';  
48   $rdate_format = $conf->config('date_format') || '%m/%d/%Y';  
49 } );
50
51 =head1 NAME
52
53 FS::cust_bill - Object methods for cust_bill records
54
55 =head1 SYNOPSIS
56
57   use FS::cust_bill;
58
59   $record = new FS::cust_bill \%hash;
60   $record = new FS::cust_bill { 'column' => 'value' };
61
62   $error = $record->insert;
63
64   $error = $new_record->replace($old_record);
65
66   $error = $record->delete;
67
68   $error = $record->check;
69
70   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
71
72   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
73
74   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
75
76   @cust_pay_objects = $cust_bill->cust_pay;
77
78   $tax_amount = $record->tax;
79
80   @lines = $cust_bill->print_text;
81   @lines = $cust_bill->print_text $time;
82
83 =head1 DESCRIPTION
84
85 An FS::cust_bill object represents an invoice; a declaration that a customer
86 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
87 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
88 following fields are currently supported:
89
90 Regular fields
91
92 =over 4
93
94 =item invnum - primary key (assigned automatically for new invoices)
95
96 =item custnum - customer (see L<FS::cust_main>)
97
98 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
99 L<Time::Local> and L<Date::Parse> for conversion functions.
100
101 =item charged - amount of this invoice
102
103 =item invoice_terms - optional terms override for this specific invoice
104
105 =back
106
107 Customer info at invoice generation time
108
109 =over 4
110
111 =item previous_balance
112
113 =item billing_balance
114
115 =back
116
117 Deprecated
118
119 =over 4
120
121 =item printed - deprecated
122
123 =back
124
125 Specific use cases
126
127 =over 4
128
129 =item closed - books closed flag, empty or `Y'
130
131 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
132
133 =item agent_invid - legacy invoice number
134
135 =back
136
137 =head1 METHODS
138
139 =over 4
140
141 =item new HASHREF
142
143 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
144 Invoices are normally created by calling the bill method of a customer object
145 (see L<FS::cust_main>).
146
147 =cut
148
149 sub table { 'cust_bill'; }
150
151 sub cust_linked { $_[0]->cust_main_custnum; } 
152 sub cust_unlinked_msg {
153   my $self = shift;
154   "WARNING: can't find cust_main.custnum ". $self->custnum.
155   ' (cust_bill.invnum '. $self->invnum. ')';
156 }
157
158 =item insert
159
160 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
161 returns the error, otherwise returns false.
162
163 =item delete
164
165 This method now works but you probably shouldn't use it.  Instead, apply a
166 credit against the invoice.
167
168 Using this method to delete invoices outright is really, really bad.  There
169 would be no record you ever posted this invoice, and there are no check to
170 make sure charged = 0 or that there are no associated cust_bill_pkg records.
171
172 Really, don't use it.
173
174 =cut
175
176 sub delete {
177   my $self = shift;
178   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
179
180   local $SIG{HUP} = 'IGNORE';
181   local $SIG{INT} = 'IGNORE';
182   local $SIG{QUIT} = 'IGNORE';
183   local $SIG{TERM} = 'IGNORE';
184   local $SIG{TSTP} = 'IGNORE';
185   local $SIG{PIPE} = 'IGNORE';
186
187   my $oldAutoCommit = $FS::UID::AutoCommit;
188   local $FS::UID::AutoCommit = 0;
189   my $dbh = dbh;
190
191   foreach my $table (qw(
192     cust_bill_event
193     cust_event
194     cust_credit_bill
195     cust_bill_pay
196     cust_bill_pay
197     cust_credit_bill
198     cust_pay_batch
199     cust_bill_pay_batch
200     cust_bill_pkg
201   )) {
202
203     foreach my $linked ( $self->$table() ) {
204       my $error = $linked->delete;
205       if ( $error ) {
206         $dbh->rollback if $oldAutoCommit;
207         return $error;
208       }
209     }
210
211   }
212
213   my $error = $self->SUPER::delete(@_);
214   if ( $error ) {
215     $dbh->rollback if $oldAutoCommit;
216     return $error;
217   }
218
219   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
220
221   '';
222
223 }
224
225 =item replace OLD_RECORD
226
227 Replaces the OLD_RECORD with this one in the database.  If there is an error,
228 returns the error, otherwise returns false.
229
230 Only printed may be changed.  printed is normally updated by calling the
231 collect method of a customer object (see L<FS::cust_main>).
232
233 =cut
234
235 #replace can be inherited from Record.pm
236
237 # replace_check is now the preferred way to #implement replace data checks
238 # (so $object->replace() works without an argument)
239
240 sub replace_check {
241   my( $new, $old ) = ( shift, shift );
242   return "Can't change custnum!" unless $old->custnum == $new->custnum;
243   #return "Can't change _date!" unless $old->_date eq $new->_date;
244   return "Can't change _date!" unless $old->_date == $new->_date;
245   return "Can't change charged!" unless $old->charged == $new->charged
246                                      || $old->charged == 0;
247
248   '';
249 }
250
251 =item check
252
253 Checks all fields to make sure this is a valid invoice.  If there is an error,
254 returns the error, otherwise returns false.  Called by the insert and replace
255 methods.
256
257 =cut
258
259 sub check {
260   my $self = shift;
261
262   my $error =
263     $self->ut_numbern('invnum')
264     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
265     || $self->ut_numbern('_date')
266     || $self->ut_money('charged')
267     || $self->ut_numbern('printed')
268     || $self->ut_enum('closed', [ '', 'Y' ])
269     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
270     || $self->ut_numbern('agent_invid') #varchar?
271   ;
272   return $error if $error;
273
274   $self->_date(time) unless $self->_date;
275
276   $self->printed(0) if $self->printed eq '';
277
278   $self->SUPER::check;
279 }
280
281 =item display_invnum
282
283 Returns the displayed invoice number for this invoice: agent_invid if
284 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
285
286 =cut
287
288 sub display_invnum {
289   my $self = shift;
290   if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
291     return $self->agent_invid;
292   } else {
293     return $self->invnum;
294   }
295 }
296
297 =item previous
298
299 Returns a list consisting of the total previous balance for this customer, 
300 followed by the previous outstanding invoices (as FS::cust_bill objects also).
301
302 =cut
303
304 sub previous {
305   my $self = shift;
306   my $total = 0;
307   my @cust_bill = sort { $a->_date <=> $b->_date }
308     grep { $_->owed != 0 && $_->_date < $self->_date }
309       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
310   ;
311   foreach ( @cust_bill ) { $total += $_->owed; }
312   $total, @cust_bill;
313 }
314
315 =item cust_bill_pkg
316
317 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
318
319 =cut
320
321 sub cust_bill_pkg {
322   my $self = shift;
323   qsearch(
324     { 'table'    => 'cust_bill_pkg',
325       'hashref'  => { 'invnum' => $self->invnum },
326       'order_by' => 'ORDER BY billpkgnum',
327     }
328   );
329 }
330
331 =item cust_bill_pkg_pkgnum PKGNUM
332
333 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
334 specified pkgnum.
335
336 =cut
337
338 sub cust_bill_pkg_pkgnum {
339   my( $self, $pkgnum ) = @_;
340   qsearch(
341     { 'table'    => 'cust_bill_pkg',
342       'hashref'  => { 'invnum' => $self->invnum,
343                       'pkgnum' => $pkgnum,
344                     },
345       'order_by' => 'ORDER BY billpkgnum',
346     }
347   );
348 }
349
350 =item cust_pkg
351
352 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
353 this invoice.
354
355 =cut
356
357 sub cust_pkg {
358   my $self = shift;
359   my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
360                      $self->cust_bill_pkg;
361   my %saw = ();
362   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
363 }
364
365 =item no_auto
366
367 Returns true if any of the packages (or their definitions) corresponding to the
368 line items for this invoice have the no_auto flag set.
369
370 =cut
371
372 sub no_auto {
373   my $self = shift;
374   grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
375 }
376
377 =item open_cust_bill_pkg
378
379 Returns the open line items for this invoice.
380
381 Note that cust_bill_pkg with both setup and recur fees are returned as two
382 separate line items, each with only one fee.
383
384 =cut
385
386 # modeled after cust_main::open_cust_bill
387 sub open_cust_bill_pkg {
388   my $self = shift;
389
390   # grep { $_->owed > 0 } $self->cust_bill_pkg
391
392   my %other = ( 'recur' => 'setup',
393                 'setup' => 'recur', );
394   my @open = ();
395   foreach my $field ( qw( recur setup )) {
396     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
397                 grep { $_->owed($field) > 0 }
398                 $self->cust_bill_pkg;
399   }
400
401   @open;
402 }
403
404 =item cust_bill_event
405
406 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
407
408 =cut
409
410 sub cust_bill_event {
411   my $self = shift;
412   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
413 }
414
415 =item num_cust_bill_event
416
417 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
418
419 =cut
420
421 sub num_cust_bill_event {
422   my $self = shift;
423   my $sql =
424     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
425   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
426   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
427   $sth->fetchrow_arrayref->[0];
428 }
429
430 =item cust_event
431
432 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
433
434 =cut
435
436 #false laziness w/cust_pkg.pm
437 sub cust_event {
438   my $self = shift;
439   qsearch({
440     'table'     => 'cust_event',
441     'addl_from' => 'JOIN part_event USING ( eventpart )',
442     'hashref'   => { 'tablenum' => $self->invnum },
443     'extra_sql' => " AND eventtable = 'cust_bill' ",
444   });
445 }
446
447 =item num_cust_event
448
449 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
450
451 =cut
452
453 #false laziness w/cust_pkg.pm
454 sub num_cust_event {
455   my $self = shift;
456   my $sql =
457     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
458     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
459   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
460   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
461   $sth->fetchrow_arrayref->[0];
462 }
463
464 =item cust_main
465
466 Returns the customer (see L<FS::cust_main>) for this invoice.
467
468 =cut
469
470 sub cust_main {
471   my $self = shift;
472   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
473 }
474
475 =item cust_suspend_if_balance_over AMOUNT
476
477 Suspends the customer associated with this invoice if the total amount owed on
478 this invoice and all older invoices is greater than the specified amount.
479
480 Returns a list: an empty list on success or a list of errors.
481
482 =cut
483
484 sub cust_suspend_if_balance_over {
485   my( $self, $amount ) = ( shift, shift );
486   my $cust_main = $self->cust_main;
487   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
488     return ();
489   } else {
490     $cust_main->suspend(@_);
491   }
492 }
493
494 =item cust_credit
495
496 Depreciated.  See the cust_credited method.
497
498  #Returns a list consisting of the total previous credited (see
499  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
500  #outstanding credits (FS::cust_credit objects).
501
502 =cut
503
504 sub cust_credit {
505   use Carp;
506   croak "FS::cust_bill->cust_credit depreciated; see ".
507         "FS::cust_bill->cust_credit_bill";
508   #my $self = shift;
509   #my $total = 0;
510   #my @cust_credit = sort { $a->_date <=> $b->_date }
511   #  grep { $_->credited != 0 && $_->_date < $self->_date }
512   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
513   #;
514   #foreach (@cust_credit) { $total += $_->credited; }
515   #$total, @cust_credit;
516 }
517
518 =item cust_pay
519
520 Depreciated.  See the cust_bill_pay method.
521
522 #Returns all payments (see L<FS::cust_pay>) for this invoice.
523
524 =cut
525
526 sub cust_pay {
527   use Carp;
528   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
529   #my $self = shift;
530   #sort { $a->_date <=> $b->_date }
531   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
532   #;
533 }
534
535 sub cust_pay_batch {
536   my $self = shift;
537   qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
538 }
539
540 sub cust_bill_pay_batch {
541   my $self = shift;
542   qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
543 }
544
545 =item cust_bill_pay
546
547 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
548
549 =cut
550
551 sub cust_bill_pay {
552   my $self = shift;
553   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
554   sort { $a->_date <=> $b->_date }
555     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
556 }
557
558 =item cust_credited
559
560 =item cust_credit_bill
561
562 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
563
564 =cut
565
566 sub cust_credited {
567   my $self = shift;
568   map { $_ } #return $self->num_cust_credit_bill unless wantarray;
569   sort { $a->_date <=> $b->_date }
570     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
571   ;
572 }
573
574 sub cust_credit_bill {
575   shift->cust_credited(@_);
576 }
577
578 =item cust_bill_pay_pkgnum PKGNUM
579
580 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
581 with matching pkgnum.
582
583 =cut
584
585 sub cust_bill_pay_pkgnum {
586   my( $self, $pkgnum ) = @_;
587   map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
588   sort { $a->_date <=> $b->_date }
589     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
590                                 'pkgnum' => $pkgnum,
591                               }
592            );
593 }
594
595 =item cust_credited_pkgnum PKGNUM
596
597 =item cust_credit_bill_pkgnum PKGNUM
598
599 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
600 with matching pkgnum.
601
602 =cut
603
604 sub cust_credited_pkgnum {
605   my( $self, $pkgnum ) = @_;
606   map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
607   sort { $a->_date <=> $b->_date }
608     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
609                                    'pkgnum' => $pkgnum,
610                                  }
611            );
612 }
613
614 sub cust_credit_bill_pkgnum {
615   shift->cust_credited_pkgnum(@_);
616 }
617
618 =item tax
619
620 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
621
622 =cut
623
624 sub tax {
625   my $self = shift;
626   my $total = 0;
627   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
628                                              'pkgnum' => 0 } );
629   foreach (@taxlines) { $total += $_->setup; }
630   $total;
631 }
632
633 =item owed
634
635 Returns the amount owed (still outstanding) on this invoice, which is charged
636 minus all payment applications (see L<FS::cust_bill_pay>) and credit
637 applications (see L<FS::cust_credit_bill>).
638
639 =cut
640
641 sub owed {
642   my $self = shift;
643   my $balance = $self->charged;
644   $balance -= $_->amount foreach ( $self->cust_bill_pay );
645   $balance -= $_->amount foreach ( $self->cust_credited );
646   $balance = sprintf( "%.2f", $balance);
647   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
648   $balance;
649 }
650
651 sub owed_pkgnum {
652   my( $self, $pkgnum ) = @_;
653
654   #my $balance = $self->charged;
655   my $balance = 0;
656   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
657
658   $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
659   $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
660
661   $balance = sprintf( "%.2f", $balance);
662   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
663   $balance;
664 }
665
666 =item apply_payments_and_credits [ OPTION => VALUE ... ]
667
668 Applies unapplied payments and credits to this invoice.
669
670 A hash of optional arguments may be passed.  Currently "manual" is supported.
671 If true, a payment receipt is sent instead of a statement when
672 'payment_receipt_email' configuration option is set.
673
674 If there is an error, returns the error, otherwise returns false.
675
676 =cut
677
678 sub apply_payments_and_credits {
679   my( $self, %options ) = @_;
680
681   local $SIG{HUP} = 'IGNORE';
682   local $SIG{INT} = 'IGNORE';
683   local $SIG{QUIT} = 'IGNORE';
684   local $SIG{TERM} = 'IGNORE';
685   local $SIG{TSTP} = 'IGNORE';
686   local $SIG{PIPE} = 'IGNORE';
687
688   my $oldAutoCommit = $FS::UID::AutoCommit;
689   local $FS::UID::AutoCommit = 0;
690   my $dbh = dbh;
691
692   $self->select_for_update; #mutex
693
694   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
695   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
696
697   if ( $conf->exists('pkg-balances') ) {
698     # limit @payments & @credits to those w/ a pkgnum grepped from $self
699     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
700     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
701     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
702   }
703
704   while ( $self->owed > 0 and ( @payments || @credits ) ) {
705
706     my $app = '';
707     if ( @payments && @credits ) {
708
709       #decide which goes first by weight of top (unapplied) line item
710
711       my @open_lineitems = $self->open_cust_bill_pkg;
712
713       my $max_pay_weight =
714         max( map  { $_->part_pkg->pay_weight || 0 }
715              grep { $_ }
716              map  { $_->cust_pkg }
717                   @open_lineitems
718            );
719       my $max_credit_weight =
720         max( map  { $_->part_pkg->credit_weight || 0 }
721              grep { $_ } 
722              map  { $_->cust_pkg }
723                   @open_lineitems
724            );
725
726       #if both are the same... payments first?  it has to be something
727       if ( $max_pay_weight >= $max_credit_weight ) {
728         $app = 'pay';
729       } else {
730         $app = 'credit';
731       }
732     
733     } elsif ( @payments ) {
734       $app = 'pay';
735     } elsif ( @credits ) {
736       $app = 'credit';
737     } else {
738       die "guru meditation #12 and 35";
739     }
740
741     my $unapp_amount;
742     if ( $app eq 'pay' ) {
743
744       my $payment = shift @payments;
745       $unapp_amount = $payment->unapplied;
746       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
747       $app->pkgnum( $payment->pkgnum )
748         if $conf->exists('pkg-balances') && $payment->pkgnum;
749
750     } elsif ( $app eq 'credit' ) {
751
752       my $credit = shift @credits;
753       $unapp_amount = $credit->credited;
754       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
755       $app->pkgnum( $credit->pkgnum )
756         if $conf->exists('pkg-balances') && $credit->pkgnum;
757
758     } else {
759       die "guru meditation #12 and 35";
760     }
761
762     my $owed;
763     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
764       warn "owed_pkgnum ". $app->pkgnum;
765       $owed = $self->owed_pkgnum($app->pkgnum);
766     } else {
767       $owed = $self->owed;
768     }
769     next unless $owed > 0;
770
771     warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
772     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
773
774     $app->invnum( $self->invnum );
775
776     my $error = $app->insert(%options);
777     if ( $error ) {
778       $dbh->rollback if $oldAutoCommit;
779       return "Error inserting ". $app->table. " record: $error";
780     }
781     die $error if $error;
782
783   }
784
785   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
786   ''; #no error
787
788 }
789
790 =item generate_email OPTION => VALUE ...
791
792 Options:
793
794 =over 4
795
796 =item from
797
798 sender address, required
799
800 =item tempate
801
802 alternate template name, optional
803
804 =item print_text
805
806 text attachment arrayref, optional
807
808 =item subject
809
810 email subject, optional
811
812 =item notice_name
813
814 notice name instead of "Invoice", optional
815
816 =back
817
818 Returns an argument list to be passed to L<FS::Misc::send_email>.
819
820 =cut
821
822 use MIME::Entity;
823
824 sub generate_email {
825
826   my $self = shift;
827   my %args = @_;
828
829   my $me = '[FS::cust_bill::generate_email]';
830
831   my %return = (
832     'from'      => $args{'from'},
833     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
834   );
835
836   my %opt = (
837     'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
838     'template'      => $args{'template'},
839     'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
840   );
841
842   my $cust_main = $self->cust_main;
843
844   if (ref($args{'to'}) eq 'ARRAY') {
845     $return{'to'} = $args{'to'};
846   } else {
847     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
848                            $cust_main->invoicing_list
849                     ];
850   }
851
852   if ( $conf->exists('invoice_html') ) {
853
854     warn "$me creating HTML/text multipart message"
855       if $DEBUG;
856
857     $return{'nobody'} = 1;
858
859     my $alternative = build MIME::Entity
860       'Type'        => 'multipart/alternative',
861       'Encoding'    => '7bit',
862       'Disposition' => 'inline'
863     ;
864
865     my $data;
866     if ( $conf->exists('invoice_email_pdf')
867          and scalar($conf->config('invoice_email_pdf_note')) ) {
868
869       warn "$me using 'invoice_email_pdf_note' in multipart message"
870         if $DEBUG;
871       $data = [ map { $_ . "\n" }
872                     $conf->config('invoice_email_pdf_note')
873               ];
874
875     } else {
876
877       warn "$me not using 'invoice_email_pdf_note' in multipart message"
878         if $DEBUG;
879       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
880         $data = $args{'print_text'};
881       } else {
882         $data = [ $self->print_text(\%opt) ];
883       }
884
885     }
886
887     $alternative->attach(
888       'Type'        => 'text/plain',
889       #'Encoding'    => 'quoted-printable',
890       'Encoding'    => '7bit',
891       'Data'        => $data,
892       'Disposition' => 'inline',
893     );
894
895     $args{'from'} =~ /\@([\w\.\-]+)/;
896     my $from = $1 || 'example.com';
897     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
898
899     my $logo;
900     my $agentnum = $cust_main->agentnum;
901     if ( defined($args{'template'}) && length($args{'template'})
902          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
903        )
904     {
905       $logo = 'logo_'. $args{'template'}. '.png';
906     } else {
907       $logo = "logo.png";
908     }
909     my $image_data = $conf->config_binary( $logo, $agentnum);
910
911     my $image = build MIME::Entity
912       'Type'       => 'image/png',
913       'Encoding'   => 'base64',
914       'Data'       => $image_data,
915       'Filename'   => 'logo.png',
916       'Content-ID' => "<$content_id>",
917     ;
918
919     $alternative->attach(
920       'Type'        => 'text/html',
921       'Encoding'    => 'quoted-printable',
922       'Data'        => [ '<html>',
923                          '  <head>',
924                          '    <title>',
925                          '      '. encode_entities($return{'subject'}), 
926                          '    </title>',
927                          '  </head>',
928                          '  <body bgcolor="#e8e8e8">',
929                          $self->print_html({ 'cid'=>$content_id, %opt }),
930                          '  </body>',
931                          '</html>',
932                        ],
933       'Disposition' => 'inline',
934       #'Filename'    => 'invoice.pdf',
935     );
936
937     my @otherparts = ();
938     if ( $cust_main->email_csv_cdr ) {
939
940       push @otherparts, build MIME::Entity
941         'Type'        => 'text/csv',
942         'Encoding'    => '7bit',
943         'Data'        => [ map { "$_\n" }
944                              $self->call_details('prepend_billed_number' => 1)
945                          ],
946         'Disposition' => 'attachment',
947         'Filename'    => 'usage-'. $self->invnum. '.csv',
948       ;
949
950     }
951
952     if ( $conf->exists('invoice_email_pdf') ) {
953
954       #attaching pdf too:
955       # multipart/mixed
956       #   multipart/related
957       #     multipart/alternative
958       #       text/plain
959       #       text/html
960       #     image/png
961       #   application/pdf
962
963       my $related = build MIME::Entity 'Type'     => 'multipart/related',
964                                        'Encoding' => '7bit';
965
966       #false laziness w/Misc::send_email
967       $related->head->replace('Content-type',
968         $related->mime_type.
969         '; boundary="'. $related->head->multipart_boundary. '"'.
970         '; type=multipart/alternative'
971       );
972
973       $related->add_part($alternative);
974
975       $related->add_part($image);
976
977       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
978
979       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
980
981     } else {
982
983       #no other attachment:
984       # multipart/related
985       #   multipart/alternative
986       #     text/plain
987       #     text/html
988       #   image/png
989
990       $return{'content-type'} = 'multipart/related';
991       $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
992       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
993       #$return{'disposition'} = 'inline';
994
995     }
996   
997   } else {
998
999     if ( $conf->exists('invoice_email_pdf') ) {
1000       warn "$me creating PDF attachment"
1001         if $DEBUG;
1002
1003       #mime parts arguments a la MIME::Entity->build().
1004       $return{'mimeparts'} = [
1005         { $self->mimebuild_pdf(\%opt) }
1006       ];
1007     }
1008   
1009     if ( $conf->exists('invoice_email_pdf')
1010          and scalar($conf->config('invoice_email_pdf_note')) ) {
1011
1012       warn "$me using 'invoice_email_pdf_note'"
1013         if $DEBUG;
1014       $return{'body'} = [ map { $_ . "\n" }
1015                               $conf->config('invoice_email_pdf_note')
1016                         ];
1017
1018     } else {
1019
1020       warn "$me not using 'invoice_email_pdf_note'"
1021         if $DEBUG;
1022       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1023         $return{'body'} = $args{'print_text'};
1024       } else {
1025         $return{'body'} = [ $self->print_text(\%opt) ];
1026       }
1027
1028     }
1029
1030   }
1031
1032   %return;
1033
1034 }
1035
1036 =item mimebuild_pdf
1037
1038 Returns a list suitable for passing to MIME::Entity->build(), representing
1039 this invoice as PDF attachment.
1040
1041 =cut
1042
1043 sub mimebuild_pdf {
1044   my $self = shift;
1045   (
1046     'Type'        => 'application/pdf',
1047     'Encoding'    => 'base64',
1048     'Data'        => [ $self->print_pdf(@_) ],
1049     'Disposition' => 'attachment',
1050     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
1051   );
1052 }
1053
1054 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1055
1056 Sends this invoice to the destinations configured for this customer: sends
1057 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1058
1059 Options can be passed as a hashref (recommended) or as a list of up to 
1060 four values for templatename, agentnum, invoice_from and amount.
1061
1062 I<template>, if specified, is the name of a suffix for alternate invoices.
1063
1064 I<agentnum>, if specified, means that this invoice will only be sent for customers
1065 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
1066 single agent) or an arrayref of agentnums.
1067
1068 I<invoice_from>, if specified, overrides the default email invoice From: address.
1069
1070 I<amount>, if specified, only sends the invoice if the total amount owed on this
1071 invoice and all older invoices is greater than the specified amount.
1072
1073 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1074
1075 =cut
1076
1077 sub queueable_send {
1078   my %opt = @_;
1079
1080   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1081     or die "invalid invoice number: " . $opt{invnum};
1082
1083   my @args = ( $opt{template}, $opt{agentnum} );
1084   push @args, $opt{invoice_from}
1085     if exists($opt{invoice_from}) && $opt{invoice_from};
1086
1087   my $error = $self->send( @args );
1088   die $error if $error;
1089
1090 }
1091
1092 sub send {
1093   my $self = shift;
1094
1095   my( $template, $invoice_from, $notice_name );
1096   my $agentnums = '';
1097   my $balance_over = 0;
1098
1099   if ( ref($_[0]) ) {
1100     my $opt = shift;
1101     $template = $opt->{'template'} || '';
1102     if ( $agentnums = $opt->{'agentnum'} ) {
1103       $agentnums = [ $agentnums ] unless ref($agentnums);
1104     }
1105     $invoice_from = $opt->{'invoice_from'};
1106     $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1107     $notice_name = $opt->{'notice_name'};
1108   } else {
1109     $template = scalar(@_) ? shift : '';
1110     if ( scalar(@_) && $_[0]  ) {
1111       $agentnums = ref($_[0]) ? shift : [ shift ];
1112     }
1113     $invoice_from = shift if scalar(@_);
1114     $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1115   }
1116
1117   return 'N/A' unless ! $agentnums
1118                    or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1119
1120   return ''
1121     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1122
1123   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1124                     $conf->config('invoice_from', $self->cust_main->agentnum );
1125
1126   my %opt = (
1127     'template'     => $template,
1128     'invoice_from' => $invoice_from,
1129     'notice_name'  => ( $notice_name || 'Invoice' ),
1130   );
1131
1132   my @invoicing_list = $self->cust_main->invoicing_list;
1133
1134   #$self->email_invoice(\%opt)
1135   $self->email(\%opt)
1136     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1137
1138   #$self->print_invoice(\%opt)
1139   $self->print(\%opt)
1140     if grep { $_ eq 'POST' } @invoicing_list; #postal
1141
1142   $self->fax_invoice(\%opt)
1143     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1144
1145   '';
1146
1147 }
1148
1149 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
1150
1151 Emails this invoice.
1152
1153 Options can be passed as a hashref (recommended) or as a list of up to 
1154 two values for templatename and invoice_from.
1155
1156 I<template>, if specified, is the name of a suffix for alternate invoices.
1157
1158 I<invoice_from>, if specified, overrides the default email invoice From: address.
1159
1160 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1161
1162 =cut
1163
1164 sub queueable_email {
1165   my %opt = @_;
1166
1167   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1168     or die "invalid invoice number: " . $opt{invnum};
1169
1170   my @args = ( $opt{template} );
1171   push @args, $opt{invoice_from}
1172     if exists($opt{invoice_from}) && $opt{invoice_from};
1173
1174   my $error = $self->email( @args );
1175   die $error if $error;
1176
1177 }
1178
1179 #sub email_invoice {
1180 sub email {
1181   my $self = shift;
1182
1183   my( $template, $invoice_from, $notice_name );
1184   if ( ref($_[0]) ) {
1185     my $opt = shift;
1186     $template = $opt->{'template'} || '';
1187     $invoice_from = $opt->{'invoice_from'};
1188     $notice_name = $opt->{'notice_name'} || 'Invoice';
1189   } else {
1190     $template = scalar(@_) ? shift : '';
1191     $invoice_from = shift if scalar(@_);
1192     $notice_name = 'Invoice';
1193   }
1194
1195   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1196                     $conf->config('invoice_from', $self->cust_main->agentnum );
1197
1198   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1199                             $self->cust_main->invoicing_list;
1200
1201   #better to notify this person than silence
1202   @invoicing_list = ($invoice_from) unless @invoicing_list;
1203
1204   my $subject = $self->email_subject($template);
1205
1206   my $error = send_email(
1207     $self->generate_email(
1208       'from'        => $invoice_from,
1209       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1210       'subject'     => $subject,
1211       'template'    => $template,
1212       'notice_name' => $notice_name,
1213     )
1214   );
1215   die "can't email invoice: $error\n" if $error;
1216   #die "$error\n" if $error;
1217
1218 }
1219
1220 sub email_subject {
1221   my $self = shift;
1222
1223   #my $template = scalar(@_) ? shift : '';
1224   #per-template?
1225
1226   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1227                 || 'Invoice';
1228
1229   my $cust_main = $self->cust_main;
1230   my $name = $cust_main->name;
1231   my $name_short = $cust_main->name_short;
1232   my $invoice_number = $self->invnum;
1233   my $invoice_date = $self->_date_pretty;
1234
1235   eval qq("$subject");
1236 }
1237
1238 =item lpr_data HASHREF | [ TEMPLATE ]
1239
1240 Returns the postscript or plaintext for this invoice as an arrayref.
1241
1242 Options can be passed as a hashref (recommended) or as a single optional value
1243 for template.
1244
1245 I<template>, if specified, is the name of a suffix for alternate invoices.
1246
1247 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1248
1249 =cut
1250
1251 sub lpr_data {
1252   my $self = shift;
1253   my( $template, $notice_name );
1254   if ( ref($_[0]) ) {
1255     my $opt = shift;
1256     $template = $opt->{'template'} || '';
1257     $notice_name = $opt->{'notice_name'} || 'Invoice';
1258   } else {
1259     $template = scalar(@_) ? shift : '';
1260     $notice_name = 'Invoice';
1261   }
1262
1263   my %opt = (
1264     'template'    => $template,
1265     'notice_name' => $notice_name,
1266   );
1267
1268   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1269   [ $self->$method( \%opt ) ];
1270 }
1271
1272 =item print HASHREF | [ TEMPLATE ]
1273
1274 Prints this invoice.
1275
1276 Options can be passed as a hashref (recommended) or as a single optional
1277 value for template.
1278
1279 I<template>, if specified, is the name of a suffix for alternate invoices.
1280
1281 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1282
1283 =cut
1284
1285 #sub print_invoice {
1286 sub print {
1287   my $self = shift;
1288   my( $template, $notice_name );
1289   if ( ref($_[0]) ) {
1290     my $opt = shift;
1291     $template = $opt->{'template'} || '';
1292     $notice_name = $opt->{'notice_name'} || 'Invoice';
1293   } else {
1294     $template = scalar(@_) ? shift : '';
1295     $notice_name = 'Invoice';
1296   }
1297
1298   my %opt = (
1299     'template'    => $template,
1300     'notice_name' => $notice_name,
1301   );
1302
1303   do_print $self->lpr_data(\%opt);
1304 }
1305
1306 =item fax_invoice HASHREF | [ TEMPLATE ] 
1307
1308 Faxes this invoice.
1309
1310 Options can be passed as a hashref (recommended) or as a single optional
1311 value for template.
1312
1313 I<template>, if specified, is the name of a suffix for alternate invoices.
1314
1315 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1316
1317 =cut
1318
1319 sub fax_invoice {
1320   my $self = shift;
1321   my( $template, $notice_name );
1322   if ( ref($_[0]) ) {
1323     my $opt = shift;
1324     $template = $opt->{'template'} || '';
1325     $notice_name = $opt->{'notice_name'} || 'Invoice';
1326   } else {
1327     $template = scalar(@_) ? shift : '';
1328     $notice_name = 'Invoice';
1329   }
1330
1331   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1332     unless $conf->exists('invoice_latex');
1333
1334   my $dialstring = $self->cust_main->getfield('fax');
1335   #Check $dialstring?
1336
1337   my %opt = (
1338     'template'    => $template,
1339     'notice_name' => $notice_name,
1340   );
1341
1342   my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
1343                         'dialstring' => $dialstring,
1344                       );
1345   die $error if $error;
1346
1347 }
1348
1349 =item ftp_invoice [ TEMPLATENAME ] 
1350
1351 Sends this invoice data via FTP.
1352
1353 TEMPLATENAME is unused?
1354
1355 =cut
1356
1357 sub ftp_invoice {
1358   my $self = shift;
1359   my $template = scalar(@_) ? shift : '';
1360
1361   $self->send_csv(
1362     'protocol'   => 'ftp',
1363     'server'     => $conf->config('cust_bill-ftpserver'),
1364     'username'   => $conf->config('cust_bill-ftpusername'),
1365     'password'   => $conf->config('cust_bill-ftppassword'),
1366     'dir'        => $conf->config('cust_bill-ftpdir'),
1367     'format'     => $conf->config('cust_bill-ftpformat'),
1368   );
1369 }
1370
1371 =item spool_invoice [ TEMPLATENAME ] 
1372
1373 Spools this invoice data (see L<FS::spool_csv>)
1374
1375 TEMPLATENAME is unused?
1376
1377 =cut
1378
1379 sub spool_invoice {
1380   my $self = shift;
1381   my $template = scalar(@_) ? shift : '';
1382
1383   $self->spool_csv(
1384     'format'       => $conf->config('cust_bill-spoolformat'),
1385     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1386   );
1387 }
1388
1389 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1390
1391 Like B<send>, but only sends the invoice if it is the newest open invoice for
1392 this customer.
1393
1394 =cut
1395
1396 sub send_if_newest {
1397   my $self = shift;
1398
1399   return ''
1400     if scalar(
1401                grep { $_->owed > 0 } 
1402                     qsearch('cust_bill', {
1403                       'custnum' => $self->custnum,
1404                       #'_date'   => { op=>'>', value=>$self->_date },
1405                       'invnum'  => { op=>'>', value=>$self->invnum },
1406                     } )
1407              );
1408     
1409   $self->send(@_);
1410 }
1411
1412 =item send_csv OPTION => VALUE, ...
1413
1414 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1415
1416 Options are:
1417
1418 protocol - currently only "ftp"
1419 server
1420 username
1421 password
1422 dir
1423
1424 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1425 and YYMMDDHHMMSS is a timestamp.
1426
1427 See L</print_csv> for a description of the output format.
1428
1429 =cut
1430
1431 sub send_csv {
1432   my($self, %opt) = @_;
1433
1434   #create file(s)
1435
1436   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1437   mkdir $spooldir, 0700 unless -d $spooldir;
1438
1439   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1440   my $file = "$spooldir/$tracctnum.csv";
1441   
1442   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1443
1444   open(CSV, ">$file") or die "can't open $file: $!";
1445   print CSV $header;
1446
1447   print CSV $detail;
1448
1449   close CSV;
1450
1451   my $net;
1452   if ( $opt{protocol} eq 'ftp' ) {
1453     eval "use Net::FTP;";
1454     die $@ if $@;
1455     $net = Net::FTP->new($opt{server}) or die @$;
1456   } else {
1457     die "unknown protocol: $opt{protocol}";
1458   }
1459
1460   $net->login( $opt{username}, $opt{password} )
1461     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1462
1463   $net->binary or die "can't set binary mode";
1464
1465   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1466
1467   $net->put($file) or die "can't put $file: $!";
1468
1469   $net->quit;
1470
1471   unlink $file;
1472
1473 }
1474
1475 =item spool_csv
1476
1477 Spools CSV invoice data.
1478
1479 Options are:
1480
1481 =over 4
1482
1483 =item format - 'default' or 'billco'
1484
1485 =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>).
1486
1487 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1488
1489 =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.
1490
1491 =back
1492
1493 =cut
1494
1495 sub spool_csv {
1496   my($self, %opt) = @_;
1497
1498   my $cust_main = $self->cust_main;
1499
1500   if ( $opt{'dest'} ) {
1501     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1502                              $cust_main->invoicing_list;
1503     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1504                      || ! keys %invoicing_list;
1505   }
1506
1507   if ( $opt{'balanceover'} ) {
1508     return 'N/A'
1509       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1510   }
1511
1512   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1513   mkdir $spooldir, 0700 unless -d $spooldir;
1514
1515   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1516
1517   my $file =
1518     "$spooldir/".
1519     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1520     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1521     '.csv';
1522   
1523   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1524
1525   open(CSV, ">>$file") or die "can't open $file: $!";
1526   flock(CSV, LOCK_EX);
1527   seek(CSV, 0, 2);
1528
1529   print CSV $header;
1530
1531   if ( lc($opt{'format'}) eq 'billco' ) {
1532
1533     flock(CSV, LOCK_UN);
1534     close CSV;
1535
1536     $file =
1537       "$spooldir/".
1538       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1539       '-detail.csv';
1540
1541     open(CSV,">>$file") or die "can't open $file: $!";
1542     flock(CSV, LOCK_EX);
1543     seek(CSV, 0, 2);
1544   }
1545
1546   print CSV $detail;
1547
1548   flock(CSV, LOCK_UN);
1549   close CSV;
1550
1551   return '';
1552
1553 }
1554
1555 =item print_csv OPTION => VALUE, ...
1556
1557 Returns CSV data for this invoice.
1558
1559 Options are:
1560
1561 format - 'default' or 'billco'
1562
1563 Returns a list consisting of two scalars.  The first is a single line of CSV
1564 header information for this invoice.  The second is one or more lines of CSV
1565 detail information for this invoice.
1566
1567 If I<format> is not specified or "default", the fields of the CSV file are as
1568 follows:
1569
1570 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1571
1572 =over 4
1573
1574 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1575
1576 B<record_type> is C<cust_bill> for the initial header line only.  The
1577 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1578 fields are filled in.
1579
1580 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1581 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1582 are filled in.
1583
1584 =item invnum - invoice number
1585
1586 =item custnum - customer number
1587
1588 =item _date - invoice date
1589
1590 =item charged - total invoice amount
1591
1592 =item first - customer first name
1593
1594 =item last - customer first name
1595
1596 =item company - company name
1597
1598 =item address1 - address line 1
1599
1600 =item address2 - address line 1
1601
1602 =item city
1603
1604 =item state
1605
1606 =item zip
1607
1608 =item country
1609
1610 =item pkg - line item description
1611
1612 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1613
1614 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1615
1616 =item sdate - start date for recurring fee
1617
1618 =item edate - end date for recurring fee
1619
1620 =back
1621
1622 If I<format> is "billco", the fields of the header CSV file are as follows:
1623
1624   +-------------------------------------------------------------------+
1625   |                        FORMAT HEADER FILE                         |
1626   |-------------------------------------------------------------------|
1627   | Field | Description                   | Name       | Type | Width |
1628   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1629   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1630   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1631   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1632   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1633   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1634   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1635   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1636   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1637   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1638   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1639   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1640   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1641   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1642   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1643   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1644   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1645   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1646   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1647   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1648   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1649   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1650   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1651   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1652   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1653   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1654   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1655   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1656   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1657   +-------+-------------------------------+------------+------+-------+
1658
1659 If I<format> is "billco", the fields of the detail CSV file are as follows:
1660
1661                                   FORMAT FOR DETAIL FILE
1662         |                            |           |      |
1663   Field | Description                | Name      | Type | Width
1664   1     | N/A-Leave Empty            | RC        | CHAR |     2
1665   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1666   3     | Account Number             | TRACCTNUM | CHAR |    15
1667   4     | Invoice Number             | TRINVOICE | CHAR |    15
1668   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1669   6     | Transaction Detail         | DETAILS   | CHAR |   100
1670   7     | Amount                     | AMT       | NUM* |     9
1671   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1672   9     | Grouping Code              | GROUP     | CHAR |     2
1673   10    | User Defined               | ACCT CODE | CHAR |    15
1674
1675 =cut
1676
1677 sub print_csv {
1678   my($self, %opt) = @_;
1679   
1680   eval "use Text::CSV_XS";
1681   die $@ if $@;
1682
1683   my $cust_main = $self->cust_main;
1684
1685   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1686
1687   if ( lc($opt{'format'}) eq 'billco' ) {
1688
1689     my $taxtotal = 0;
1690     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1691
1692     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1693
1694     my( $previous_balance, @unused ) = $self->previous; #previous balance
1695
1696     my $pmt_cr_applied = 0;
1697     $pmt_cr_applied += $_->{'amount'}
1698       foreach ( $self->_items_payments, $self->_items_credits ) ;
1699
1700     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1701
1702     $csv->combine(
1703       '',                         #  1 | N/A-Leave Empty               CHAR   2
1704       '',                         #  2 | N/A-Leave Empty               CHAR  15
1705       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1706       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1707       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1708       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1709       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1710       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1711       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1712       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1713       '',                         # 10 | Ancillary Billing Information CHAR  30
1714       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1715       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1716
1717       # XXX ?
1718       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1719
1720       # XXX ?
1721       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1722
1723       $previous_balance,          # 15 | Previous Balance              NUM*   9
1724       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1725       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1726       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1727       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1728       '',                         # 20 | 30 Day Aging                  NUM*   9
1729       '',                         # 21 | 60 Day Aging                  NUM*   9
1730       '',                         # 22 | 90 Day Aging                  NUM*   9
1731       'N',                        # 23 | Y/N                           CHAR   1
1732       '',                         # 24 | Remittance automation         CHAR 100
1733       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1734       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1735       '0',                        # 27 | Federal Tax***                NUM*   9
1736       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1737       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1738     );
1739
1740   } else {
1741   
1742     $csv->combine(
1743       'cust_bill',
1744       $self->invnum,
1745       $self->custnum,
1746       time2str("%x", $self->_date),
1747       sprintf("%.2f", $self->charged),
1748       ( map { $cust_main->getfield($_) }
1749           qw( first last company address1 address2 city state zip country ) ),
1750       map { '' } (1..5),
1751     ) or die "can't create csv";
1752   }
1753
1754   my $header = $csv->string. "\n";
1755
1756   my $detail = '';
1757   if ( lc($opt{'format'}) eq 'billco' ) {
1758
1759     my $lineseq = 0;
1760     foreach my $item ( $self->_items_pkg ) {
1761
1762       $csv->combine(
1763         '',                     #  1 | N/A-Leave Empty            CHAR   2
1764         '',                     #  2 | N/A-Leave Empty            CHAR  15
1765         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1766         $self->invnum,          #  4 | Invoice Number             CHAR  15
1767         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1768         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1769         $item->{'amount'},      #  7 | Amount                     NUM*   9
1770         '',                     #  8 | Line Format Control**      CHAR   2
1771         '',                     #  9 | Grouping Code              CHAR   2
1772         '',                     # 10 | User Defined               CHAR  15
1773       );
1774
1775       $detail .= $csv->string. "\n";
1776
1777     }
1778
1779   } else {
1780
1781     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1782
1783       my($pkg, $setup, $recur, $sdate, $edate);
1784       if ( $cust_bill_pkg->pkgnum ) {
1785       
1786         ($pkg, $setup, $recur, $sdate, $edate) = (
1787           $cust_bill_pkg->part_pkg->pkg,
1788           ( $cust_bill_pkg->setup != 0
1789             ? sprintf("%.2f", $cust_bill_pkg->setup )
1790             : '' ),
1791           ( $cust_bill_pkg->recur != 0
1792             ? sprintf("%.2f", $cust_bill_pkg->recur )
1793             : '' ),
1794           ( $cust_bill_pkg->sdate 
1795             ? time2str("%x", $cust_bill_pkg->sdate)
1796             : '' ),
1797           ($cust_bill_pkg->edate 
1798             ?time2str("%x", $cust_bill_pkg->edate)
1799             : '' ),
1800         );
1801   
1802       } else { #pkgnum tax
1803         next unless $cust_bill_pkg->setup != 0;
1804         $pkg = $cust_bill_pkg->desc;
1805         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1806         ( $sdate, $edate ) = ( '', '' );
1807       }
1808   
1809       $csv->combine(
1810         'cust_bill_pkg',
1811         $self->invnum,
1812         ( map { '' } (1..11) ),
1813         ($pkg, $setup, $recur, $sdate, $edate)
1814       ) or die "can't create csv";
1815
1816       $detail .= $csv->string. "\n";
1817
1818     }
1819
1820   }
1821
1822   ( $header, $detail );
1823
1824 }
1825
1826 =item comp
1827
1828 Pays this invoice with a compliemntary payment.  If there is an error,
1829 returns the error, otherwise returns false.
1830
1831 =cut
1832
1833 sub comp {
1834   my $self = shift;
1835   my $cust_pay = new FS::cust_pay ( {
1836     'invnum'   => $self->invnum,
1837     'paid'     => $self->owed,
1838     '_date'    => '',
1839     'payby'    => 'COMP',
1840     'payinfo'  => $self->cust_main->payinfo,
1841     'paybatch' => '',
1842   } );
1843   $cust_pay->insert;
1844 }
1845
1846 =item realtime_card
1847
1848 Attempts to pay this invoice with a credit card payment via a
1849 Business::OnlinePayment realtime gateway.  See
1850 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1851 for supported processors.
1852
1853 =cut
1854
1855 sub realtime_card {
1856   my $self = shift;
1857   $self->realtime_bop( 'CC', @_ );
1858 }
1859
1860 =item realtime_ach
1861
1862 Attempts to pay this invoice with an electronic check (ACH) payment via a
1863 Business::OnlinePayment realtime gateway.  See
1864 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1865 for supported processors.
1866
1867 =cut
1868
1869 sub realtime_ach {
1870   my $self = shift;
1871   $self->realtime_bop( 'ECHECK', @_ );
1872 }
1873
1874 =item realtime_lec
1875
1876 Attempts to pay this invoice with phone bill (LEC) payment via a
1877 Business::OnlinePayment realtime gateway.  See
1878 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1879 for supported processors.
1880
1881 =cut
1882
1883 sub realtime_lec {
1884   my $self = shift;
1885   $self->realtime_bop( 'LEC', @_ );
1886 }
1887
1888 sub realtime_bop {
1889   my( $self, $method ) = @_;
1890
1891   my $cust_main = $self->cust_main;
1892   my $balance = $cust_main->balance;
1893   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1894   $amount = sprintf("%.2f", $amount);
1895   return "not run (balance $balance)" unless $amount > 0;
1896
1897   my $description = 'Internet Services';
1898   if ( $conf->exists('business-onlinepayment-description') ) {
1899     my $dtempl = $conf->config('business-onlinepayment-description');
1900
1901     my $agent_obj = $cust_main->agent
1902       or die "can't retreive agent for $cust_main (agentnum ".
1903              $cust_main->agentnum. ")";
1904     my $agent = $agent_obj->agent;
1905     my $pkgs = join(', ',
1906       map { $_->part_pkg->pkg }
1907         grep { $_->pkgnum } $self->cust_bill_pkg
1908     );
1909     $description = eval qq("$dtempl");
1910   }
1911
1912   $cust_main->realtime_bop($method, $amount,
1913     'description' => $description,
1914     'invnum'      => $self->invnum,
1915 #this didn't do what we want, it just calls apply_payments_and_credits
1916 #    'apply'       => 1,
1917     'apply_to_invoice' => 1,
1918  #what we want:
1919  #this changes application behavior: auto payments
1920                         #triggered against a specific invoice are now applied
1921                         #to that invoice instead of oldest open.
1922                         #seem okay to me...
1923   );
1924
1925 }
1926
1927 =item batch_card OPTION => VALUE...
1928
1929 Adds a payment for this invoice to the pending credit card batch (see
1930 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1931 runs the payment using a realtime gateway.
1932
1933 =cut
1934
1935 sub batch_card {
1936   my ($self, %options) = @_;
1937   my $cust_main = $self->cust_main;
1938
1939   $options{invnum} = $self->invnum;
1940   
1941   $cust_main->batch_card(%options);
1942 }
1943
1944 sub _agent_template {
1945   my $self = shift;
1946   $self->cust_main->agent_template;
1947 }
1948
1949 sub _agent_invoice_from {
1950   my $self = shift;
1951   $self->cust_main->agent_invoice_from;
1952 }
1953
1954 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1955
1956 Returns an text invoice, as a list of lines.
1957
1958 Options can be passed as a hashref (recommended) or as a list of time, template
1959 and then any key/value pairs for any other options.
1960
1961 I<time>, if specified, is used to control the printing of overdue messages.  The
1962 default is now.  It isn't the date of the invoice; that's the `_date' field.
1963 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1964 L<Time::Local> and L<Date::Parse> for conversion functions.
1965
1966 I<template>, if specified, is the name of a suffix for alternate invoices.
1967
1968 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1969
1970 =cut
1971
1972 sub print_text {
1973   my $self = shift;
1974   my( $today, $template, %opt );
1975   if ( ref($_[0]) ) {
1976     %opt = %{ shift() };
1977     $today = delete($opt{'time'}) || '';
1978     $template = delete($opt{template}) || '';
1979   } else {
1980     ( $today, $template, %opt ) = @_;
1981   }
1982
1983   my %params = ( 'format' => 'template' );
1984   $params{'time'} = $today if $today;
1985   $params{'template'} = $template if $template;
1986   $params{$_} = $opt{$_} 
1987     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1988
1989   $self->print_generic( %params );
1990 }
1991
1992 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1993
1994 Internal method - returns a filename of a filled-in LaTeX template for this
1995 invoice (Note: add ".tex" to get the actual filename), and a filename of
1996 an associated logo (with the .eps extension included).
1997
1998 See print_ps and print_pdf for methods that return PostScript and PDF output.
1999
2000 Options can be passed as a hashref (recommended) or as a list of time, template
2001 and then any key/value pairs for any other options.
2002
2003 I<time>, if specified, is used to control the printing of overdue messages.  The
2004 default is now.  It isn't the date of the invoice; that's the `_date' field.
2005 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2006 L<Time::Local> and L<Date::Parse> for conversion functions.
2007
2008 I<template>, if specified, is the name of a suffix for alternate invoices.
2009
2010 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2011
2012 =cut
2013
2014 sub print_latex {
2015   my $self = shift;
2016   my( $today, $template, %opt );
2017   if ( ref($_[0]) ) {
2018     %opt = %{ shift() };
2019     $today = delete($opt{'time'}) || '';
2020     $template = delete($opt{template}) || '';
2021   } else {
2022     ( $today, $template, %opt ) = @_;
2023   }
2024
2025   my %params = ( 'format' => 'latex' );
2026   $params{'time'} = $today if $today;
2027   $params{'template'} = $template if $template;
2028   $params{$_} = $opt{$_} 
2029     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2030
2031   $template ||= $self->_agent_template;
2032
2033   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2034   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2035                            DIR      => $dir,
2036                            SUFFIX   => '.eps',
2037                            UNLINK   => 0,
2038                          ) or die "can't open temp file: $!\n";
2039
2040   my $agentnum = $self->cust_main->agentnum;
2041
2042   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2043     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2044       or die "can't write temp file: $!\n";
2045   } else {
2046     print $lh $conf->config_binary('logo.eps', $agentnum)
2047       or die "can't write temp file: $!\n";
2048   }
2049   close $lh;
2050   $params{'logo_file'} = $lh->filename;
2051
2052   my @filled_in = $self->print_generic( %params );
2053   
2054   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2055                            DIR      => $dir,
2056                            SUFFIX   => '.tex',
2057                            UNLINK   => 0,
2058                          ) or die "can't open temp file: $!\n";
2059   print $fh join('', @filled_in );
2060   close $fh;
2061
2062   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2063   return ($1, $params{'logo_file'});
2064
2065 }
2066
2067 =item print_generic OPTION => VALUE ...
2068
2069 Internal method - returns a filled-in template for this invoice as a scalar.
2070
2071 See print_ps and print_pdf for methods that return PostScript and PDF output.
2072
2073 Non optional options include 
2074   format - latex, html, template
2075
2076 Optional options include
2077
2078 template - a value used as a suffix for a configuration template
2079
2080 time - a value used to control the printing of overdue messages.  The
2081 default is now.  It isn't the date of the invoice; that's the `_date' field.
2082 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2083 L<Time::Local> and L<Date::Parse> for conversion functions.
2084
2085 cid - 
2086
2087 unsquelch_cdr - overrides any per customer cdr squelching when true
2088
2089 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2090
2091 =cut
2092
2093 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
2094 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2095 # yes: fixed width (dot matrix) text printing will be borked
2096 sub print_generic {
2097
2098   my( $self, %params ) = @_;
2099   my $today = $params{today} ? $params{today} : time;
2100   warn "$me print_generic called on $self with suffix $params{template}\n"
2101     if $DEBUG;
2102
2103   my $format = $params{format};
2104   die "Unknown format: $format"
2105     unless $format =~ /^(latex|html|template)$/;
2106
2107   my $cust_main = $self->cust_main;
2108   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2109     unless $cust_main->payname
2110         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2111
2112   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
2113                      'html'     => [ '<%=', '%>' ],
2114                      'template' => [ '{', '}' ],
2115                    );
2116
2117   #create the template
2118   my $template = $params{template} ? $params{template} : $self->_agent_template;
2119   my $templatefile = "invoice_$format";
2120   $templatefile .= "_$template"
2121     if length($template);
2122   my @invoice_template = map "$_\n", $conf->config($templatefile)
2123     or die "cannot load config data $templatefile";
2124
2125   my $old_latex = '';
2126   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2127     #change this to a die when the old code is removed
2128     warn "old-style invoice template $templatefile; ".
2129          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2130     $old_latex = 'true';
2131     @invoice_template = _translate_old_latex_format(@invoice_template);
2132   } 
2133
2134   my $text_template = new Text::Template(
2135     TYPE => 'ARRAY',
2136     SOURCE => \@invoice_template,
2137     DELIMITERS => $delimiters{$format},
2138   );
2139
2140   $text_template->compile()
2141     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2142
2143
2144   # additional substitution could possibly cause breakage in existing templates
2145   my %convert_maps = ( 
2146     'latex' => {
2147                  'notes'         => sub { map "$_", @_ },
2148                  'footer'        => sub { map "$_", @_ },
2149                  'smallfooter'   => sub { map "$_", @_ },
2150                  'returnaddress' => sub { map "$_", @_ },
2151                  'coupon'        => sub { map "$_", @_ },
2152                  'summary'       => sub { map "$_", @_ },
2153                },
2154     'html'  => {
2155                  'notes' =>
2156                    sub {
2157                      map { 
2158                        s/%%(.*)$/<!-- $1 -->/g;
2159                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2160                        s/\\begin\{enumerate\}/<ol>/g;
2161                        s/\\item /  <li>/g;
2162                        s/\\end\{enumerate\}/<\/ol>/g;
2163                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2164                        s/\\\\\*/<br>/g;
2165                        s/\\dollar ?/\$/g;
2166                        s/\\#/#/g;
2167                        s/~/&nbsp;/g;
2168                        $_;
2169                      }  @_
2170                    },
2171                  'footer' =>
2172                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2173                  'smallfooter' =>
2174                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2175                  'returnaddress' =>
2176                    sub {
2177                      map { 
2178                        s/~/&nbsp;/g;
2179                        s/\\\\\*?\s*$/<BR>/;
2180                        s/\\hyphenation\{[\w\s\-]+}//;
2181                        s/\\([&])/$1/g;
2182                        $_;
2183                      }  @_
2184                    },
2185                  'coupon'        => sub { "" },
2186                  'summary'       => sub { "" },
2187                },
2188     'template' => {
2189                  'notes' =>
2190                    sub {
2191                      map { 
2192                        s/%%.*$//g;
2193                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2194                        s/\\begin\{enumerate\}//g;
2195                        s/\\item /  * /g;
2196                        s/\\end\{enumerate\}//g;
2197                        s/\\textbf\{(.*)\}/$1/g;
2198                        s/\\\\\*/ /;
2199                        s/\\dollar ?/\$/g;
2200                        $_;
2201                      }  @_
2202                    },
2203                  'footer' =>
2204                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2205                  'smallfooter' =>
2206                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2207                  'returnaddress' =>
2208                    sub {
2209                      map { 
2210                        s/~/ /g;
2211                        s/\\\\\*?\s*$/\n/;             # dubious
2212                        s/\\hyphenation\{[\w\s\-]+}//;
2213                        $_;
2214                      }  @_
2215                    },
2216                  'coupon'        => sub { "" },
2217                  'summary'       => sub { "" },
2218                },
2219   );
2220
2221
2222   # hashes for differing output formats
2223   my %nbsps = ( 'latex'    => '~',
2224                 'html'     => '',    # '&nbps;' would be nice
2225                 'template' => '',    # not used
2226               );
2227   my $nbsp = $nbsps{$format};
2228
2229   my %escape_functions = ( 'latex'    => \&_latex_escape,
2230                            'html'     => \&encode_entities,
2231                            'template' => sub { shift },
2232                          );
2233   my $escape_function = $escape_functions{$format};
2234
2235   my %date_formats = ( 'latex'    => '%b %o, %Y',
2236                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
2237                        'template' => '%s',
2238                      );
2239   my $date_format = $date_formats{$format};
2240
2241   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2242                                                },
2243                              'html'     => sub { return '<b>'. shift(). '</b>'
2244                                                },
2245                              'template' => sub { shift },
2246                            );
2247   my $embolden_function = $embolden_functions{$format};
2248
2249
2250   # generate template variables
2251   my $returnaddress;
2252   if (
2253          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2254                                         $template
2255                                       )
2256                 )
2257        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2258                                         $template
2259                                       )
2260                 )
2261   ) {
2262
2263     $returnaddress = join("\n",
2264       $conf->config_orbase("invoice_${format}returnaddress", $template)
2265     );
2266
2267   } elsif ( grep /\S/,
2268             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2269
2270     my $convert_map = $convert_maps{$format}{'returnaddress'};
2271     $returnaddress =
2272       join( "\n",
2273             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2274                                                  $template
2275                                                )
2276                          )
2277           );
2278   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2279
2280     my $convert_map = $convert_maps{$format}{'returnaddress'};
2281     $returnaddress = join( "\n", &$convert_map(
2282                                    map { s/( {2,})/'~' x length($1)/eg;
2283                                          s/$/\\\\\*/;
2284                                          $_
2285                                        }
2286                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2287                                        $conf->config('company_address', $self->cust_main->agentnum),
2288                                      )
2289                                  )
2290                      );
2291
2292   } else {
2293
2294     my $warning = "Couldn't find a return address; ".
2295                   "do you need to set the company_address configuration value?";
2296     warn "$warning\n";
2297     $returnaddress = $nbsp;
2298     #$returnaddress = $warning;
2299
2300   }
2301
2302   my %invoice_data = (
2303
2304     #invoice from info
2305     'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2306     'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2307     'returnaddress'   => $returnaddress,
2308     'agent'           => &$escape_function($cust_main->agent->agent),
2309
2310     #invoice info
2311     'invnum'          => $self->invnum,
2312     'date'            => time2str($date_format, $self->_date),
2313     'today'           => time2str('%b %o, %Y', $today),
2314     'terms'           => $self->terms,
2315     'template'        => $template, #params{'template'},
2316     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2317     'current_charges' => sprintf("%.2f", $self->charged),
2318     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2319
2320     #customer info
2321     'custnum'         => $cust_main->display_custnum,
2322     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2323     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2324       payname company address1 address2 city state zip fax
2325     )),
2326
2327     #global config
2328     'ship_enable'     => $conf->exists('invoice-ship_address'),
2329     'unitprices'      => $conf->exists('invoice-unitprice'),
2330     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2331     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2332     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2333    
2334     # better hang on to conf_dir for a while (for old templates)
2335     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2336
2337     #these are only used when doing paged plaintext
2338     'page'            => 1,
2339     'total_pages'     => 1,
2340
2341   );
2342
2343   $invoice_data{finance_section} = '';
2344   if ( $conf->config('finance_pkgclass') ) {
2345     my $pkg_class =
2346       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2347     $invoice_data{finance_section} = $pkg_class->categoryname;
2348   } 
2349  $invoice_data{finance_amount} = '0.00';
2350
2351   my $countrydefault = $conf->config('countrydefault') || 'US';
2352   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2353   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2354     my $method = $prefix.$_;
2355     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2356   }
2357   $invoice_data{'ship_country'} = ''
2358     if ( $invoice_data{'ship_country'} eq $countrydefault );
2359   
2360   $invoice_data{'cid'} = $params{'cid'}
2361     if $params{'cid'};
2362
2363   if ( $cust_main->country eq $countrydefault ) {
2364     $invoice_data{'country'} = '';
2365   } else {
2366     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2367   }
2368
2369   my @address = ();
2370   $invoice_data{'address'} = \@address;
2371   push @address,
2372     $cust_main->payname.
2373       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2374         ? " (P.O. #". $cust_main->payinfo. ")"
2375         : ''
2376       )
2377   ;
2378   push @address, $cust_main->company
2379     if $cust_main->company;
2380   push @address, $cust_main->address1;
2381   push @address, $cust_main->address2
2382     if $cust_main->address2;
2383   push @address,
2384     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2385   push @address, $invoice_data{'country'}
2386     if $invoice_data{'country'};
2387   push @address, ''
2388     while (scalar(@address) < 5);
2389
2390   $invoice_data{'logo_file'} = $params{'logo_file'}
2391     if $params{'logo_file'};
2392
2393   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2394 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2395   #my $balance_due = $self->owed + $pr_total - $cr_total;
2396   my $balance_due = $self->owed + $pr_total;
2397   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2398   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2399   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2400   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2401
2402   my $agentnum = $self->cust_main->agentnum;
2403
2404   my $summarypage = '';
2405   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2406     $summarypage = 1;
2407   }
2408   $invoice_data{'summarypage'} = $summarypage;
2409
2410   #do variable substitution in notes, footer, smallfooter
2411   foreach my $include (qw( notes footer smallfooter coupon )) {
2412
2413     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2414     my @inc_src;
2415
2416     if ( $conf->exists($inc_file, $agentnum)
2417          && length( $conf->config($inc_file, $agentnum) ) ) {
2418
2419       @inc_src = $conf->config($inc_file, $agentnum);
2420
2421     } else {
2422
2423       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2424
2425       my $convert_map = $convert_maps{$format}{$include};
2426
2427       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2428                        s/--\@\]/$delimiters{$format}[1]/g;
2429                        $_;
2430                      } 
2431                  &$convert_map( $conf->config($inc_file, $agentnum) );
2432
2433     }
2434
2435     my $inc_tt = new Text::Template (
2436       TYPE       => 'ARRAY',
2437       SOURCE     => [ map "$_\n", @inc_src ],
2438       DELIMITERS => $delimiters{$format},
2439     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2440
2441     unless ( $inc_tt->compile() ) {
2442       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2443       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2444       die $error;
2445     }
2446
2447     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2448
2449     $invoice_data{$include} =~ s/\n+$//
2450       if ($format eq 'latex');
2451   }
2452
2453   $invoice_data{'po_line'} =
2454     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2455       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2456       : $nbsp;
2457
2458   my %money_chars = ( 'latex'    => '',
2459                       'html'     => $conf->config('money_char') || '$',
2460                       'template' => '',
2461                     );
2462   my $money_char = $money_chars{$format};
2463
2464   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2465                             'html'     => $conf->config('money_char') || '$',
2466                             'template' => '',
2467                           );
2468   my $other_money_char = $other_money_chars{$format};
2469   $invoice_data{'dollar'} = $other_money_char;
2470
2471   my @detail_items = ();
2472   my @total_items = ();
2473   my @buf = ();
2474   my @sections = ();
2475
2476   $invoice_data{'detail_items'} = \@detail_items;
2477   $invoice_data{'total_items'} = \@total_items;
2478   $invoice_data{'buf'} = \@buf;
2479   $invoice_data{'sections'} = \@sections;
2480
2481   my $previous_section = { 'description' => 'Previous Charges',
2482                            'subtotal'    => $other_money_char.
2483                                             sprintf('%.2f', $pr_total),
2484                            'summarized'  => $summarypage ? 'Y' : '',
2485                          };
2486   $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
2487     join(' / ', map { $cust_main->balance_date_range(@$_) }
2488                 $self->_prior_month30s
2489         )
2490     if $conf->exists('invoice_include_aging');
2491
2492   my $taxtotal = 0;
2493   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2494                       'subtotal'    => $taxtotal,   # adjusted below
2495                       'summarized'  => $summarypage ? 'Y' : '',
2496                     };
2497   my $tax_weight = _pkg_category($tax_section->{description})
2498                         ? _pkg_category($tax_section->{description})->weight
2499                         : 0;
2500   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2501   $tax_section->{'sort_weight'} = $tax_weight;
2502
2503
2504   my $adjusttotal = 0;
2505   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2506                          'subtotal'    => 0,   # adjusted below
2507                          'summarized'  => $summarypage ? 'Y' : '',
2508                        };
2509   my $adjust_weight = _pkg_category($adjust_section->{description})
2510                         ? _pkg_category($adjust_section->{description})->weight
2511                         : 0;
2512   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2513   $adjust_section->{'sort_weight'} = $adjust_weight;
2514
2515   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2516   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2517   $invoice_data{'multisection'} = $multisection;
2518   my $late_sections = [];
2519   my $extra_sections = [];
2520   my $extra_lines = ();
2521   if ( $multisection ) {
2522     ($extra_sections, $extra_lines) =
2523       $self->_items_extra_usage_sections($escape_function, $format)
2524       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2525
2526     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2527
2528     push @detail_items, @$extra_lines if $extra_lines;
2529     push @sections,
2530       $self->_items_sections( $late_sections,      # this could stand a refactor
2531                               $summarypage,
2532                               $escape_function,
2533                               $extra_sections,
2534                               $format,             #bah
2535                             );
2536     if ($conf->exists('svc_phone_sections')) {
2537       my ($phone_sections, $phone_lines) =
2538         $self->_items_svc_phone_sections($escape_function, $format);
2539       push @{$late_sections}, @$phone_sections;
2540       push @detail_items, @$phone_lines;
2541     }
2542   }else{
2543     push @sections, { 'description' => '', 'subtotal' => '' };
2544   }
2545
2546   unless (    $conf->exists('disable_previous_balance')
2547            || $conf->exists('previous_balance-summary_only')
2548          )
2549   {
2550
2551     foreach my $line_item ( $self->_items_previous ) {
2552
2553       my $detail = {
2554         ext_description => [],
2555       };
2556       $detail->{'ref'} = $line_item->{'pkgnum'};
2557       $detail->{'quantity'} = 1;
2558       $detail->{'section'} = $previous_section;
2559       $detail->{'description'} = &$escape_function($line_item->{'description'});
2560       if ( exists $line_item->{'ext_description'} ) {
2561         @{$detail->{'ext_description'}} = map {
2562           &$escape_function($_);
2563         } @{$line_item->{'ext_description'}};
2564       }
2565       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2566                             $line_item->{'amount'};
2567       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2568
2569       push @detail_items, $detail;
2570       push @buf, [ $detail->{'description'},
2571                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2572                  ];
2573     }
2574
2575   }
2576
2577   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2578     push @buf, ['','-----------'];
2579     push @buf, [ 'Total Previous Balance',
2580                  $money_char. sprintf("%10.2f", $pr_total) ];
2581     push @buf, ['',''];
2582   }
2583
2584   foreach my $section (@sections, @$late_sections) {
2585
2586     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2587       if ( $invoice_data{finance_section} &&
2588            $section->{'description'} eq $invoice_data{finance_section} );
2589
2590     $section->{'subtotal'} = $other_money_char.
2591                              sprintf('%.2f', $section->{'subtotal'})
2592       if $multisection;
2593
2594     # begin some normalization
2595     $section->{'amount'}   = $section->{'subtotal'}
2596       if $multisection;
2597
2598
2599     if ( $section->{'description'} ) {
2600       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2601                    [ '', '' ],
2602                  );
2603     }
2604
2605     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2606     my %options = ();
2607     $options{'section'} = $section if $multisection;
2608     $options{'format'} = $format;
2609     $options{'escape_function'} = $escape_function;
2610     $options{'format_function'} = sub { () } unless $unsquelched;
2611     $options{'unsquelched'} = $unsquelched;
2612     $options{'summary_page'} = $summarypage;
2613     $options{'skip_usage'} =
2614       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2615     $options{'multilocation'} = $multilocation;
2616
2617     foreach my $line_item ( $self->_items_pkg(%options) ) {
2618       my $detail = {
2619         ext_description => [],
2620       };
2621       $detail->{'ref'} = $line_item->{'pkgnum'};
2622       $detail->{'quantity'} = $line_item->{'quantity'};
2623       $detail->{'section'} = $section;
2624       $detail->{'description'} = &$escape_function($line_item->{'description'});
2625       if ( exists $line_item->{'ext_description'} ) {
2626         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2627       }
2628       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2629                               $line_item->{'amount'};
2630       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2631                                  $line_item->{'unit_amount'};
2632       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2633   
2634       push @detail_items, $detail;
2635       push @buf, ( [ $detail->{'description'},
2636                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2637                    ],
2638                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2639                  );
2640     }
2641
2642     if ( $section->{'description'} ) {
2643       push @buf, ( ['','-----------'],
2644                    [ $section->{'description'}. ' sub-total',
2645                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2646                    ],
2647                    [ '', '' ],
2648                    [ '', '' ],
2649                  );
2650     }
2651   
2652   }
2653   
2654   $invoice_data{current_less_finance} =
2655     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2656
2657   if ( $multisection && !$conf->exists('disable_previous_balance')
2658     || $conf->exists('previous_balance-summary_only') )
2659   {
2660     unshift @sections, $previous_section if $pr_total;
2661   }
2662
2663   foreach my $tax ( $self->_items_tax ) {
2664
2665     $taxtotal += $tax->{'amount'};
2666
2667     my $description = &$escape_function( $tax->{'description'} );
2668     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2669
2670     if ( $multisection ) {
2671
2672       my $money = $old_latex ? '' : $money_char;
2673       push @detail_items, {
2674         ext_description => [],
2675         ref          => '',
2676         quantity     => '',
2677         description  => $description,
2678         amount       => $money. $amount,
2679         product_code => '',
2680         section      => $tax_section,
2681       };
2682
2683     } else {
2684
2685       push @total_items, {
2686         'total_item'   => $description,
2687         'total_amount' => $other_money_char. $amount,
2688       };
2689
2690     }
2691
2692     push @buf,[ $description,
2693                 $money_char. $amount,
2694               ];
2695
2696   }
2697   
2698   if ( $taxtotal ) {
2699     my $total = {};
2700     $total->{'total_item'} = 'Sub-total';
2701     $total->{'total_amount'} =
2702       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2703
2704     if ( $multisection ) {
2705       $tax_section->{'subtotal'} = $other_money_char.
2706                                    sprintf('%.2f', $taxtotal);
2707       $tax_section->{'pretotal'} = 'New charges sub-total '.
2708                                    $total->{'total_amount'};
2709       push @sections, $tax_section if $taxtotal;
2710     }else{
2711       unshift @total_items, $total;
2712     }
2713   }
2714   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2715
2716   push @buf,['','-----------'];
2717   push @buf,[( $conf->exists('disable_previous_balance') 
2718                ? 'Total Charges'
2719                : 'Total New Charges'
2720              ),
2721              $money_char. sprintf("%10.2f",$self->charged) ];
2722   push @buf,['',''];
2723
2724   {
2725     my $total = {};
2726     my $item = 'Total';
2727     $item = $conf->config('previous_balance-exclude_from_total')
2728          || 'Total New Charges'
2729       if $conf->exists('previous_balance-exclude_from_total');
2730     my $amount = $self->charged +
2731                    ( $conf->exists('disable_previous_balance') ||
2732                      $conf->exists('previous_balance-exclude_from_total')
2733                      ? 0
2734                      : $pr_total
2735                    );
2736     $total->{'total_item'} = &$embolden_function($item);
2737     $total->{'total_amount'} =
2738       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
2739     if ( $multisection ) {
2740       if ( $adjust_section->{'sort_weight'} ) {
2741         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2742           sprintf("%.2f", ($self->billing_balance || 0) );
2743       } else {
2744         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2745                                         sprintf('%.2f', $self->charged );
2746       } 
2747     }else{
2748       push @total_items, $total;
2749     }
2750     push @buf,['','-----------'];
2751     push @buf,[$item,
2752                $money_char.
2753                sprintf( '%10.2f', $amount )
2754               ];
2755     push @buf,['',''];
2756   }
2757   
2758   unless ( $conf->exists('disable_previous_balance') ) {
2759     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2760   
2761     # credits
2762     my $credittotal = 0;
2763     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2764
2765       my $total;
2766       $total->{'total_item'} = &$escape_function($credit->{'description'});
2767       $credittotal += $credit->{'amount'};
2768       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2769       $adjusttotal += $credit->{'amount'};
2770       if ( $multisection ) {
2771         my $money = $old_latex ? '' : $money_char;
2772         push @detail_items, {
2773           ext_description => [],
2774           ref          => '',
2775           quantity     => '',
2776           description  => &$escape_function($credit->{'description'}),
2777           amount       => $money. $credit->{'amount'},
2778           product_code => '',
2779           section      => $adjust_section,
2780         };
2781       } else {
2782         push @total_items, $total;
2783       }
2784
2785     }
2786     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2787
2788     #credits (again)
2789     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2790       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2791     }
2792
2793     # payments
2794     my $paymenttotal = 0;
2795     foreach my $payment ( $self->_items_payments ) {
2796       my $total = {};
2797       $total->{'total_item'} = &$escape_function($payment->{'description'});
2798       $paymenttotal += $payment->{'amount'};
2799       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2800       $adjusttotal += $payment->{'amount'};
2801       if ( $multisection ) {
2802         my $money = $old_latex ? '' : $money_char;
2803         push @detail_items, {
2804           ext_description => [],
2805           ref          => '',
2806           quantity     => '',
2807           description  => &$escape_function($payment->{'description'}),
2808           amount       => $money. $payment->{'amount'},
2809           product_code => '',
2810           section      => $adjust_section,
2811         };
2812       }else{
2813         push @total_items, $total;
2814       }
2815       push @buf, [ $payment->{'description'},
2816                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2817                  ];
2818     }
2819     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2820   
2821     if ( $multisection ) {
2822       $adjust_section->{'subtotal'} = $other_money_char.
2823                                       sprintf('%.2f', $adjusttotal);
2824       push @sections, $adjust_section
2825         unless $adjust_section->{sort_weight};
2826     }
2827
2828     { 
2829       my $total;
2830       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2831       $total->{'total_amount'} =
2832         &$embolden_function(
2833           $other_money_char. sprintf('%.2f', $summarypage 
2834                                                ? $self->charged +
2835                                                  $self->billing_balance
2836                                                : $self->owed + $pr_total
2837                                     )
2838         );
2839       if ( $multisection && !$adjust_section->{sort_weight} ) {
2840         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2841                                          $total->{'total_amount'};
2842       }else{
2843         push @total_items, $total;
2844       }
2845       push @buf,['','-----------'];
2846       push @buf,[$self->balance_due_msg, $money_char. 
2847         sprintf("%10.2f", $balance_due ) ];
2848     }
2849   }
2850
2851   if ( $multisection ) {
2852     if ($conf->exists('svc_phone_sections')) {
2853       my $total;
2854       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2855       $total->{'total_amount'} =
2856         &$embolden_function(
2857           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2858         );
2859       my $last_section = pop @sections;
2860       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2861                                      $total->{'total_amount'};
2862       push @sections, $last_section;
2863     }
2864     push @sections, @$late_sections
2865       if $unsquelched;
2866   }
2867
2868   my @includelist = ();
2869   push @includelist, 'summary' if $summarypage;
2870   foreach my $include ( @includelist ) {
2871
2872     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2873     my @inc_src;
2874
2875     if ( length( $conf->config($inc_file, $agentnum) ) ) {
2876
2877       @inc_src = $conf->config($inc_file, $agentnum);
2878
2879     } else {
2880
2881       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2882
2883       my $convert_map = $convert_maps{$format}{$include};
2884
2885       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2886                        s/--\@\]/$delimiters{$format}[1]/g;
2887                        $_;
2888                      } 
2889                  &$convert_map( $conf->config($inc_file, $agentnum) );
2890
2891     }
2892
2893     my $inc_tt = new Text::Template (
2894       TYPE       => 'ARRAY',
2895       SOURCE     => [ map "$_\n", @inc_src ],
2896       DELIMITERS => $delimiters{$format},
2897     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2898
2899     unless ( $inc_tt->compile() ) {
2900       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2901       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2902       die $error;
2903     }
2904
2905     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2906
2907     $invoice_data{$include} =~ s/\n+$//
2908       if ($format eq 'latex');
2909   }
2910
2911   $invoice_lines = 0;
2912   my $wasfunc = 0;
2913   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2914     /invoice_lines\((\d*)\)/;
2915     $invoice_lines += $1 || scalar(@buf);
2916     $wasfunc=1;
2917   }
2918   die "no invoice_lines() functions in template?"
2919     if ( $format eq 'template' && !$wasfunc );
2920
2921   if ($format eq 'template') {
2922
2923     if ( $invoice_lines ) {
2924       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2925       $invoice_data{'total_pages'}++
2926         if scalar(@buf) % $invoice_lines;
2927     }
2928
2929     #setup subroutine for the template
2930     sub FS::cust_bill::_template::invoice_lines {
2931       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2932       map { 
2933         scalar(@FS::cust_bill::_template::buf)
2934           ? shift @FS::cust_bill::_template::buf
2935           : [ '', '' ];
2936       }
2937       ( 1 .. $lines );
2938     }
2939
2940     my $lines;
2941     my @collect;
2942     while (@buf) {
2943       push @collect, split("\n",
2944         $text_template->fill_in( HASH => \%invoice_data,
2945                                  PACKAGE => 'FS::cust_bill::_template'
2946                                )
2947       );
2948       $FS::cust_bill::_template::page++;
2949     }
2950     map "$_\n", @collect;
2951   }else{
2952     warn "filling in template for invoice ". $self->invnum. "\n"
2953       if $DEBUG;
2954     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2955       if $DEBUG > 1;
2956
2957     $text_template->fill_in(HASH => \%invoice_data);
2958   }
2959 }
2960
2961 # helper routine for generating date ranges
2962 sub _prior_month30s {
2963   my $self = shift;
2964   my @ranges = (
2965    [ 1,       2592000 ], # 0-30 days ago
2966    [ 2592000, 5184000 ], # 30-60 days ago
2967    [ 5184000, 7776000 ], # 60-90 days ago
2968    [ 7776000, 0       ], # 90+   days ago
2969   );
2970
2971   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
2972           $_->[1] ? $self->_date - $_->[1] - 1 : '',
2973       ] }
2974   @ranges;
2975 }
2976
2977 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2978
2979 Returns an postscript invoice, as a scalar.
2980
2981 Options can be passed as a hashref (recommended) or as a list of time, template
2982 and then any key/value pairs for any other options.
2983
2984 I<time> an optional value used to control the printing of overdue messages.  The
2985 default is now.  It isn't the date of the invoice; that's the `_date' field.
2986 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2987 L<Time::Local> and L<Date::Parse> for conversion functions.
2988
2989 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2990
2991 =cut
2992
2993 sub print_ps {
2994   my $self = shift;
2995
2996   my ($file, $lfile) = $self->print_latex(@_);
2997   my $ps = generate_ps($file);
2998   unlink($lfile);
2999
3000   $ps;
3001 }
3002
3003 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3004
3005 Returns an PDF invoice, as a scalar.
3006
3007 Options can be passed as a hashref (recommended) or as a list of time, template
3008 and then any key/value pairs for any other options.
3009
3010 I<time> an optional value used to control the printing of overdue messages.  The
3011 default is now.  It isn't the date of the invoice; that's the `_date' field.
3012 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3013 L<Time::Local> and L<Date::Parse> for conversion functions.
3014
3015 I<template>, if specified, is the name of a suffix for alternate invoices.
3016
3017 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3018
3019 =cut
3020
3021 sub print_pdf {
3022   my $self = shift;
3023
3024   my ($file, $lfile) = $self->print_latex(@_);
3025   my $pdf = generate_pdf($file);
3026   unlink($lfile);
3027
3028   $pdf;
3029 }
3030
3031 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3032
3033 Returns an HTML invoice, as a scalar.
3034
3035 I<time> an optional value used to control the printing of overdue messages.  The
3036 default is now.  It isn't the date of the invoice; that's the `_date' field.
3037 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3038 L<Time::Local> and L<Date::Parse> for conversion functions.
3039
3040 I<template>, if specified, is the name of a suffix for alternate invoices.
3041
3042 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3043
3044 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3045 when emailing the invoice as part of a multipart/related MIME email.
3046
3047 =cut
3048
3049 sub print_html {
3050   my $self = shift;
3051   my %params;
3052   if ( ref($_[0]) ) {
3053     %params = %{ shift() }; 
3054   }else{
3055     $params{'time'} = shift;
3056     $params{'template'} = shift;
3057     $params{'cid'} = shift;
3058   }
3059
3060   $params{'format'} = 'html';
3061
3062   $self->print_generic( %params );
3063 }
3064
3065 # quick subroutine for print_latex
3066 #
3067 # There are ten characters that LaTeX treats as special characters, which
3068 # means that they do not simply typeset themselves: 
3069 #      # $ % & ~ _ ^ \ { }
3070 #
3071 # TeX ignores blanks following an escaped character; if you want a blank (as
3072 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3073
3074 sub _latex_escape {
3075   my $value = shift;
3076   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3077   $value =~ s/([<>])/\$$1\$/g;
3078   $value;
3079 }
3080
3081 #utility methods for print_*
3082
3083 sub _translate_old_latex_format {
3084   warn "_translate_old_latex_format called\n"
3085     if $DEBUG; 
3086
3087   my @template = ();
3088   while ( @_ ) {
3089     my $line = shift;
3090   
3091     if ( $line =~ /^%%Detail\s*$/ ) {
3092   
3093       push @template, q![@--!,
3094                       q!  foreach my $_tr_line (@detail_items) {!,
3095                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3096                       q!      $_tr_line->{'description'} .= !, 
3097                       q!        "\\tabularnewline\n~~".!,
3098                       q!        join( "\\tabularnewline\n~~",!,
3099                       q!          @{$_tr_line->{'ext_description'}}!,
3100                       q!        );!,
3101                       q!    }!;
3102
3103       while ( ( my $line_item_line = shift )
3104               !~ /^%%EndDetail\s*$/                            ) {
3105         $line_item_line =~ s/'/\\'/g;    # nice LTS
3106         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3107         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3108         push @template, "    \$OUT .= '$line_item_line';";
3109       }
3110
3111       push @template, '}',
3112                       '--@]';
3113       #' doh, gvim
3114     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3115
3116       push @template, '[@--',
3117                       '  foreach my $_tr_line (@total_items) {';
3118
3119       while ( ( my $total_item_line = shift )
3120               !~ /^%%EndTotalDetails\s*$/                      ) {
3121         $total_item_line =~ s/'/\\'/g;    # nice LTS
3122         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3123         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3124         push @template, "    \$OUT .= '$total_item_line';";
3125       }
3126
3127       push @template, '}',
3128                       '--@]';
3129
3130     } else {
3131       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3132       push @template, $line;  
3133     }
3134   
3135   }
3136
3137   if ($DEBUG) {
3138     warn "$_\n" foreach @template;
3139   }
3140
3141   (@template);
3142 }
3143
3144 sub terms {
3145   my $self = shift;
3146
3147   #check for an invoice-specific override
3148   return $self->invoice_terms if $self->invoice_terms;
3149   
3150   #check for a customer- specific override
3151   my $cust_main = $self->cust_main;
3152   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3153
3154   #use configured default
3155   $conf->config('invoice_default_terms') || '';
3156 }
3157
3158 sub due_date {
3159   my $self = shift;
3160   my $duedate = '';
3161   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3162     $duedate = $self->_date() + ( $1 * 86400 );
3163   }
3164   $duedate;
3165 }
3166
3167 sub due_date2str {
3168   my $self = shift;
3169   $self->due_date ? time2str(shift, $self->due_date) : '';
3170 }
3171
3172 sub balance_due_msg {
3173   my $self = shift;
3174   my $msg = 'Balance Due';
3175   return $msg unless $self->terms;
3176   if ( $self->due_date ) {
3177     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3178   } elsif ( $self->terms ) {
3179     $msg .= ' - '. $self->terms;
3180   }
3181   $msg;
3182 }
3183
3184 sub balance_due_date {
3185   my $self = shift;
3186   my $duedate = '';
3187   if (    $conf->exists('invoice_default_terms') 
3188        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3189     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3190   }
3191   $duedate;
3192 }
3193
3194 =item invnum_date_pretty
3195
3196 Returns a string with the invoice number and date, for example:
3197 "Invoice #54 (3/20/2008)"
3198
3199 =cut
3200
3201 sub invnum_date_pretty {
3202   my $self = shift;
3203   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3204 }
3205
3206 =item _date_pretty
3207
3208 Returns a string with the date, for example: "3/20/2008"
3209
3210 =cut
3211
3212 sub _date_pretty {
3213   my $self = shift;
3214   time2str($date_format, $self->_date);
3215 }
3216
3217 use vars qw(%pkg_category_cache);
3218 sub _items_sections {
3219   my $self = shift;
3220   my $late = shift;
3221   my $summarypage = shift;
3222   my $escape = shift;
3223   my $extra_sections = shift;
3224   my $format = shift;
3225
3226   my %subtotal = ();
3227   my %late_subtotal = ();
3228   my %not_tax = ();
3229
3230   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3231   {
3232
3233       my $usage = $cust_bill_pkg->usage;
3234
3235       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3236         next if ( $display->summary && $summarypage );
3237
3238         my $section = $display->section;
3239         my $type    = $display->type;
3240
3241         $not_tax{$section} = 1
3242           unless $cust_bill_pkg->pkgnum == 0;
3243
3244         if ( $display->post_total && !$summarypage ) {
3245           if (! $type || $type eq 'S') {
3246             $late_subtotal{$section} += $cust_bill_pkg->setup
3247               if $cust_bill_pkg->setup != 0;
3248           }
3249
3250           if (! $type) {
3251             $late_subtotal{$section} += $cust_bill_pkg->recur
3252               if $cust_bill_pkg->recur != 0;
3253           }
3254
3255           if ($type && $type eq 'R') {
3256             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3257               if $cust_bill_pkg->recur != 0;
3258           }
3259           
3260           if ($type && $type eq 'U') {
3261             $late_subtotal{$section} += $usage
3262               unless scalar(@$extra_sections);
3263           }
3264
3265         } else {
3266
3267           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3268
3269           if (! $type || $type eq 'S') {
3270             $subtotal{$section} += $cust_bill_pkg->setup
3271               if $cust_bill_pkg->setup != 0;
3272           }
3273
3274           if (! $type) {
3275             $subtotal{$section} += $cust_bill_pkg->recur
3276               if $cust_bill_pkg->recur != 0;
3277           }
3278
3279           if ($type && $type eq 'R') {
3280             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3281               if $cust_bill_pkg->recur != 0;
3282           }
3283           
3284           if ($type && $type eq 'U') {
3285             $subtotal{$section} += $usage
3286               unless scalar(@$extra_sections);
3287           }
3288
3289         }
3290
3291       }
3292
3293   }
3294
3295   %pkg_category_cache = ();
3296
3297   push @$late, map { { 'description' => &{$escape}($_),
3298                        'subtotal'    => $late_subtotal{$_},
3299                        'post_total'  => 1,
3300                        'sort_weight' => ( _pkg_category($_)
3301                                             ? _pkg_category($_)->weight
3302                                             : 0
3303                                        ),
3304                        ((_pkg_category($_) && _pkg_category($_)->condense)
3305                                            ? $self->_condense_section($format)
3306                                            : ()
3307                        ),
3308                    } }
3309                  sort _sectionsort keys %late_subtotal;
3310
3311   my @sections;
3312   if ( $summarypage ) {
3313     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3314                 map { $_->categoryname } qsearch('pkg_category', {});
3315   } else {
3316     @sections = keys %subtotal;
3317   }
3318
3319   my @early = map { { 'description' => &{$escape}($_),
3320                       'subtotal'    => $subtotal{$_},
3321                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3322                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3323                       'sort_weight' => ( _pkg_category($_)
3324                                            ? _pkg_category($_)->weight
3325                                            : 0
3326                                        ),
3327                        ((_pkg_category($_) && _pkg_category($_)->condense)
3328                                            ? $self->_condense_section($format)
3329                                            : ()
3330                        ),
3331                     }
3332                   } @sections;
3333   push @early, @$extra_sections if $extra_sections;
3334  
3335   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3336
3337 }
3338
3339 #helper subs for above
3340
3341 sub _sectionsort {
3342   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3343 }
3344
3345 sub _pkg_category {
3346   my $categoryname = shift;
3347   $pkg_category_cache{$categoryname} ||=
3348     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3349 }
3350
3351 my %condensed_format = (
3352   'label' => [ qw( Description Qty Amount ) ],
3353   'fields' => [
3354                 sub { shift->{description} },
3355                 sub { shift->{quantity} },
3356                 sub { shift->{amount} },
3357               ],
3358   'align'  => [ qw( l r r ) ],
3359   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3360   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3361 );
3362
3363 sub _condense_section {
3364   my ( $self, $format ) = ( shift, shift );
3365   ( 'condensed' => 1,
3366     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3367       qw( description_generator
3368           header_generator
3369           total_generator
3370           total_line_generator
3371         )
3372   );
3373 }
3374
3375 sub _condensed_generator_defaults {
3376   my ( $self, $format ) = ( shift, shift );
3377   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3378 }
3379
3380 my %html_align = (
3381   'c' => 'center',
3382   'l' => 'left',
3383   'r' => 'right',
3384 );
3385
3386 sub _condensed_header_generator {
3387   my ( $self, $format ) = ( shift, shift );
3388
3389   my ( $f, $prefix, $suffix, $separator, $column ) =
3390     _condensed_generator_defaults($format);
3391
3392   if ($format eq 'latex') {
3393     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3394     $suffix = "\\\\\n\\hline";
3395     $separator = "&\n";
3396     $column =
3397       sub { my ($d,$a,$s,$w) = @_;
3398             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3399           };
3400   } elsif ( $format eq 'html' ) {
3401     $prefix = '<th></th>';
3402     $suffix = '';
3403     $separator = '';
3404     $column =
3405       sub { my ($d,$a,$s,$w) = @_;
3406             return qq!<th align="$html_align{$a}">$d</th>!;
3407       };
3408   }
3409
3410   sub {
3411     my @args = @_;
3412     my @result = ();
3413
3414     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3415       push @result,
3416         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3417     }
3418
3419     $prefix. join($separator, @result). $suffix;
3420   };
3421
3422 }
3423
3424 sub _condensed_description_generator {
3425   my ( $self, $format ) = ( shift, shift );
3426
3427   my ( $f, $prefix, $suffix, $separator, $column ) =
3428     _condensed_generator_defaults($format);
3429
3430   if ($format eq 'latex') {
3431     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3432     $suffix = '\\\\';
3433     $separator = " & \n";
3434     $column =
3435       sub { my ($d,$a,$s,$w) = @_;
3436             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3437           };
3438   }elsif ( $format eq 'html' ) {
3439     $prefix = '"><td align="center"></td>';
3440     $suffix = '';
3441     $separator = '';
3442     $column =
3443       sub { my ($d,$a,$s,$w) = @_;
3444             return qq!<td align="$html_align{$a}">$d</td>!;
3445       };
3446   }
3447
3448   sub {
3449     my @args = @_;
3450     my @result = ();
3451
3452     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3453       push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3454                                 map { $f->{$_}->[$i] } qw(align span width)
3455                               );
3456     }
3457
3458     $prefix. join( $separator, @result ). $suffix;
3459   };
3460
3461 }
3462
3463 sub _condensed_total_generator {
3464   my ( $self, $format ) = ( shift, shift );
3465
3466   my ( $f, $prefix, $suffix, $separator, $column ) =
3467     _condensed_generator_defaults($format);
3468   my $style = '';
3469
3470   if ($format eq 'latex') {
3471     $prefix = "& ";
3472     $suffix = "\\\\\n";
3473     $separator = " & \n";
3474     $column =
3475       sub { my ($d,$a,$s,$w) = @_;
3476             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3477           };
3478   }elsif ( $format eq 'html' ) {
3479     $prefix = '';
3480     $suffix = '';
3481     $separator = '';
3482     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3483     $column =
3484       sub { my ($d,$a,$s,$w) = @_;
3485             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3486       };
3487   }
3488
3489
3490   sub {
3491     my @args = @_;
3492     my @result = ();
3493
3494     #  my $r = &{$f->{fields}->[$i]}(@args);
3495     #  $r .= ' Total' unless $i;
3496
3497     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3498       push @result,
3499         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3500                     map { $f->{$_}->[$i] } qw(align span width)
3501                   );
3502     }
3503
3504     $prefix. join( $separator, @result ). $suffix;
3505   };
3506
3507 }
3508
3509 =item total_line_generator FORMAT
3510
3511 Returns a coderef used for generation of invoice total line items for this
3512 usage_class.  FORMAT is either html or latex
3513
3514 =cut
3515
3516 # should not be used: will have issues with hash element names (description vs
3517 # total_item and amount vs total_amount -- another array of functions?
3518
3519 sub _condensed_total_line_generator {
3520   my ( $self, $format ) = ( shift, shift );
3521
3522   my ( $f, $prefix, $suffix, $separator, $column ) =
3523     _condensed_generator_defaults($format);
3524   my $style = '';
3525
3526   if ($format eq 'latex') {
3527     $prefix = "& ";
3528     $suffix = "\\\\\n";
3529     $separator = " & \n";
3530     $column =
3531       sub { my ($d,$a,$s,$w) = @_;
3532             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3533           };
3534   }elsif ( $format eq 'html' ) {
3535     $prefix = '';
3536     $suffix = '';
3537     $separator = '';
3538     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3539     $column =
3540       sub { my ($d,$a,$s,$w) = @_;
3541             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3542       };
3543   }
3544
3545
3546   sub {
3547     my @args = @_;
3548     my @result = ();
3549
3550     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3551       push @result,
3552         &{$column}( &{$f->{fields}->[$i]}(@args),
3553                     map { $f->{$_}->[$i] } qw(align span width)
3554                   );
3555     }
3556
3557     $prefix. join( $separator, @result ). $suffix;
3558   };
3559
3560 }
3561
3562 #sub _items_extra_usage_sections {
3563 #  my $self = shift;
3564 #  my $escape = shift;
3565 #
3566 #  my %sections = ();
3567 #
3568 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3569 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3570 #  {
3571 #    next unless $cust_bill_pkg->pkgnum > 0;
3572 #
3573 #    foreach my $section ( keys %usage_class ) {
3574 #
3575 #      my $usage = $cust_bill_pkg->usage($section);
3576 #
3577 #      next unless $usage && $usage > 0;
3578 #
3579 #      $sections{$section} ||= 0;
3580 #      $sections{$section} += $usage;
3581 #
3582 #    }
3583 #
3584 #  }
3585 #
3586 #  map { { 'description' => &{$escape}($_),
3587 #          'subtotal'    => $sections{$_},
3588 #          'summarized'  => '',
3589 #          'tax_section' => '',
3590 #        }
3591 #      }
3592 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3593 #
3594 #}
3595
3596 sub _items_extra_usage_sections {
3597   my $self = shift;
3598   my $escape = shift;
3599   my $format = shift;
3600
3601   my %sections = ();
3602   my %classnums = ();
3603   my %lines = ();
3604
3605   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3606   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3607     next unless $cust_bill_pkg->pkgnum > 0;
3608
3609     foreach my $classnum ( keys %usage_class ) {
3610       my $section = $usage_class{$classnum}->classname;
3611       $classnums{$section} = $classnum;
3612
3613       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3614         my $amount = $detail->amount;
3615         next unless $amount && $amount > 0;
3616  
3617         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3618         $sections{$section}{amount} += $amount;  #subtotal
3619         $sections{$section}{calls}++;
3620         $sections{$section}{duration} += $detail->duration;
3621
3622         my $desc = $detail->regionname; 
3623         my $description = $desc;
3624         $description = substr($desc, 0, 50). '...'
3625           if $format eq 'latex' && length($desc) > 50;
3626
3627         $lines{$section}{$desc} ||= {
3628           description     => &{$escape}($description),
3629           #pkgpart         => $part_pkg->pkgpart,
3630           pkgnum          => $cust_bill_pkg->pkgnum,
3631           ref             => '',
3632           amount          => 0,
3633           calls           => 0,
3634           duration        => 0,
3635           #unit_amount     => $cust_bill_pkg->unitrecur,
3636           quantity        => $cust_bill_pkg->quantity,
3637           product_code    => 'N/A',
3638           ext_description => [],
3639         };
3640
3641         $lines{$section}{$desc}{amount} += $amount;
3642         $lines{$section}{$desc}{calls}++;
3643         $lines{$section}{$desc}{duration} += $detail->duration;
3644
3645       }
3646     }
3647   }
3648
3649   my %sectionmap = ();
3650   foreach (keys %sections) {
3651     my $usage_class = $usage_class{$classnums{$_}};
3652     $sectionmap{$_} = { 'description' => &{$escape}($_),
3653                         'amount'    => $sections{$_}{amount},    #subtotal
3654                         'calls'       => $sections{$_}{calls},
3655                         'duration'    => $sections{$_}{duration},
3656                         'summarized'  => '',
3657                         'tax_section' => '',
3658                         'sort_weight' => $usage_class->weight,
3659                         ( $usage_class->format
3660                           ? ( map { $_ => $usage_class->$_($format) }
3661                               qw( description_generator header_generator total_generator total_line_generator )
3662                             )
3663                           : ()
3664                         ), 
3665                       };
3666   }
3667
3668   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3669                  values %sectionmap;
3670
3671   my @lines = ();
3672   foreach my $section ( keys %lines ) {
3673     foreach my $line ( keys %{$lines{$section}} ) {
3674       my $l = $lines{$section}{$line};
3675       $l->{section}     = $sectionmap{$section};
3676       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3677       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3678       push @lines, $l;
3679     }
3680   }
3681
3682   return(\@sections, \@lines);
3683
3684 }
3685
3686 sub _items_svc_phone_sections {
3687   my $self = shift;
3688   my $escape = shift;
3689   my $format = shift;
3690
3691   my %sections = ();
3692   my %classnums = ();
3693   my %lines = ();
3694
3695   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3696
3697   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3698     next unless $cust_bill_pkg->pkgnum > 0;
3699
3700     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3701
3702       my $phonenum = $detail->phonenum;
3703       next unless $phonenum;
3704
3705       my $amount = $detail->amount;
3706       next unless $amount && $amount > 0;
3707
3708       $sections{$phonenum} ||= { 'amount'      => 0,
3709                                  'calls'       => 0,
3710                                  'duration'    => 0,
3711                                  'sort_weight' => -1,
3712                                  'phonenum'    => $phonenum,
3713                                 };
3714       $sections{$phonenum}{amount} += $amount;  #subtotal
3715       $sections{$phonenum}{calls}++;
3716       $sections{$phonenum}{duration} += $detail->duration;
3717
3718       my $desc = $detail->regionname; 
3719       my $description = $desc;
3720       $description = substr($desc, 0, 50). '...'
3721         if $format eq 'latex' && length($desc) > 50;
3722
3723       $lines{$phonenum}{$desc} ||= {
3724         description     => &{$escape}($description),
3725         #pkgpart         => $part_pkg->pkgpart,
3726         pkgnum          => '',
3727         ref             => '',
3728         amount          => 0,
3729         calls           => 0,
3730         duration        => 0,
3731         #unit_amount     => '',
3732         quantity        => '',
3733         product_code    => 'N/A',
3734         ext_description => [],
3735       };
3736
3737       $lines{$phonenum}{$desc}{amount} += $amount;
3738       $lines{$phonenum}{$desc}{calls}++;
3739       $lines{$phonenum}{$desc}{duration} += $detail->duration;
3740
3741       my $line = $usage_class{$detail->classnum}->classname;
3742       $sections{"$phonenum $line"} ||=
3743         { 'amount' => 0,
3744           'calls' => 0,
3745           'duration' => 0,
3746           'sort_weight' => $usage_class{$detail->classnum}->weight,
3747           'phonenum' => $phonenum,
3748         };
3749       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
3750       $sections{"$phonenum $line"}{calls}++;
3751       $sections{"$phonenum $line"}{duration} += $detail->duration;
3752
3753       $lines{"$phonenum $line"}{$desc} ||= {
3754         description     => &{$escape}($description),
3755         #pkgpart         => $part_pkg->pkgpart,
3756         pkgnum          => '',
3757         ref             => '',
3758         amount          => 0,
3759         calls           => 0,
3760         duration        => 0,
3761         #unit_amount     => '',
3762         quantity        => '',
3763         product_code    => 'N/A',
3764         ext_description => [],
3765       };
3766
3767       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3768       $lines{"$phonenum $line"}{$desc}{calls}++;
3769       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3770       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3771            $detail->formatted('format' => $format);
3772
3773     }
3774   }
3775
3776   my %sectionmap = ();
3777   my $simple = new FS::usage_class { format => 'simple' }; #bleh
3778   my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3779   foreach ( keys %sections ) {
3780     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3781     my $usage_class = $summary ? $simple : $usage_simple;
3782     my $ending = $summary ? ' usage charges' : '';
3783     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3784                         'amount'    => $sections{$_}{amount},    #subtotal
3785                         'calls'       => $sections{$_}{calls},
3786                         'duration'    => $sections{$_}{duration},
3787                         'summarized'  => '',
3788                         'tax_section' => '',
3789                         'phonenum'    => $sections{$_}{phonenum},
3790                         'sort_weight' => $sections{$_}{sort_weight},
3791                         'post_total'  => $summary, #inspire pagebreak
3792                         (
3793                           ( map { $_ => $usage_class->$_($format) }
3794                             qw( description_generator
3795                                 header_generator
3796                                 total_generator
3797                                 total_line_generator
3798                               )
3799                           )
3800                         ), 
3801                       };
3802   }
3803
3804   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3805                         $a->{sort_weight} <=> $b->{sort_weight}
3806                       }
3807                  values %sectionmap;
3808
3809   my @lines = ();
3810   foreach my $section ( keys %lines ) {
3811     foreach my $line ( keys %{$lines{$section}} ) {
3812       my $l = $lines{$section}{$line};
3813       $l->{section}     = $sectionmap{$section};
3814       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3815       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3816       push @lines, $l;
3817     }
3818   }
3819
3820   return(\@sections, \@lines);
3821
3822 }
3823
3824 sub _items {
3825   my $self = shift;
3826
3827   #my @display = scalar(@_)
3828   #              ? @_
3829   #              : qw( _items_previous _items_pkg );
3830   #              #: qw( _items_pkg );
3831   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3832   my @display = qw( _items_previous _items_pkg );
3833
3834   my @b = ();
3835   foreach my $display ( @display ) {
3836     push @b, $self->$display(@_);
3837   }
3838   @b;
3839 }
3840
3841 sub _items_previous {
3842   my $self = shift;
3843   my $cust_main = $self->cust_main;
3844   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3845   my @b = ();
3846   foreach ( @pr_cust_bill ) {
3847     my $date = $conf->exists('invoice_show_prior_due_date')
3848                ? 'due '. $_->due_date2str($date_format)
3849                : time2str($date_format, $_->_date);
3850     push @b, {
3851       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3852       #'pkgpart'     => 'N/A',
3853       'pkgnum'      => 'N/A',
3854       'amount'      => sprintf("%.2f", $_->owed),
3855     };
3856   }
3857   @b;
3858
3859   #{
3860   #    'description'     => 'Previous Balance',
3861   #    #'pkgpart'         => 'N/A',
3862   #    'pkgnum'          => 'N/A',
3863   #    'amount'          => sprintf("%10.2f", $pr_total ),
3864   #    'ext_description' => [ map {
3865   #                                 "Invoice ". $_->invnum.
3866   #                                 " (". time2str("%x",$_->_date). ") ".
3867   #                                 sprintf("%10.2f", $_->owed)
3868   #                         } @pr_cust_bill ],
3869
3870   #};
3871 }
3872
3873 sub _items_pkg {
3874   my $self = shift;
3875   my %options = @_;
3876   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3877   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3878   if ($options{section} && $options{section}->{condensed}) {
3879     my %itemshash = ();
3880     local $Storable::canonical = 1;
3881     foreach ( @items ) {
3882       my $item = { %$_ };
3883       delete $item->{ref};
3884       delete $item->{ext_description};
3885       my $key = freeze($item);
3886       $itemshash{$key} ||= 0;
3887       $itemshash{$key} ++; # += $item->{quantity};
3888     }
3889     @items = sort { $a->{description} cmp $b->{description} }
3890              map { my $i = thaw($_);
3891                    $i->{quantity} = $itemshash{$_};
3892                    $i->{amount} =
3893                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3894                    $i;
3895                  }
3896              keys %itemshash;
3897   }
3898   @items;
3899 }
3900
3901 sub _taxsort {
3902   return 0 unless $a cmp $b;
3903   return -1 if $b eq 'Tax';
3904   return 1 if $a eq 'Tax';
3905   return -1 if $b eq 'Other surcharges';
3906   return 1 if $a eq 'Other surcharges';
3907   $a cmp $b;
3908 }
3909
3910 sub _items_tax {
3911   my $self = shift;
3912   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3913   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3914 }
3915
3916 sub _items_cust_bill_pkg {
3917   my $self = shift;
3918   my $cust_bill_pkg = shift;
3919   my %opt = @_;
3920
3921   my $format = $opt{format} || '';
3922   my $escape_function = $opt{escape_function} || sub { shift };
3923   my $format_function = $opt{format_function} || '';
3924   my $unsquelched = $opt{unsquelched} || '';
3925   my $section = $opt{section}->{description} if $opt{section};
3926   my $summary_page = $opt{summary_page} || '';
3927   my $multilocation = $opt{multilocation} || '';
3928
3929   my @b = ();
3930   my ($s, $r, $u) = ( undef, undef, undef );
3931   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3932   {
3933
3934     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3935       if ( $_ && !$cust_bill_pkg->hidden ) {
3936         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3937         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3938         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3939         push @b, { %$_ }
3940           unless $_->{amount} == 0;
3941         $_ = undef;
3942       }
3943     }
3944
3945     foreach my $display ( grep { defined($section)
3946                                  ? $_->section eq $section
3947                                  : 1
3948                                }
3949                           grep { !$_->summary || !$summary_page }
3950                           $cust_bill_pkg->cust_bill_pkg_display
3951                         )
3952     {
3953
3954       my $type = $display->type;
3955
3956       my $desc = $cust_bill_pkg->desc;
3957       $desc = substr($desc, 0, 50). '...'
3958         if $format eq 'latex' && length($desc) > 50;
3959
3960       my %details_opt = ( 'format'          => $format,
3961                           'escape_function' => $escape_function,
3962                           'format_function' => $format_function,
3963                         );
3964
3965       if ( $cust_bill_pkg->pkgnum > 0 ) {
3966
3967         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3968
3969         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3970
3971           my $description = $desc;
3972           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3973
3974           my @d = ();
3975           unless ( $cust_pkg->part_pkg->hide_svc_detail
3976                 || $cust_bill_pkg->hidden )
3977           {
3978             push @d, map &{$escape_function}($_),
3979                          $cust_pkg->h_labels_short($self->_date);
3980             if ( $multilocation ) {
3981               my $loc = $cust_pkg->location_label;
3982               $loc = substr($desc, 0, 50). '...'
3983                 if $format eq 'latex' && length($loc) > 50;
3984               push @d, &{$escape_function}($loc);
3985             }
3986           }
3987           push @d, $cust_bill_pkg->details(%details_opt)
3988             if $cust_bill_pkg->recur == 0;
3989
3990           if ( $cust_bill_pkg->hidden ) {
3991             $s->{amount}      += $cust_bill_pkg->setup;
3992             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3993             push @{ $s->{ext_description} }, @d;
3994           } else {
3995             $s = {
3996               description     => $description,
3997               #pkgpart         => $part_pkg->pkgpart,
3998               pkgnum          => $cust_bill_pkg->pkgnum,
3999               amount          => $cust_bill_pkg->setup,
4000               unit_amount     => $cust_bill_pkg->unitsetup,
4001               quantity        => $cust_bill_pkg->quantity,
4002               ext_description => \@d,
4003             };
4004           };
4005
4006         }
4007
4008         if ( $cust_bill_pkg->recur != 0 &&
4009              ( !$type || $type eq 'R' || $type eq 'U' )
4010            )
4011         {
4012
4013           my $is_summary = $display->summary;
4014           my $description = ($is_summary && $type && $type eq 'U')
4015                             ? "Usage charges" : $desc;
4016
4017           unless ( $conf->exists('disable_line_item_date_ranges') ) {
4018             $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4019                             " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4020           }
4021
4022           my @d = ();
4023
4024           #at least until cust_bill_pkg has "past" ranges in addition to
4025           #the "future" sdate/edate ones... see #3032
4026           my @dates = ( $self->_date );
4027           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4028           push @dates, $prev->sdate if $prev;
4029
4030           unless ( $cust_pkg->part_pkg->hide_svc_detail
4031                 || $cust_bill_pkg->itemdesc
4032                 || $cust_bill_pkg->hidden
4033                 || $is_summary && $type && $type eq 'U' )
4034           {
4035             push @d, map &{$escape_function}($_),
4036                          $cust_pkg->h_labels_short(@dates)
4037                                                    #$cust_bill_pkg->edate,
4038                                                    #$cust_bill_pkg->sdate)
4039             ;
4040             if ( $multilocation ) {
4041               my $loc = $cust_pkg->location_label;
4042               $loc = substr($desc, 0, 50). '...'
4043                 if $format eq 'latex' && length($loc) > 50;
4044               push @d, &{$escape_function}($loc);
4045             }
4046           }
4047
4048           push @d, $cust_bill_pkg->details(%details_opt)
4049             unless ($is_summary || $type && $type eq 'R');
4050   
4051           my $amount = 0;
4052           if (!$type) {
4053             $amount = $cust_bill_pkg->recur;
4054           }elsif($type eq 'R') {
4055             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4056           }elsif($type eq 'U') {
4057             $amount = $cust_bill_pkg->usage;
4058           }
4059   
4060           if ( !$type || $type eq 'R' ) {
4061
4062             if ( $cust_bill_pkg->hidden ) {
4063               $r->{amount}      += $amount;
4064               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4065               push @{ $r->{ext_description} }, @d;
4066             } else {
4067               $r = {
4068                 description     => $description,
4069                 #pkgpart         => $part_pkg->pkgpart,
4070                 pkgnum          => $cust_bill_pkg->pkgnum,
4071                 amount          => $amount,
4072                 unit_amount     => $cust_bill_pkg->unitrecur,
4073                 quantity        => $cust_bill_pkg->quantity,
4074                 ext_description => \@d,
4075               };
4076             }
4077
4078           } elsif ( $amount ) {  # && $type eq 'U'
4079
4080             if ( $cust_bill_pkg->hidden ) {
4081               $u->{amount}      += $amount;
4082               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4083               push @{ $u->{ext_description} }, @d;
4084             } else {
4085               $u = {
4086                 description     => $description,
4087                 #pkgpart         => $part_pkg->pkgpart,
4088                 pkgnum          => $cust_bill_pkg->pkgnum,
4089                 amount          => $amount,
4090                 unit_amount     => $cust_bill_pkg->unitrecur,
4091                 quantity        => $cust_bill_pkg->quantity,
4092                 ext_description => \@d,
4093               };
4094             }
4095
4096           }
4097
4098         } # recurring or usage with recurring charge
4099
4100       } else { #pkgnum tax or one-shot line item (??)
4101
4102         if ( $cust_bill_pkg->setup != 0 ) {
4103           push @b, {
4104             'description' => $desc,
4105             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4106           };
4107         }
4108         if ( $cust_bill_pkg->recur != 0 ) {
4109           push @b, {
4110             'description' => "$desc (".
4111                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4112                              time2str($date_format, $cust_bill_pkg->edate). ')',
4113             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4114           };
4115         }
4116
4117       }
4118
4119     }
4120
4121   }
4122
4123   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4124     if ( $_  ) {
4125       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4126       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4127       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4128       push @b, { %$_ }
4129         unless $_->{amount} == 0;
4130     }
4131   }
4132
4133   @b;
4134
4135 }
4136
4137 sub _items_credits {
4138   my( $self, %opt ) = @_;
4139   my $trim_len = $opt{'trim_len'} || 60;
4140
4141   my @b;
4142   #credits
4143   foreach ( $self->cust_credited ) {
4144
4145     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4146
4147     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4148     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4149     $reason = " ($reason) " if $reason;
4150
4151     push @b, {
4152       #'description' => 'Credit ref\#'. $_->crednum.
4153       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4154       #                 $reason,
4155       'description' => 'Credit applied '.
4156                        time2str($date_format,$_->cust_credit->_date). $reason,
4157       'amount'      => sprintf("%.2f",$_->amount),
4158     };
4159   }
4160
4161   @b;
4162
4163 }
4164
4165 sub _items_payments {
4166   my $self = shift;
4167
4168   my @b;
4169   #get & print payments
4170   foreach ( $self->cust_bill_pay ) {
4171
4172     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4173
4174     push @b, {
4175       'description' => "Payment received ".
4176                        time2str($date_format,$_->cust_pay->_date ),
4177       'amount'      => sprintf("%.2f", $_->amount )
4178     };
4179   }
4180
4181   @b;
4182
4183 }
4184
4185 =item call_details [ OPTION => VALUE ... ]
4186
4187 Returns an array of CSV strings representing the call details for this invoice
4188 The only option available is the boolean prepend_billed_number
4189
4190 =cut
4191
4192 sub call_details {
4193   my ($self, %opt) = @_;
4194
4195   my $format_function = sub { shift };
4196
4197   if ($opt{prepend_billed_number}) {
4198     $format_function = sub {
4199       my $detail = shift;
4200       my $row = shift;
4201
4202       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4203       
4204     };
4205   }
4206
4207   my @details = map { $_->details( 'format_function' => $format_function,
4208                                    'escape_function' => sub{ return() },
4209                                  )
4210                     }
4211                   grep { $_->pkgnum }
4212                   $self->cust_bill_pkg;
4213   my $header = $details[0];
4214   ( $header, grep { $_ ne $header } @details );
4215 }
4216
4217
4218 =back
4219
4220 =head1 SUBROUTINES
4221
4222 =over 4
4223
4224 =item process_reprint
4225
4226 =cut
4227
4228 sub process_reprint {
4229   process_re_X('print', @_);
4230 }
4231
4232 =item process_reemail
4233
4234 =cut
4235
4236 sub process_reemail {
4237   process_re_X('email', @_);
4238 }
4239
4240 =item process_refax
4241
4242 =cut
4243
4244 sub process_refax {
4245   process_re_X('fax', @_);
4246 }
4247
4248 =item process_reftp
4249
4250 =cut
4251
4252 sub process_reftp {
4253   process_re_X('ftp', @_);
4254 }
4255
4256 =item respool
4257
4258 =cut
4259
4260 sub process_respool {
4261   process_re_X('spool', @_);
4262 }
4263
4264 use Storable qw(thaw);
4265 use Data::Dumper;
4266 use MIME::Base64;
4267 sub process_re_X {
4268   my( $method, $job ) = ( shift, shift );
4269   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4270
4271   my $param = thaw(decode_base64(shift));
4272   warn Dumper($param) if $DEBUG;
4273
4274   re_X(
4275     $method,
4276     $job,
4277     %$param,
4278   );
4279
4280 }
4281
4282 sub re_X {
4283   my($method, $job, %param ) = @_;
4284   if ( $DEBUG ) {
4285     warn "re_X $method for job $job with param:\n".
4286          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4287   }
4288
4289   #some false laziness w/search/cust_bill.html
4290   my $distinct = '';
4291   my $orderby = 'ORDER BY cust_bill._date';
4292
4293   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4294
4295   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4296      
4297   my @cust_bill = qsearch( {
4298     #'select'    => "cust_bill.*",
4299     'table'     => 'cust_bill',
4300     'addl_from' => $addl_from,
4301     'hashref'   => {},
4302     'extra_sql' => $extra_sql,
4303     'order_by'  => $orderby,
4304     'debug' => 1,
4305   } );
4306
4307   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4308
4309   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4310     if $DEBUG;
4311
4312   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4313   foreach my $cust_bill ( @cust_bill ) {
4314     $cust_bill->$method();
4315
4316     if ( $job ) { #progressbar foo
4317       $num++;
4318       if ( time - $min_sec > $last ) {
4319         my $error = $job->update_statustext(
4320           int( 100 * $num / scalar(@cust_bill) )
4321         );
4322         die $error if $error;
4323         $last = time;
4324       }
4325     }
4326
4327   }
4328
4329 }
4330
4331 =back
4332
4333 =head1 CLASS METHODS
4334
4335 =over 4
4336
4337 =item owed_sql
4338
4339 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4340
4341 =cut
4342
4343 sub owed_sql {
4344   my ($class, $start, $end) = @_;
4345   'charged - '. 
4346     $class->paid_sql($start, $end). ' - '. 
4347     $class->credited_sql($start, $end);
4348 }
4349
4350 =item net_sql
4351
4352 Returns an SQL fragment to retreive the net amount (charged minus credited).
4353
4354 =cut
4355
4356 sub net_sql {
4357   my ($class, $start, $end) = @_;
4358   'charged - '. $class->credited_sql($start, $end);
4359 }
4360
4361 =item paid_sql
4362
4363 Returns an SQL fragment to retreive the amount paid against this invoice.
4364
4365 =cut
4366
4367 sub paid_sql {
4368   my ($class, $start, $end) = @_;
4369   $start &&= "AND cust_bill_pay._date <= $start";
4370   $end   &&= "AND cust_bill_pay._date > $end";
4371   $start = '' unless defined($start);
4372   $end   = '' unless defined($end);
4373   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4374        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
4375 }
4376
4377 =item credited_sql
4378
4379 Returns an SQL fragment to retreive the amount credited against this invoice.
4380
4381 =cut
4382
4383 sub credited_sql {
4384   my ($class, $start, $end) = @_;
4385   $start &&= "AND cust_credit_bill._date <= $start";
4386   $end   &&= "AND cust_credit_bill._date >  $end";
4387   $start = '' unless defined($start);
4388   $end   = '' unless defined($end);
4389   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4390        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
4391 }
4392
4393 =item search_sql_where HASHREF
4394
4395 Class method which returns an SQL WHERE fragment to search for parameters
4396 specified in HASHREF.  Valid parameters are
4397
4398 =over 4
4399
4400 =item _date
4401
4402 List reference of start date, end date, as UNIX timestamps.
4403
4404 =item invnum_min
4405
4406 =item invnum_max
4407
4408 =item agentnum
4409
4410 =item charged
4411
4412 List reference of charged limits (exclusive).
4413
4414 =item owed
4415
4416 List reference of charged limits (exclusive).
4417
4418 =item open
4419
4420 flag, return open invoices only
4421
4422 =item net
4423
4424 flag, return net invoices only
4425
4426 =item days
4427
4428 =item newest_percust
4429
4430 =back
4431
4432 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4433
4434 =cut
4435
4436 sub search_sql_where {
4437   my($class, $param) = @_;
4438   if ( $DEBUG ) {
4439     warn "$me search_sql_where called with params: \n".
4440          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
4441   }
4442
4443   my @search = ();
4444
4445   #agentnum
4446   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4447     push @search, "cust_main.agentnum = $1";
4448   }
4449
4450   #_date
4451   if ( $param->{_date} ) {
4452     my($beginning, $ending) = @{$param->{_date}};
4453
4454     push @search, "cust_bill._date >= $beginning",
4455                   "cust_bill._date <  $ending";
4456   }
4457
4458   #invnum
4459   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4460     push @search, "cust_bill.invnum >= $1";
4461   }
4462   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4463     push @search, "cust_bill.invnum <= $1";
4464   }
4465
4466   #charged
4467   if ( $param->{charged} ) {
4468     my @charged = ref($param->{charged})
4469                     ? @{ $param->{charged} }
4470                     : ($param->{charged});
4471
4472     push @search, map { s/^charged/cust_bill.charged/; $_; }
4473                       @charged;
4474   }
4475
4476   my $owed_sql = FS::cust_bill->owed_sql;
4477
4478   #owed
4479   if ( $param->{owed} ) {
4480     my @owed = ref($param->{owed})
4481                  ? @{ $param->{owed} }
4482                  : ($param->{owed});
4483     push @search, map { s/^owed/$owed_sql/; $_; }
4484                       @owed;
4485   }
4486
4487   #open/net flags
4488   push @search, "0 != $owed_sql"
4489     if $param->{'open'};
4490   push @search, '0 != '. FS::cust_bill->net_sql
4491     if $param->{'net'};
4492
4493   #days
4494   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4495     if $param->{'days'};
4496
4497   #newest_percust
4498   if ( $param->{'newest_percust'} ) {
4499
4500     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4501     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4502
4503     my @newest_where = map { my $x = $_;
4504                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
4505                              $x;
4506                            }
4507                            grep ! /^cust_main./, @search;
4508     my $newest_where = scalar(@newest_where)
4509                          ? ' AND '. join(' AND ', @newest_where)
4510                          : '';
4511
4512
4513     push @search, "cust_bill._date = (
4514       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4515         WHERE newest_cust_bill.custnum = cust_bill.custnum
4516           $newest_where
4517     )";
4518
4519   }
4520
4521   #agent virtualization
4522   my $curuser = $FS::CurrentUser::CurrentUser;
4523   if ( $curuser->username eq 'fs_queue'
4524        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4525     my $username = $1;
4526     my $newuser = qsearchs('access_user', {
4527       'username' => $username,
4528       'disabled' => '',
4529     } );
4530     if ( $newuser ) {
4531       $curuser = $newuser;
4532     } else {
4533       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4534     }
4535   }
4536   push @search, $curuser->agentnums_sql;
4537
4538   join(' AND ', @search );
4539
4540 }
4541
4542 =back
4543
4544 =head1 BUGS
4545
4546 The delete method.
4547
4548 =head1 SEE ALSO
4549
4550 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4551 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
4552 documentation.
4553
4554 =cut
4555
4556 1;
4557