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