fix historical services showing up on invoices, RT#24405
[freeside.git] / FS / FS / Template_Mixin.pm
1 package FS::Template_Mixin;
2
3 use strict;
4 use vars qw( $DEBUG $me
5              $money_char $date_format $rdate_format $date_format_long );
6              # but NOT $conf
7 use vars qw( $invoice_lines @buf ); #yuck
8 use List::Util qw(sum);
9 use Date::Format;
10 use Date::Language;
11 use Text::Template 1.20;
12 use File::Temp 0.14;
13 use HTML::Entities;
14 use Locale::Country;
15 use Cwd;
16 use FS::UID;
17 use FS::Record qw( qsearch qsearchs );
18 use FS::Misc qw( generate_ps generate_pdf );
19 use FS::pkg_category;
20 use FS::pkg_class;
21 use FS::L10N;
22
23 $DEBUG = 0;
24 $me = '[FS::Template_Mixin]';
25 FS::UID->install_callback( sub { 
26   my $conf = new FS::Conf; #global
27   $money_char       = $conf->config('money_char')       || '$';  
28   $date_format      = $conf->config('date_format')      || '%x'; #/YY
29   $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
30   $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
31 } );
32
33 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
34
35 Returns an text invoice, as a list of lines.
36
37 Options can be passed as a hashref (recommended) or as a list of time, template
38 and then any key/value pairs for any other options.
39
40 I<time>, if specified, is used to control the printing of overdue messages.  The
41 default is now.  It isn't the date of the invoice; that's the `_date' field.
42 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
43 L<Time::Local> and L<Date::Parse> for conversion functions.
44
45 I<template>, if specified, is the name of a suffix for alternate invoices.
46
47 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
48
49 =cut
50
51 sub print_text {
52   my $self = shift;
53   my( $today, $template, %opt );
54   if ( ref($_[0]) ) {
55     %opt = %{ shift() };
56     $today = delete($opt{'time'}) || '';
57     $template = delete($opt{template}) || '';
58   } else {
59     ( $today, $template, %opt ) = @_;
60   }
61
62   my %params = ( 'format' => 'template' );
63   $params{'time'} = $today if $today;
64   $params{'template'} = $template if $template;
65   $params{$_} = $opt{$_} 
66     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
67
68   $self->print_generic( %params );
69 }
70
71 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
72
73 Internal method - returns a filename of a filled-in LaTeX template for this
74 invoice (Note: add ".tex" to get the actual filename), and a filename of
75 an associated logo (with the .eps extension included).
76
77 See print_ps and print_pdf for methods that return PostScript and PDF output.
78
79 Options can be passed as a hashref (recommended) or as a list of time, template
80 and then any key/value pairs for any other options.
81
82 I<time>, if specified, is used to control the printing of overdue messages.  The
83 default is now.  It isn't the date of the invoice; that's the `_date' field.
84 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
85 L<Time::Local> and L<Date::Parse> for conversion functions.
86
87 I<template>, if specified, is the name of a suffix for alternate invoices.
88
89 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
90
91 =cut
92
93 sub print_latex {
94   my $self = shift;
95   my $conf = $self->conf;
96   my( $today, $template, %opt );
97   if ( ref($_[0]) ) {
98     %opt = %{ shift() };
99     $today = delete($opt{'time'}) || '';
100     $template = delete($opt{template}) || '';
101   } else {
102     ( $today, $template, %opt ) = @_;
103   }
104
105   my %params = ( 'format' => 'latex' );
106   $params{'time'} = $today if $today;
107   $params{'template'} = $template if $template;
108   $params{$_} = $opt{$_} 
109     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name no_date no_number );
110
111   $template ||= $self->_agent_template
112     if $self->can('_agent_template');
113
114   my $pkey = $self->primary_key;
115   my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
116
117   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
118   my $lh = new File::Temp(
119     TEMPLATE => $tmp_template,
120     DIR      => $dir,
121     SUFFIX   => '.eps',
122     UNLINK   => 0,
123   ) or die "can't open temp file: $!\n";
124
125   my $agentnum = $self->agentnum;
126
127   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
128     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
129       or die "can't write temp file: $!\n";
130   } else {
131     print $lh $conf->config_binary('logo.eps', $agentnum)
132       or die "can't write temp file: $!\n";
133   }
134   close $lh;
135   $params{'logo_file'} = $lh->filename;
136
137   if( $conf->exists('invoice-barcode') 
138         && $self->can('invoice_barcode')
139         && $self->invnum ) { # don't try to barcode statements
140       my $png_file = $self->invoice_barcode($dir);
141       my $eps_file = $png_file;
142       $eps_file =~ s/\.png$/.eps/g;
143       $png_file =~ /(barcode.*png)/;
144       $png_file = $1;
145       $eps_file =~ /(barcode.*eps)/;
146       $eps_file = $1;
147
148       my $curr_dir = cwd();
149       chdir($dir); 
150       # after painfuly long experimentation, it was determined that sam2p won't
151       # accept : and other chars in the path, no matter how hard I tried to
152       # escape them, hence the chdir (and chdir back, just to be safe)
153       system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
154         or die "sam2p failed: $!\n";
155       unlink($png_file);
156       chdir($curr_dir);
157
158       $params{'barcode_file'} = $eps_file;
159   }
160
161   my @filled_in = $self->print_generic( %params );
162   
163   my $fh = new File::Temp( TEMPLATE => $tmp_template,
164                            DIR      => $dir,
165                            SUFFIX   => '.tex',
166                            UNLINK   => 0,
167                          ) or die "can't open temp file: $!\n";
168   binmode($fh, ':utf8'); # language support
169   print $fh join('', @filled_in );
170   close $fh;
171
172   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
173   return ($1, $params{'logo_file'}, $params{'barcode_file'});
174
175 }
176
177 sub agentnum {
178   my $self = shift;
179   my $cust_main = $self->cust_main;
180   $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
181 }
182
183 =item print_generic OPTION => VALUE ...
184
185 Internal method - returns a filled-in template for this invoice as a scalar.
186
187 See print_ps and print_pdf for methods that return PostScript and PDF output.
188
189 Non optional options include 
190   format - latex, html, template
191
192 Optional options include
193
194 template - a value used as a suffix for a configuration template
195
196 time - a value used to control the printing of overdue messages.  The
197 default is now.  It isn't the date of the invoice; that's the `_date' field.
198 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
199 L<Time::Local> and L<Date::Parse> for conversion functions.
200
201 cid - 
202
203 unsquelch_cdr - overrides any per customer cdr squelching when true
204
205 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
206
207 locale - override customer's locale
208
209 =cut
210
211 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
212 # (alignment in text invoice?) problems to change them all to '%.2f' ?
213 # yes: fixed width/plain text printing will be borked
214 sub print_generic {
215   my( $self, %params ) = @_;
216   my $conf = $self->conf;
217   my $today = $params{today} ? $params{today} : time;
218   warn "$me print_generic called on $self with suffix $params{template}\n"
219     if $DEBUG;
220
221   my $format = $params{format};
222   die "Unknown format: $format"
223     unless $format =~ /^(latex|html|template)$/;
224
225   my $cust_main = $self->cust_main || $self->prospect_main;
226   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
227     unless $cust_main->payname
228         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
229
230   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
231                      'html'     => [ '<%=', '%>' ],
232                      'template' => [ '{', '}' ],
233                    );
234
235   warn "$me print_generic creating template\n"
236     if $DEBUG > 1;
237
238   #create the template
239   my $template = $params{template} ? $params{template} : $self->_agent_template;
240   my $templatefile = $self->template_conf. $format;
241   $templatefile .= "_$template"
242     if length($template) && $conf->exists($templatefile."_$template");
243   my @invoice_template = map "$_\n", $conf->config($templatefile)
244     or die "cannot load config data $templatefile";
245
246   my $old_latex = '';
247   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
248     #change this to a die when the old code is removed
249     warn "old-style invoice template $templatefile; ".
250          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
251     $old_latex = 'true';
252     @invoice_template = _translate_old_latex_format(@invoice_template);
253   } 
254
255   warn "$me print_generic creating T:T object\n"
256     if $DEBUG > 1;
257
258   my $text_template = new Text::Template(
259     TYPE => 'ARRAY',
260     SOURCE => \@invoice_template,
261     DELIMITERS => $delimiters{$format},
262   );
263
264   warn "$me print_generic compiling T:T object\n"
265     if $DEBUG > 1;
266
267   $text_template->compile()
268     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
269
270
271   # additional substitution could possibly cause breakage in existing templates
272   my %convert_maps = ( 
273     'latex' => {
274                  'notes'         => sub { map "$_", @_ },
275                  'footer'        => sub { map "$_", @_ },
276                  'smallfooter'   => sub { map "$_", @_ },
277                  'returnaddress' => sub { map "$_", @_ },
278                  'coupon'        => sub { map "$_", @_ },
279                  'summary'       => sub { map "$_", @_ },
280                },
281     'html'  => {
282                  'notes' =>
283                    sub {
284                      map { 
285                        s/%%(.*)$/<!-- $1 -->/g;
286                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
287                        s/\\begin\{enumerate\}/<ol>/g;
288                        s/\\item /  <li>/g;
289                        s/\\end\{enumerate\}/<\/ol>/g;
290                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
291                        s/\\\\\*/<br>/g;
292                        s/\\dollar ?/\$/g;
293                        s/\\#/#/g;
294                        s/~/&nbsp;/g;
295                        $_;
296                      }  @_
297                    },
298                  'footer' =>
299                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
300                  'smallfooter' =>
301                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
302                  'returnaddress' =>
303                    sub {
304                      map { 
305                        s/~/&nbsp;/g;
306                        s/\\\\\*?\s*$/<BR>/;
307                        s/\\hyphenation\{[\w\s\-]+}//;
308                        s/\\([&])/$1/g;
309                        $_;
310                      }  @_
311                    },
312                  'coupon'        => sub { "" },
313                  'summary'       => sub { "" },
314                },
315     'template' => {
316                  'notes' =>
317                    sub {
318                      map { 
319                        s/%%.*$//g;
320                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
321                        s/\\begin\{enumerate\}//g;
322                        s/\\item /  * /g;
323                        s/\\end\{enumerate\}//g;
324                        s/\\textbf\{(.*)\}/$1/g;
325                        s/\\\\\*/ /;
326                        s/\\dollar ?/\$/g;
327                        $_;
328                      }  @_
329                    },
330                  'footer' =>
331                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
332                  'smallfooter' =>
333                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
334                  'returnaddress' =>
335                    sub {
336                      map { 
337                        s/~/ /g;
338                        s/\\\\\*?\s*$/\n/;             # dubious
339                        s/\\hyphenation\{[\w\s\-]+}//;
340                        $_;
341                      }  @_
342                    },
343                  'coupon'        => sub { "" },
344                  'summary'       => sub { "" },
345                },
346   );
347
348
349   # hashes for differing output formats
350   my %nbsps = ( 'latex'    => '~',
351                 'html'     => '',    # '&nbps;' would be nice
352                 'template' => '',    # not used
353               );
354   my $nbsp = $nbsps{$format};
355
356   my %escape_functions = ( 'latex'    => \&_latex_escape,
357                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
358                            'template' => sub { shift },
359                          );
360   my $escape_function = $escape_functions{$format};
361   my $escape_function_nonbsp = ($format eq 'html')
362                                  ? \&_html_escape : $escape_function;
363
364   my %date_formats = ( 'latex'    => $date_format_long,
365                        'html'     => $date_format_long,
366                        'template' => '%s',
367                      );
368   $date_formats{'html'} =~ s/ /&nbsp;/g;
369
370   my $date_format = $date_formats{$format};
371
372   my %newline_tokens = (  'latex'     => '\\\\',
373                           'html'      => '<br>',
374                           'template'  => "\n",
375                         );
376   my $newline_token = $newline_tokens{$format};
377
378   warn "$me generating template variables\n"
379     if $DEBUG > 1;
380
381   # generate template variables
382   my $returnaddress;
383   if (
384          defined( $conf->config_orbase( "invoice_${format}returnaddress",
385                                         $template
386                                       )
387                 )
388        && length( $conf->config_orbase( "invoice_${format}returnaddress",
389                                         $template
390                                       )
391                 )
392   ) {
393
394     $returnaddress = join("\n",
395       $conf->config_orbase("invoice_${format}returnaddress", $template)
396     );
397
398   } elsif ( grep /\S/,
399             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
400
401     my $convert_map = $convert_maps{$format}{'returnaddress'};
402     $returnaddress =
403       join( "\n",
404             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
405                                                  $template
406                                                )
407                          )
408           );
409   } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
410
411     my $convert_map = $convert_maps{$format}{'returnaddress'};
412     $returnaddress = join( "\n", &$convert_map(
413                                    map { s/( {2,})/'~' x length($1)/eg;
414                                          s/$/\\\\\*/;
415                                          $_
416                                        }
417                                      ( $conf->config('company_name', $cust_main->agentnum),
418                                        $conf->config('company_address', $cust_main->agentnum),
419                                      )
420                                  )
421                      );
422
423   } else {
424
425     my $warning = "Couldn't find a return address; ".
426                   "do you need to set the company_address configuration value?";
427     warn "$warning\n";
428     $returnaddress = $nbsp;
429     #$returnaddress = $warning;
430
431   }
432
433   warn "$me generating invoice data\n"
434     if $DEBUG > 1;
435
436   my $agentnum = $cust_main->agentnum;
437
438   my %invoice_data = (
439
440     #invoice from info
441     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
442     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
443     'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
444     'returnaddress'   => $returnaddress,
445     'agent'           => &$escape_function($cust_main->agent->agent),
446
447     #invoice/quotation info
448     'no_number'       => $params{'no_number'},
449     'invnum'          => ( $params{'no_number'} ? '' : $self->invnum ),
450     'quotationnum'    => $self->quotationnum,
451     'no_date'         => $params{'no_date'},
452     '_date'           => ( $params{'no_date'} ? '' : $self->_date ),
453     'date'            => ( $params{'no_date'}
454                              ? ''
455                              : time2str($date_format, $self->_date)
456                          ),
457     'today'           => time2str($date_format_long, $today),
458     'terms'           => $self->terms,
459     'template'        => $template, #params{'template'},
460     'notice_name'     => ($params{'notice_name'} || $self->notice_name),#escape_function?
461     'current_charges' => sprintf("%.2f", $self->charged),
462     'duedate'         => $self->due_date2str($rdate_format), #date_format?
463
464     #customer info
465     'custnum'         => $cust_main->display_custnum,
466     'prospectnum'     => $cust_main->prospectnum,
467     'agent_custid'    => &$escape_function($cust_main->agent_custid),
468     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
469       payname company address1 address2 city state zip fax
470     )),
471
472     #global config
473     'ship_enable'     => $conf->exists('invoice-ship_address'),
474     'unitprices'      => $conf->exists('invoice-unitprice'),
475     'smallernotes'    => $conf->exists('invoice-smallernotes'),
476     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
477     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
478    
479     #layout info -- would be fancy to calc some of this and bury the template
480     #               here in the code
481     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
482     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
483     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
484     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
485     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
486     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
487     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
488     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
489     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
490     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
491
492     # better hang on to conf_dir for a while (for old templates)
493     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
494
495     #these are only used when doing paged plaintext
496     'page'            => 1,
497     'total_pages'     => 1,
498
499   );
500  
501   #localization
502   my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
503   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
504   my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
505   # eval to avoid death for unimplemented languages
506   my $dh = eval { Date::Language->new($info{'name'}) } ||
507            Date::Language->new(); # fall back to English
508   # prototype here to silence warnings
509   $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
510   # eventually use this date handle everywhere in here, too
511
512   my $min_sdate = 999999999999;
513   my $max_edate = 0;
514   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
515     next unless $cust_bill_pkg->pkgnum > 0;
516     $min_sdate = $cust_bill_pkg->sdate
517       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
518     $max_edate = $cust_bill_pkg->edate
519       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
520   }
521
522   $invoice_data{'bill_period'} = '';
523   $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
524     . " to " . time2str('%e %h', $max_edate)
525     if ($max_edate != 0 && $min_sdate != 999999999999);
526
527   $invoice_data{finance_section} = '';
528   if ( $conf->config('finance_pkgclass') ) {
529     my $pkg_class =
530       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
531     $invoice_data{finance_section} = $pkg_class->categoryname;
532   } 
533   $invoice_data{finance_amount} = '0.00';
534   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
535
536   my $countrydefault = $conf->config('countrydefault') || 'US';
537   foreach ( qw( address1 address2 city state zip country fax) ){
538     my $method = 'ship_'.$_;
539     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
540   }
541   foreach ( qw( contact company ) ) { #compatibility
542     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
543   }
544   $invoice_data{'ship_country'} = ''
545     if ( $invoice_data{'ship_country'} eq $countrydefault );
546   
547   $invoice_data{'cid'} = $params{'cid'}
548     if $params{'cid'};
549
550   if ( $cust_main->country eq $countrydefault ) {
551     $invoice_data{'country'} = '';
552   } else {
553     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
554   }
555
556   my @address = ();
557   $invoice_data{'address'} = \@address;
558   push @address,
559     $cust_main->payname.
560       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
561         ? " (P.O. #". $cust_main->payinfo. ")"
562         : ''
563       )
564   ;
565   push @address, $cust_main->company
566     if $cust_main->company;
567   push @address, $cust_main->address1;
568   push @address, $cust_main->address2
569     if $cust_main->address2;
570   push @address,
571     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
572   push @address, $invoice_data{'country'}
573     if $invoice_data{'country'};
574   push @address, ''
575     while (scalar(@address) < 5);
576
577   $invoice_data{'logo_file'} = $params{'logo_file'}
578     if $params{'logo_file'};
579   $invoice_data{'barcode_file'} = $params{'barcode_file'}
580     if $params{'barcode_file'};
581   $invoice_data{'barcode_img'} = $params{'barcode_img'}
582     if $params{'barcode_img'};
583   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
584     if $params{'barcode_cid'};
585
586   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
587 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
588   #my $balance_due = $self->owed + $pr_total - $cr_total;
589   my $balance_due = $self->owed + $pr_total;
590
591   #these are used on the summary page only
592
593     # the customer's current balance as shown on the invoice before this one
594     $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
595
596     # the change in balance from that invoice to this one
597     $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
598
599     # the sum of amount owed on all previous invoices
600     # ($pr_total is used elsewhere but not as $previous_balance)
601     $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
602
603   # the sum of amount owed on all invoices
604   # (this is used in the summary & on the payment coupon)
605   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
606
607   # info from customer's last invoice before this one, for some 
608   # summary formats
609   $invoice_data{'last_bill'} = {};
610
611   # returns the last unpaid bill, not the last bill
612   #my $last_bill = $pr_cust_bill[-1];
613
614   if ( $self->custnum && $self->invnum ) {
615
616     # THIS returns the customer's last bill before  this one
617     my $last_bill = qsearchs({
618         'table'   => 'cust_bill',
619         'hashref' => { 'custnum' => $self->custnum,
620                        'invnum'  => { op => '<', value => $self->invnum },
621                      },
622         'order_by'  => ' ORDER BY invnum DESC LIMIT 1'
623     });
624     if ( $last_bill ) {
625       $invoice_data{'last_bill'} = {
626         '_date'     => $last_bill->_date, #unformatted
627         # all we need for now
628       };
629       my (@payments, @credits);
630       # for formats that itemize previous payments
631       foreach my $cust_pay ( qsearch('cust_pay', {
632                               'custnum' => $self->custnum,
633                               '_date'   => { op => '>=',
634                                              value => $last_bill->_date }
635                              } ) )
636       {
637         next if $cust_pay->_date > $self->_date;
638         push @payments, {
639             '_date'       => $cust_pay->_date,
640             'date'        => time2str($date_format, $cust_pay->_date),
641             'payinfo'     => $cust_pay->payby_payinfo_pretty,
642             'amount'      => sprintf('%.2f', $cust_pay->paid),
643         };
644         # not concerned about applications
645       }
646       foreach my $cust_credit ( qsearch('cust_credit', {
647                               'custnum' => $self->custnum,
648                               '_date'   => { op => '>=',
649                                              value => $last_bill->_date }
650                              } ) )
651       {
652         next if $cust_credit->_date > $self->_date;
653         push @credits, {
654             '_date'       => $cust_credit->_date,
655             'date'        => time2str($date_format, $cust_credit->_date),
656             'creditreason'=> $cust_credit->reason,
657             'amount'      => sprintf('%.2f', $cust_credit->amount),
658         };
659       }
660       $invoice_data{'previous_payments'} = \@payments;
661       $invoice_data{'previous_credits'}  = \@credits;
662     }
663
664   }
665
666   my $summarypage = '';
667   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
668     $summarypage = 1;
669   }
670   $invoice_data{'summarypage'} = $summarypage;
671
672   warn "$me substituting variables in notes, footer, smallfooter\n"
673     if $DEBUG > 1;
674
675   my $tc = $self->template_conf;
676   my @include = ( [ $tc,        'notes' ],
677                   [ 'invoice_', 'footer' ],
678                   [ 'invoice_', 'smallfooter', ],
679                 );
680   push @include, [ $tc,        'coupon', ]
681     unless $params{'no_coupon'};
682
683   foreach my $i (@include) {
684
685     my($base, $include) = @$i;
686
687     my $inc_file = $conf->key_orbase("$base$format$include", $template);
688     my @inc_src;
689
690     if ( $conf->exists($inc_file, $agentnum)
691          && length( $conf->config($inc_file, $agentnum) ) ) {
692
693       @inc_src = $conf->config($inc_file, $agentnum);
694
695     } else {
696
697       $inc_file = $conf->key_orbase("${base}latex$include", $template);
698
699       my $convert_map = $convert_maps{$format}{$include};
700
701       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
702                        s/--\@\]/$delimiters{$format}[1]/g;
703                        $_;
704                      } 
705                  &$convert_map( $conf->config($inc_file, $agentnum) );
706
707     }
708
709     my $inc_tt = new Text::Template (
710       TYPE       => 'ARRAY',
711       SOURCE     => [ map "$_\n", @inc_src ],
712       DELIMITERS => $delimiters{$format},
713     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
714
715     unless ( $inc_tt->compile() ) {
716       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
717       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
718       die $error;
719     }
720
721     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
722
723     $invoice_data{$include} =~ s/\n+$//
724       if ($format eq 'latex');
725   }
726
727   # let invoices use either of these as needed
728   $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
729     ? $cust_main->payinfo : '';
730   $invoice_data{'po_line'} = 
731     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
732       ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
733       : $nbsp;
734
735   my %money_chars = ( 'latex'    => '',
736                       'html'     => $conf->config('money_char') || '$',
737                       'template' => '',
738                     );
739   my $money_char = $money_chars{$format};
740
741   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
742                             'html'     => $conf->config('money_char') || '$',
743                             'template' => '',
744                           );
745   my $other_money_char = $other_money_chars{$format};
746   $invoice_data{'dollar'} = $other_money_char;
747
748   my %minus_signs = ( 'latex'    => '$-$',
749                       'html'     => '&minus;',
750                       'template' => '- ' );
751   my $minus = $minus_signs{$format};
752
753   my @detail_items = ();
754   my @total_items = ();
755   my @buf = ();
756   my @sections = ();
757
758   $invoice_data{'detail_items'} = \@detail_items;
759   $invoice_data{'total_items'} = \@total_items;
760   $invoice_data{'buf'} = \@buf;
761   $invoice_data{'sections'} = \@sections;
762
763   warn "$me generating sections\n"
764     if $DEBUG > 1;
765
766   my $taxtotal = 0;
767   my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
768                       'subtotal'    => $taxtotal,   # adjusted below
769                     };
770   my $tax_weight = _pkg_category($tax_section->{description})
771                         ? _pkg_category($tax_section->{description})->weight
772                         : 0;
773   $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
774   $tax_section->{'sort_weight'} = $tax_weight;
775
776   my $adjusttotal = 0;
777   my $adjust_section = {
778     'description'    => $self->mt('Credits, Payments, and Adjustments'),
779     'adjust_section' => 1,
780     'subtotal'       => 0,   # adjusted below
781   };
782   my $adjust_weight = _pkg_category($adjust_section->{description})
783                         ? _pkg_category($adjust_section->{description})->weight
784                         : 0;
785   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
786   $adjust_section->{'sort_weight'} = $adjust_weight;
787
788   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
789   my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
790                      $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
791   $invoice_data{'multisection'} = $multisection;
792   my $late_sections;
793   my $extra_sections = [];
794   my $extra_lines = ();
795
796   # default section ('Charges')
797   my $default_section = { 'description' => '',
798                           'subtotal'    => '', 
799                           'no_subtotal' => 1,
800                         };
801
802   # Previous Charges section
803   # subtotal is the first return value from $self->previous
804   my $previous_section;
805   # if the invoice has major sections, or if we're summarizing previous 
806   # charges with a single line, or if we've been specifically told to put them
807   # in a section, create a section for previous charges:
808   if ( $multisection or
809        $conf->exists('previous_balance-summary_only') or
810        $conf->exists('previous_balance-section') ) {
811     
812     $previous_section =  { 'description' => $self->mt('Previous Charges'),
813                            'subtotal'    => $other_money_char.
814                                             sprintf('%.2f', $pr_total),
815                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
816                          };
817     $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
818       join(' / ', map { $cust_main->balance_date_range(@$_) }
819                   $self->_prior_month30s
820           )
821       if $conf->exists('invoice_include_aging');
822
823   } else {
824     # otherwise put them in the main section
825     $previous_section = $default_section;
826   }
827
828   if ( $multisection ) {
829     ($extra_sections, $extra_lines) =
830       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
831       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
832       && $self->can('_items_extra_usage_sections');
833
834     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
835
836     push @detail_items, @$extra_lines if $extra_lines;
837
838     # the code is written so that both methods can be used together, but
839     # we haven't yet changed the template to take advantage of that, so for 
840     # now, treat them as mutually exclusive.
841     my %section_method = ( by_category => 1 );
842     if ( $conf->exists($tc.'sections_by_location') ) {
843       %section_method = ( by_location => 1 );
844     }
845     my ($early, $late) =
846       $self->_items_sections( 'summary' => $summarypage,
847                               'escape'  => $escape_function_nonbsp,
848                               'extra_sections' => $extra_sections,
849                               'format'  => $format,
850                               %section_method
851                             );
852     push @sections, @$early;
853     $late_sections = $late;
854
855     if (    $conf->exists('svc_phone_sections')
856          && $self->can('_items_svc_phone_sections')
857        )
858     {
859       my ($phone_sections, $phone_lines) =
860         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
861       push @{$late_sections}, @$phone_sections;
862       push @detail_items, @$phone_lines;
863     }
864     if ( $conf->exists('voip-cust_accountcode_cdr')
865          && $cust_main->accountcode_cdr
866          && $self->can('_items_accountcode_cdr')
867        )
868     {
869       my ($accountcode_section, $accountcode_lines) =
870         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
871       if ( scalar(@$accountcode_lines) ) {
872           push @{$late_sections}, $accountcode_section;
873           push @detail_items, @$accountcode_lines;
874       }
875     }
876   } else {# not multisection
877     # make a default section
878     push @sections, $default_section;
879     # and calculate the finance charge total, since it won't get done otherwise.
880     # and the default section total
881     # XXX possibly finance_pkgclass should not be used in this manner?
882     my @finance_charges;
883     my @charges;
884     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
885       if ( $invoice_data{finance_section} and 
886         grep { $_->section eq $invoice_data{finance_section} }
887            $cust_bill_pkg->cust_bill_pkg_display ) {
888         # I think these are always setup fees, but just to be sure...
889         push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
890       } else {
891         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
892       }
893     }
894     $invoice_data{finance_amount} = 
895       sprintf('%.2f', sum( @finance_charges ) || 0);
896     $default_section->{subtotal} = $other_money_char.
897                                     sprintf('%.2f', sum( @charges ) || 0);
898   }
899
900   # previous invoice balances in the Previous Charges section if there
901   # is one, otherwise in the main detail section
902   # (except if summary_only is enabled, don't show them at all)
903   if ( $self->can('_items_previous') &&
904        $self->enable_previous &&
905        ! $conf->exists('previous_balance-summary_only') ) {
906
907     warn "$me adding previous balances\n"
908       if $DEBUG > 1;
909
910     foreach my $line_item ( $self->_items_previous ) {
911
912       my $detail = {
913         ref             => $line_item->{'pkgnum'},
914         pkgpart         => $line_item->{'pkgpart'},
915         quantity        => 1,
916         section         => $previous_section, # which might be $default_section
917         description     => &$escape_function($line_item->{'description'}),
918         ext_description => [ map { &$escape_function($_) } 
919                              @{ $line_item->{'ext_description'} || [] }
920                            ],
921         amount          => ( $old_latex ? '' : $money_char).
922                             $line_item->{'amount'},
923         product_code    => $line_item->{'pkgpart'} || 'N/A',
924       };
925
926       push @detail_items, $detail;
927       push @buf, [ $detail->{'description'},
928                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
929                  ];
930     }
931
932   }
933
934   if ( @pr_cust_bill && $self->enable_previous ) {
935     push @buf, ['','-----------'];
936     push @buf, [ $self->mt('Total Previous Balance'),
937                  $money_char. sprintf("%10.2f", $pr_total) ];
938     push @buf, ['',''];
939   }
940  
941   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
942       warn "$me adding DID summary\n"
943         if $DEBUG > 1;
944
945       my ($didsummary,$minutes) = $self->_did_summary;
946       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
947       push @detail_items, 
948        { 'description' => $didsummary_desc,
949            'ext_description' => [ $didsummary, $minutes ],
950        };
951   }
952
953   foreach my $section (@sections, @$late_sections) {
954
955     warn "$me adding section \n". Dumper($section)
956       if $DEBUG > 1;
957
958     # begin some normalization
959     $section->{'subtotal'} = $section->{'amount'}
960       if $multisection
961          && !exists($section->{subtotal})
962          && exists($section->{amount});
963
964     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
965       if ( $invoice_data{finance_section} &&
966            $section->{'description'} eq $invoice_data{finance_section} );
967
968     $section->{'subtotal'} = $other_money_char.
969                              sprintf('%.2f', $section->{'subtotal'})
970       if $multisection;
971
972     # continue some normalization
973     $section->{'amount'}   = $section->{'subtotal'}
974       if $multisection;
975
976
977     if ( $section->{'description'} ) {
978       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
979                    [ '', '' ],
980                  );
981     }
982
983     warn "$me   setting options\n"
984       if $DEBUG > 1;
985
986     my %options = ();
987     $options{'section'} = $section if $multisection;
988     $options{'format'} = $format;
989     $options{'escape_function'} = $escape_function;
990     $options{'no_usage'} = 1 unless $unsquelched;
991     $options{'unsquelched'} = $unsquelched;
992     $options{'summary_page'} = $summarypage;
993     $options{'skip_usage'} =
994       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
995
996     warn "$me   searching for line items\n"
997       if $DEBUG > 1;
998
999     foreach my $line_item ( $self->_items_pkg(%options) ) {
1000
1001       warn "$me     adding line item $line_item\n"
1002         if $DEBUG > 1;
1003
1004       my $detail = {
1005         ext_description => [],
1006       };
1007       $detail->{'ref'} = $line_item->{'pkgnum'};
1008       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
1009       $detail->{'quantity'} = $line_item->{'quantity'};
1010       $detail->{'section'} = $section;
1011       $detail->{'description'} = &$escape_function($line_item->{'description'});
1012       if ( exists $line_item->{'ext_description'} ) {
1013         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1014       }
1015       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1016                               $line_item->{'amount'};
1017       if ( exists $line_item->{'unit_amount'} ) {
1018         $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1019                                    $line_item->{'unit_amount'};
1020       }
1021       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1022
1023       $detail->{'sdate'} = $line_item->{'sdate'};
1024       $detail->{'edate'} = $line_item->{'edate'};
1025       $detail->{'seconds'} = $line_item->{'seconds'};
1026       $detail->{'svc_label'} = $line_item->{'svc_label'};
1027   
1028       push @detail_items, $detail;
1029       push @buf, ( [ $detail->{'description'},
1030                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1031                    ],
1032                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1033                  );
1034     }
1035
1036     if ( $section->{'description'} ) {
1037       push @buf, ( ['','-----------'],
1038                    [ $section->{'description'}. ' sub-total',
1039                       $section->{'subtotal'} # already formatted this 
1040                    ],
1041                    [ '', '' ],
1042                    [ '', '' ],
1043                  );
1044     }
1045   
1046   }
1047
1048   $invoice_data{current_less_finance} =
1049     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1050
1051   # if there's anything in the Previous Charges section, prepend it to the list
1052   if ( $pr_total and $previous_section ne $default_section ) {
1053     unshift @sections, $previous_section;
1054   }
1055
1056   warn "$me adding taxes\n"
1057     if $DEBUG > 1;
1058
1059   my @items_tax = $self->_items_tax;
1060   foreach my $tax ( @items_tax ) {
1061
1062     $taxtotal += $tax->{'amount'};
1063
1064     my $description = &$escape_function( $tax->{'description'} );
1065     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
1066
1067     if ( $multisection ) {
1068
1069       my $money = $old_latex ? '' : $money_char;
1070       push @detail_items, {
1071         ext_description => [],
1072         ref          => '',
1073         quantity     => '',
1074         description  => $description,
1075         amount       => $money. $amount,
1076         product_code => '',
1077         section      => $tax_section,
1078       };
1079
1080     } else {
1081
1082       push @total_items, {
1083         'total_item'   => $description,
1084         'total_amount' => $other_money_char. $amount,
1085       };
1086
1087     }
1088
1089     push @buf,[ $description,
1090                 $money_char. $amount,
1091               ];
1092
1093   }
1094   
1095   if ( @items_tax ) {
1096     my $total = {};
1097     $total->{'total_item'} = $self->mt('Sub-total');
1098     $total->{'total_amount'} =
1099       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1100
1101     if ( $multisection ) {
1102       $tax_section->{'subtotal'} = $other_money_char.
1103                                    sprintf('%.2f', $taxtotal);
1104       $tax_section->{'pretotal'} = 'New charges sub-total '.
1105                                    $total->{'total_amount'};
1106       push @sections, $tax_section if $taxtotal;
1107     }else{
1108       unshift @total_items, $total;
1109     }
1110   }
1111   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1112
1113   push @buf,['','-----------'];
1114   push @buf,[$self->mt( 
1115               (!$self->enable_previous)
1116                ? 'Total Charges'
1117                : 'Total New Charges'
1118              ),
1119              $money_char. sprintf("%10.2f",$self->charged) ];
1120   push @buf,['',''];
1121
1122
1123   ###
1124   # Totals
1125   ###
1126
1127   my %embolden_functions = (
1128     'latex'    => sub { return '\textbf{'. shift(). '}' },
1129     'html'     => sub { return '<b>'. shift(). '</b>' },
1130     'template' => sub { shift },
1131   );
1132   my $embolden_function = $embolden_functions{$format};
1133
1134   if ( $self->can('_items_total') ) { # quotations
1135
1136     $self->_items_total(\@total_items);
1137
1138     foreach ( @total_items ) {
1139       $_->{'total_item'}   = &$embolden_function( $_->{'total_item'} );
1140       $_->{'total_amount'} = &$embolden_function( $other_money_char.
1141                                                    $_->{'total_amount'}
1142                                                 );
1143     }
1144
1145   } else { #normal invoice case
1146
1147     # calculate total, possibly including total owed on previous
1148     # invoices
1149     my $total = {};
1150     my $item = 'Total';
1151     $item = $conf->config('previous_balance-exclude_from_total')
1152          || 'Total New Charges'
1153       if $conf->exists('previous_balance-exclude_from_total');
1154     my $amount = $self->charged;
1155     if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1156       $amount += $pr_total;
1157     }
1158
1159     $total->{'total_item'} = &$embolden_function($self->mt($item));
1160     $total->{'total_amount'} =
1161       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
1162     if ( $multisection ) {
1163       if ( $adjust_section->{'sort_weight'} ) {
1164         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1165           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
1166       } else {
1167         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1168           $other_money_char.  sprintf('%.2f', $self->charged );
1169       } 
1170     }else{
1171       push @total_items, $total;
1172     }
1173     push @buf,['','-----------'];
1174     push @buf,[$item,
1175                $money_char.
1176                sprintf( '%10.2f', $amount )
1177               ];
1178     push @buf,['',''];
1179
1180     # if we're showing previous invoices, also show previous
1181     # credits and payments 
1182     if ( $self->enable_previous 
1183           and $self->can('_items_credits')
1184           and $self->can('_items_payments') )
1185       {
1186       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1187     
1188       # credits
1189       my $credittotal = 0;
1190       foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1191
1192         my $total;
1193         $total->{'total_item'} = &$escape_function($credit->{'description'});
1194         $credittotal += $credit->{'amount'};
1195         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1196         $adjusttotal += $credit->{'amount'};
1197         if ( $multisection ) {
1198           my $money = $old_latex ? '' : $money_char;
1199           push @detail_items, {
1200             ext_description => [],
1201             ref          => '',
1202             quantity     => '',
1203             description  => &$escape_function($credit->{'description'}),
1204             amount       => $money. $credit->{'amount'},
1205             product_code => '',
1206             section      => $adjust_section,
1207           };
1208         } else {
1209           push @total_items, $total;
1210         }
1211
1212       }
1213       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1214
1215       #credits (again)
1216       foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1217         push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1218       }
1219
1220       # payments
1221       my $paymenttotal = 0;
1222       foreach my $payment ( $self->_items_payments ) {
1223         my $total = {};
1224         $total->{'total_item'} = &$escape_function($payment->{'description'});
1225         $paymenttotal += $payment->{'amount'};
1226         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1227         $adjusttotal += $payment->{'amount'};
1228         if ( $multisection ) {
1229           my $money = $old_latex ? '' : $money_char;
1230           push @detail_items, {
1231             ext_description => [],
1232             ref          => '',
1233             quantity     => '',
1234             description  => &$escape_function($payment->{'description'}),
1235             amount       => $money. $payment->{'amount'},
1236             product_code => '',
1237             section      => $adjust_section,
1238           };
1239         }else{
1240           push @total_items, $total;
1241         }
1242         push @buf, [ $payment->{'description'},
1243                      $money_char. sprintf("%10.2f", $payment->{'amount'}),
1244                    ];
1245       }
1246       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1247     
1248       if ( $multisection ) {
1249         $adjust_section->{'subtotal'} = $other_money_char.
1250                                         sprintf('%.2f', $adjusttotal);
1251         push @sections, $adjust_section
1252           unless $adjust_section->{sort_weight};
1253       }
1254
1255       # create Balance Due message
1256       { 
1257         my $total;
1258         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1259         $total->{'total_amount'} =
1260           &$embolden_function(
1261             $other_money_char. sprintf('%.2f', #why? $summarypage 
1262                                                #  ? $self->charged +
1263                                                #    $self->billing_balance
1264                                                #  :
1265                                                    $self->owed + $pr_total
1266                                       )
1267           );
1268         if ( $multisection && !$adjust_section->{sort_weight} ) {
1269           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1270                                            $total->{'total_amount'};
1271         }else{
1272           push @total_items, $total;
1273         }
1274         push @buf,['','-----------'];
1275         push @buf,[$self->balance_due_msg, $money_char. 
1276           sprintf("%10.2f", $balance_due ) ];
1277       }
1278
1279       if ( $conf->exists('previous_balance-show_credit')
1280           and $cust_main->balance < 0 ) {
1281         my $credit_total = {
1282           'total_item'    => &$embolden_function($self->credit_balance_msg),
1283           'total_amount'  => &$embolden_function(
1284             $other_money_char. sprintf('%.2f', -$cust_main->balance)
1285           ),
1286         };
1287         if ( $multisection ) {
1288           $adjust_section->{'posttotal'} .= $newline_token .
1289             $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1290         }
1291         else {
1292           push @total_items, $credit_total;
1293         }
1294         push @buf,['','-----------'];
1295         push @buf,[$self->credit_balance_msg, $money_char. 
1296           sprintf("%10.2f", -$cust_main->balance ) ];
1297       }
1298     }
1299
1300   } #end of default total adding ! can('_items_total')
1301
1302   if ( $multisection ) {
1303     if (    $conf->exists('svc_phone_sections')
1304          && $self->can('_items_svc_phone_sections')
1305        )
1306     {
1307       my $total;
1308       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1309       $total->{'total_amount'} =
1310         &$embolden_function(
1311           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1312         );
1313       my $last_section = pop @sections;
1314       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1315                                      $total->{'total_amount'};
1316       push @sections, $last_section;
1317     }
1318     push @sections, @$late_sections
1319       if $unsquelched;
1320   }
1321
1322   # make a discounts-available section, even without multisection
1323   if ( $conf->exists('discount-show_available') 
1324        and my @discounts_avail = $self->_items_discounts_avail ) {
1325     my $discount_section = {
1326       'description' => $self->mt('Discounts Available'),
1327       'subtotal'    => '',
1328       'no_subtotal' => 1,
1329     };
1330
1331     push @sections, $discount_section;
1332     push @detail_items, map { +{
1333         'ref'         => '', #should this be something else?
1334         'section'     => $discount_section,
1335         'description' => &$escape_function( $_->{description} ),
1336         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1337         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1338     } } @discounts_avail;
1339   }
1340
1341   my @summary_subtotals;
1342   # the templates say "$_->{tax_section} || !$_->{summarized}"
1343   # except 'summarized' is only true when tax_section is true, so this 
1344   # is always true, so what's the deal?
1345   foreach my $s (@sections) {
1346     # not to include in the "summary of new charges" block:
1347     # finance charges, adjustments, previous charges, 
1348     # and itemized phone usage sections
1349     if ( $s eq $adjust_section   or
1350          ($s eq $previous_section and $s ne $default_section) or
1351          ($invoice_data{'finance_section'} and 
1352           $invoice_data{'finance_section'} eq $s->{description}) or
1353          $s->{'description'} =~ /^\d+ $/ ) {
1354       next;
1355     }
1356     push @summary_subtotals, $s;
1357   }
1358   $invoice_data{summary_subtotals} = \@summary_subtotals;
1359
1360   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1361   # the invoice variables
1362   return \%invoice_data if ( $params{'diag'} );
1363
1364   # All sections and items are built; now fill in templates.
1365   my @includelist = ();
1366   push @includelist, 'summary' if $summarypage;
1367   foreach my $include ( @includelist ) {
1368
1369     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1370     my @inc_src;
1371
1372     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1373
1374       @inc_src = $conf->config($inc_file, $agentnum);
1375
1376     } else {
1377
1378       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1379
1380       my $convert_map = $convert_maps{$format}{$include};
1381
1382       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1383                        s/--\@\]/$delimiters{$format}[1]/g;
1384                        $_;
1385                      } 
1386                  &$convert_map( $conf->config($inc_file, $agentnum) );
1387
1388     }
1389
1390     my $inc_tt = new Text::Template (
1391       TYPE       => 'ARRAY',
1392       SOURCE     => [ map "$_\n", @inc_src ],
1393       DELIMITERS => $delimiters{$format},
1394     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1395
1396     unless ( $inc_tt->compile() ) {
1397       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1398       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1399       die $error;
1400     }
1401
1402     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1403
1404     $invoice_data{$include} =~ s/\n+$//
1405       if ($format eq 'latex');
1406   }
1407
1408   $invoice_lines = 0;
1409   my $wasfunc = 0;
1410   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1411     /invoice_lines\((\d*)\)/;
1412     $invoice_lines += $1 || scalar(@buf);
1413     $wasfunc=1;
1414   }
1415   die "no invoice_lines() functions in template?"
1416     if ( $format eq 'template' && !$wasfunc );
1417
1418   if ($format eq 'template') {
1419
1420     if ( $invoice_lines ) {
1421       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1422       $invoice_data{'total_pages'}++
1423         if scalar(@buf) % $invoice_lines;
1424     }
1425
1426     #setup subroutine for the template
1427     $invoice_data{invoice_lines} = sub {
1428       my $lines = shift || scalar(@buf);
1429       map { 
1430         scalar(@buf)
1431           ? shift @buf
1432           : [ '', '' ];
1433       }
1434       ( 1 .. $lines );
1435     };
1436
1437     my $lines;
1438     my @collect;
1439     while (@buf) {
1440       push @collect, split("\n",
1441         $text_template->fill_in( HASH => \%invoice_data )
1442       );
1443       $invoice_data{'page'}++;
1444     }
1445     map "$_\n", @collect;
1446
1447   } else { # this is where we actually create the invoice
1448
1449     warn "filling in template for invoice ". $self->invnum. "\n"
1450       if $DEBUG;
1451     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1452       if $DEBUG > 1;
1453
1454     $text_template->fill_in(HASH => \%invoice_data);
1455   }
1456 }
1457
1458 sub notice_name { '('.shift->table.')'; }
1459
1460 sub template_conf { 'invoice_'; }
1461
1462 # helper routine for generating date ranges
1463 sub _prior_month30s {
1464   my $self = shift;
1465   my @ranges = (
1466    [ 1,       2592000 ], # 0-30 days ago
1467    [ 2592000, 5184000 ], # 30-60 days ago
1468    [ 5184000, 7776000 ], # 60-90 days ago
1469    [ 7776000, 0       ], # 90+   days ago
1470   );
1471
1472   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1473           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1474       ] }
1475   @ranges;
1476 }
1477
1478 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1479
1480 Returns an postscript invoice, as a scalar.
1481
1482 Options can be passed as a hashref (recommended) or as a list of time, template
1483 and then any key/value pairs for any other options.
1484
1485 I<time> an optional value used to control the printing of overdue messages.  The
1486 default is now.  It isn't the date of the invoice; that's the `_date' field.
1487 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1488 L<Time::Local> and L<Date::Parse> for conversion functions.
1489
1490 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1491
1492 =cut
1493
1494 sub print_ps {
1495   my $self = shift;
1496
1497   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1498   my $ps = generate_ps($file);
1499   unlink($logofile);
1500   unlink($barcodefile) if $barcodefile;
1501
1502   $ps;
1503 }
1504
1505 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1506
1507 Returns an PDF invoice, as a scalar.
1508
1509 Options can be passed as a hashref (recommended) or as a list of time, template
1510 and then any key/value pairs for any other options.
1511
1512 I<time> an optional value used to control the printing of overdue messages.  The
1513 default is now.  It isn't the date of the invoice; that's the `_date' field.
1514 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1515 L<Time::Local> and L<Date::Parse> for conversion functions.
1516
1517 I<template>, if specified, is the name of a suffix for alternate invoices.
1518
1519 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1520
1521 =cut
1522
1523 sub print_pdf {
1524   my $self = shift;
1525
1526   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1527   my $pdf = generate_pdf($file);
1528   unlink($logofile);
1529   unlink($barcodefile) if $barcodefile;
1530
1531   $pdf;
1532 }
1533
1534 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1535
1536 Returns an HTML invoice, as a scalar.
1537
1538 I<time> an optional value used to control the printing of overdue messages.  The
1539 default is now.  It isn't the date of the invoice; that's the `_date' field.
1540 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1541 L<Time::Local> and L<Date::Parse> for conversion functions.
1542
1543 I<template>, if specified, is the name of a suffix for alternate invoices.
1544
1545 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1546
1547 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1548 when emailing the invoice as part of a multipart/related MIME email.
1549
1550 =cut
1551
1552 sub print_html {
1553   my $self = shift;
1554   my %params;
1555   if ( ref($_[0]) ) {
1556     %params = %{ shift() }; 
1557   }else{
1558     $params{'time'} = shift;
1559     $params{'template'} = shift;
1560     $params{'cid'} = shift;
1561   }
1562
1563   $params{'format'} = 'html';
1564   
1565   $self->print_generic( %params );
1566 }
1567
1568 # quick subroutine for print_latex
1569 #
1570 # There are ten characters that LaTeX treats as special characters, which
1571 # means that they do not simply typeset themselves: 
1572 #      # $ % & ~ _ ^ \ { }
1573 #
1574 # TeX ignores blanks following an escaped character; if you want a blank (as
1575 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1576
1577 sub _latex_escape {
1578   my $value = shift;
1579   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1580   $value =~ s/([<>])/\$$1\$/g;
1581   $value;
1582 }
1583
1584 sub _html_escape {
1585   my $value = shift;
1586   encode_entities($value);
1587   $value;
1588 }
1589
1590 sub _html_escape_nbsp {
1591   my $value = _html_escape(shift);
1592   $value =~ s/ +/&nbsp;/g;
1593   $value;
1594 }
1595
1596 #utility methods for print_*
1597
1598 sub _translate_old_latex_format {
1599   warn "_translate_old_latex_format called\n"
1600     if $DEBUG; 
1601
1602   my @template = ();
1603   while ( @_ ) {
1604     my $line = shift;
1605   
1606     if ( $line =~ /^%%Detail\s*$/ ) {
1607   
1608       push @template, q![@--!,
1609                       q!  foreach my $_tr_line (@detail_items) {!,
1610                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1611                       q!      $_tr_line->{'description'} .= !, 
1612                       q!        "\\tabularnewline\n~~".!,
1613                       q!        join( "\\tabularnewline\n~~",!,
1614                       q!          @{$_tr_line->{'ext_description'}}!,
1615                       q!        );!,
1616                       q!    }!;
1617
1618       while ( ( my $line_item_line = shift )
1619               !~ /^%%EndDetail\s*$/                            ) {
1620         $line_item_line =~ s/'/\\'/g;    # nice LTS
1621         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1622         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1623         push @template, "    \$OUT .= '$line_item_line';";
1624       }
1625
1626       push @template, '}',
1627                       '--@]';
1628       #' doh, gvim
1629     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1630
1631       push @template, '[@--',
1632                       '  foreach my $_tr_line (@total_items) {';
1633
1634       while ( ( my $total_item_line = shift )
1635               !~ /^%%EndTotalDetails\s*$/                      ) {
1636         $total_item_line =~ s/'/\\'/g;    # nice LTS
1637         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1638         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1639         push @template, "    \$OUT .= '$total_item_line';";
1640       }
1641
1642       push @template, '}',
1643                       '--@]';
1644
1645     } else {
1646       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1647       push @template, $line;  
1648     }
1649   
1650   }
1651
1652   if ($DEBUG) {
1653     warn "$_\n" foreach @template;
1654   }
1655
1656   (@template);
1657 }
1658
1659 sub terms {
1660   my $self = shift;
1661   my $conf = $self->conf;
1662
1663   #check for an invoice-specific override
1664   return $self->invoice_terms if $self->invoice_terms;
1665   
1666   #check for a customer- specific override
1667   my $cust_main = $self->cust_main;
1668   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1669
1670   #use configured default
1671   $conf->config('invoice_default_terms') || '';
1672 }
1673
1674 sub due_date {
1675   my $self = shift;
1676   my $duedate = '';
1677   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1678     $duedate = $self->_date() + ( $1 * 86400 );
1679   }
1680   $duedate;
1681 }
1682
1683 sub due_date2str {
1684   my $self = shift;
1685   $self->due_date ? time2str(shift, $self->due_date) : '';
1686 }
1687
1688 sub balance_due_msg {
1689   my $self = shift;
1690   my $msg = $self->mt('Balance Due');
1691   return $msg unless $self->terms;
1692   if ( $self->due_date ) {
1693     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1694       $self->due_date2str($date_format);
1695   } elsif ( $self->terms ) {
1696     $msg .= ' - '. $self->terms;
1697   }
1698   $msg;
1699 }
1700
1701 sub balance_due_date {
1702   my $self = shift;
1703   my $conf = $self->conf;
1704   my $duedate = '';
1705   if (    $conf->exists('invoice_default_terms') 
1706        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1707     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1708   }
1709   $duedate;
1710 }
1711
1712 sub credit_balance_msg { 
1713   my $self = shift;
1714   $self->mt('Credit Balance Remaining')
1715 }
1716
1717 =item _date_pretty
1718
1719 Returns a string with the date, for example: "3/20/2008"
1720
1721 =cut
1722
1723 sub _date_pretty {
1724   my $self = shift;
1725   time2str($date_format, $self->_date);
1726 }
1727
1728 =item _items_sections OPTIONS
1729
1730 Generate section information for all items appearing on this invoice.
1731 This will only be called for multi-section invoices.
1732
1733 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
1734 related display records (L<FS::cust_bill_pkg_display>) and organize 
1735 them into two groups ("early" and "late" according to whether they come 
1736 before or after the total), then into sections.  A subtotal is calculated 
1737 for each section.
1738
1739 Section descriptions are returned in sort weight order.  Each consists 
1740 of a hash containing:
1741
1742 description: the package category name, escaped
1743 subtotal: the total charges in that section
1744 tax_section: a flag indicating that the section contains only tax charges
1745 summarized: same as tax_section, for some reason
1746 sort_weight: the package category's sort weight
1747
1748 If 'condense' is set on the display record, it also contains everything 
1749 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1750 coderefs to generate parts of the invoice.  This is not advised.
1751
1752 The method returns two arrayrefs, one of "early" sections and one of "late"
1753 sections.
1754
1755 OPTIONS may include:
1756
1757 by_location: a flag to divide the invoice into sections by location.  
1758 Each section hash will have a 'location' element containing a hashref of 
1759 the location fields (see L<FS::cust_location>).  The section description
1760 will be the location label, but the template can use any of the location 
1761 fields to create a suitable label.
1762
1763 by_category: a flag to divide the invoice into sections using display 
1764 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
1765 behavior.  Each section hash will have a 'category' element containing
1766 the section name from the display record (which probably equals the 
1767 category name of the package, but may not in some cases).
1768
1769 summary: a flag indicating that this is a summary-format invoice.
1770 Turning this on has the following effects:
1771 - Ignores display items with the 'summary' flag.
1772 - Places all sections in the "early" group even if they have post_total.
1773 - Creates sections for all non-disabled package categories, even if they 
1774 have no charges on this invoice, as well as a section with no name.
1775
1776 escape: an escape function to use for section titles.
1777
1778 extra_sections: an arrayref of additional sections to return after the 
1779 sorted list.  If there are any of these, section subtotals exclude 
1780 usage charges.
1781
1782 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
1783 passed through to C<_condense_section()>.
1784
1785 =cut
1786
1787 use vars qw(%pkg_category_cache);
1788 sub _items_sections {
1789   my $self = shift;
1790   my %opt = @_;
1791   
1792   my $escape = $opt{escape};
1793   my @extra_sections = @{ $opt{extra_sections} || [] };
1794
1795   # $subtotal{$locationnum}{$categoryname} = amount.
1796   # if we're not using by_location, $locationnum is undef.
1797   # if we're not using by_category, you guessed it, $categoryname is undef.
1798   # if we're not using either one, we shouldn't be here in the first place...
1799   my %subtotal = ();
1800   my %late_subtotal = ();
1801   my %not_tax = ();
1802
1803   # About tax items + multisection invoices:
1804   # If either invoice_*summary option is enabled, AND there is a 
1805   # package category with the name of the tax, then there will be 
1806   # a display record assigning the tax item to that category.
1807   #
1808   # However, the taxes are always placed in the "Taxes, Surcharges,
1809   # and Fees" section regardless of that.  The only effect of the 
1810   # display record is to create a subtotal for the summary page.
1811
1812   # cache these
1813   my $pkg_hash = $self->cust_pkg_hash;
1814
1815   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1816   {
1817
1818       my $usage = $cust_bill_pkg->usage;
1819
1820       my $locationnum;
1821       if ( $opt{by_location} ) {
1822         if ( $cust_bill_pkg->pkgnum ) {
1823           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1824         } else {
1825           $locationnum = '';
1826         }
1827       } else {
1828         $locationnum = undef;
1829       }
1830
1831       # as in _items_cust_pkg, if a line item has no display records,
1832       # cust_bill_pkg_display() returns a default record for it
1833
1834       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1835         next if ( $display->summary && $opt{summary} );
1836
1837         my $section = $display->section;
1838         my $type    = $display->type;
1839         $section = undef unless $opt{by_category};
1840
1841         $not_tax{$locationnum}{$section} = 1
1842           unless $cust_bill_pkg->pkgnum == 0;
1843
1844         # there's actually a very important piece of logic buried in here:
1845         # incrementing $late_subtotal{$section} CREATES 
1846         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
1847         # to define the list of late sections, and likewise keys(%subtotal).
1848         # When _items_cust_bill_pkg is called to generate line items for 
1849         # real, it will be called with 'section' => $section for each 
1850         # of these.
1851         if ( $display->post_total && !$opt{summary} ) {
1852           if (! $type || $type eq 'S') {
1853             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1854               if $cust_bill_pkg->setup != 0
1855               || $cust_bill_pkg->setup_show_zero;
1856           }
1857
1858           if (! $type) {
1859             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1860               if $cust_bill_pkg->recur != 0
1861               || $cust_bill_pkg->recur_show_zero;
1862           }
1863
1864           if ($type && $type eq 'R') {
1865             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1866               if $cust_bill_pkg->recur != 0
1867               || $cust_bill_pkg->recur_show_zero;
1868           }
1869           
1870           if ($type && $type eq 'U') {
1871             $late_subtotal{$locationnum}{$section} += $usage
1872               unless scalar(@extra_sections);
1873           }
1874
1875         } else { # it's a pre-total (normal) section
1876
1877           # skip tax items unless they're explicitly included in a section
1878           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1879
1880           if (! $type || $type eq 'S') {
1881             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1882               if $cust_bill_pkg->setup != 0
1883               || $cust_bill_pkg->setup_show_zero;
1884           }
1885
1886           if (! $type) {
1887             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1888               if $cust_bill_pkg->recur != 0
1889               || $cust_bill_pkg->recur_show_zero;
1890           }
1891
1892           if ($type && $type eq 'R') {
1893             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1894               if $cust_bill_pkg->recur != 0
1895               || $cust_bill_pkg->recur_show_zero;
1896           }
1897           
1898           if ($type && $type eq 'U') {
1899             $subtotal{$locationnum}{$section} += $usage
1900               unless scalar(@extra_sections);
1901           }
1902
1903         }
1904
1905       }
1906
1907   }
1908
1909   %pkg_category_cache = ();
1910
1911   # summary invoices need subtotals for all non-disabled package categories,
1912   # even if they're zero
1913   # but currently assume that there are no location sections, or at least
1914   # that the summary page doesn't care about them
1915   if ( $opt{summary} ) {
1916     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1917       $subtotal{''}{$category->categoryname} ||= 0;
1918     }
1919     $subtotal{''}{''} ||= 0;
1920   }
1921
1922   my @sections;
1923   foreach my $post_total (0,1) {
1924     my @these;
1925     my $s = $post_total ? \%late_subtotal : \%subtotal;
1926     foreach my $locationnum (keys %$s) {
1927       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1928         my $section = {
1929                         'subtotal'    => $s->{$locationnum}{$sectionname},
1930                         'post_total'  => $post_total,
1931                         'sort_weight' => 0,
1932                       };
1933         if ( $locationnum ) {
1934           $section->{'locationnum'} = $locationnum;
1935           my $location = FS::cust_location->by_key($locationnum);
1936           $section->{'description'} = &{ $escape }($location->location_label);
1937           # Better ideas? This will roughly group them by proximity, 
1938           # which alpha sorting on any of the address fields won't.
1939           # Sorting by locationnum is meaningless.
1940           # We have to sort on _something_ or the order may change 
1941           # randomly from one invoice to the next, which will confuse
1942           # people.
1943           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1944                                       $locationnum;
1945           $section->{'location'} = {
1946             map { $_ => &{ $escape }($location->get($_)) }
1947             $location->fields
1948           };
1949         } else {
1950           $section->{'category'} = $sectionname;
1951           $section->{'description'} = &{ $escape }($sectionname);
1952           if ( _pkg_category($_) ) {
1953             $section->{'sort_weight'} = _pkg_category($_)->weight;
1954             if ( _pkg_category($_)->condense ) {
1955               $section = { %$section, $self->_condense_section($opt{format}) };
1956             }
1957           }
1958         }
1959         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1960           # then it's a tax-only section
1961           $section->{'summarized'} = 'Y';
1962           $section->{'tax_section'} = 'Y';
1963         }
1964         push @these, $section;
1965       } # foreach $sectionname
1966     } #foreach $locationnum
1967     push @these, @extra_sections if $post_total == 0;
1968     # need an alpha sort for location sections, because postal codes can 
1969     # be non-numeric
1970     $sections[ $post_total ] = [ sort {
1971       $opt{'by_location'} ? 
1972         ($a->{sort_weight} cmp $b->{sort_weight}) :
1973         ($a->{sort_weight} <=> $b->{sort_weight})
1974       } @these ];
1975   } #foreach $post_total
1976
1977   return @sections; # early, late
1978 }
1979
1980 #helper subs for above
1981
1982 sub cust_pkg_hash {
1983   my $self = shift;
1984   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
1985 }
1986
1987 sub _pkg_category {
1988   my $categoryname = shift;
1989   $pkg_category_cache{$categoryname} ||=
1990     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1991 }
1992
1993 my %condensed_format = (
1994   'label' => [ qw( Description Qty Amount ) ],
1995   'fields' => [
1996                 sub { shift->{description} },
1997                 sub { shift->{quantity} },
1998                 sub { my($href, %opt) = @_;
1999                       ($opt{dollar} || ''). $href->{amount};
2000                     },
2001               ],
2002   'align'  => [ qw( l r r ) ],
2003   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2004   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2005 );
2006
2007 sub _condense_section {
2008   my ( $self, $format ) = ( shift, shift );
2009   ( 'condensed' => 1,
2010     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2011       qw( description_generator
2012           header_generator
2013           total_generator
2014           total_line_generator
2015         )
2016   );
2017 }
2018
2019 sub _condensed_generator_defaults {
2020   my ( $self, $format ) = ( shift, shift );
2021   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2022 }
2023
2024 my %html_align = (
2025   'c' => 'center',
2026   'l' => 'left',
2027   'r' => 'right',
2028 );
2029
2030 sub _condensed_header_generator {
2031   my ( $self, $format ) = ( shift, shift );
2032
2033   my ( $f, $prefix, $suffix, $separator, $column ) =
2034     _condensed_generator_defaults($format);
2035
2036   if ($format eq 'latex') {
2037     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2038     $suffix = "\\\\\n\\hline";
2039     $separator = "&\n";
2040     $column =
2041       sub { my ($d,$a,$s,$w) = @_;
2042             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2043           };
2044   } elsif ( $format eq 'html' ) {
2045     $prefix = '<th></th>';
2046     $suffix = '';
2047     $separator = '';
2048     $column =
2049       sub { my ($d,$a,$s,$w) = @_;
2050             return qq!<th align="$html_align{$a}">$d</th>!;
2051       };
2052   }
2053
2054   sub {
2055     my @args = @_;
2056     my @result = ();
2057
2058     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2059       push @result,
2060         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2061     }
2062
2063     $prefix. join($separator, @result). $suffix;
2064   };
2065
2066 }
2067
2068 sub _condensed_description_generator {
2069   my ( $self, $format ) = ( shift, shift );
2070
2071   my ( $f, $prefix, $suffix, $separator, $column ) =
2072     _condensed_generator_defaults($format);
2073
2074   my $money_char = '$';
2075   if ($format eq 'latex') {
2076     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2077     $suffix = '\\\\';
2078     $separator = " & \n";
2079     $column =
2080       sub { my ($d,$a,$s,$w) = @_;
2081             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2082           };
2083     $money_char = '\\dollar';
2084   }elsif ( $format eq 'html' ) {
2085     $prefix = '"><td align="center"></td>';
2086     $suffix = '';
2087     $separator = '';
2088     $column =
2089       sub { my ($d,$a,$s,$w) = @_;
2090             return qq!<td align="$html_align{$a}">$d</td>!;
2091       };
2092     #$money_char = $conf->config('money_char') || '$';
2093     $money_char = '';  # this is madness
2094   }
2095
2096   sub {
2097     #my @args = @_;
2098     my $href = shift;
2099     my @result = ();
2100
2101     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2102       my $dollar = '';
2103       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2104       push @result,
2105         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2106                     map { $f->{$_}->[$i] } qw(align span width)
2107                   );
2108     }
2109
2110     $prefix. join( $separator, @result ). $suffix;
2111   };
2112
2113 }
2114
2115 sub _condensed_total_generator {
2116   my ( $self, $format ) = ( shift, shift );
2117
2118   my ( $f, $prefix, $suffix, $separator, $column ) =
2119     _condensed_generator_defaults($format);
2120   my $style = '';
2121
2122   if ($format eq 'latex') {
2123     $prefix = "& ";
2124     $suffix = "\\\\\n";
2125     $separator = " & \n";
2126     $column =
2127       sub { my ($d,$a,$s,$w) = @_;
2128             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2129           };
2130   }elsif ( $format eq 'html' ) {
2131     $prefix = '';
2132     $suffix = '';
2133     $separator = '';
2134     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2135     $column =
2136       sub { my ($d,$a,$s,$w) = @_;
2137             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2138       };
2139   }
2140
2141
2142   sub {
2143     my @args = @_;
2144     my @result = ();
2145
2146     #  my $r = &{$f->{fields}->[$i]}(@args);
2147     #  $r .= ' Total' unless $i;
2148
2149     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2150       push @result,
2151         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2152                     map { $f->{$_}->[$i] } qw(align span width)
2153                   );
2154     }
2155
2156     $prefix. join( $separator, @result ). $suffix;
2157   };
2158
2159 }
2160
2161 =item total_line_generator FORMAT
2162
2163 Returns a coderef used for generation of invoice total line items for this
2164 usage_class.  FORMAT is either html or latex
2165
2166 =cut
2167
2168 # should not be used: will have issues with hash element names (description vs
2169 # total_item and amount vs total_amount -- another array of functions?
2170
2171 sub _condensed_total_line_generator {
2172   my ( $self, $format ) = ( shift, shift );
2173
2174   my ( $f, $prefix, $suffix, $separator, $column ) =
2175     _condensed_generator_defaults($format);
2176   my $style = '';
2177
2178   if ($format eq 'latex') {
2179     $prefix = "& ";
2180     $suffix = "\\\\\n";
2181     $separator = " & \n";
2182     $column =
2183       sub { my ($d,$a,$s,$w) = @_;
2184             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2185           };
2186   }elsif ( $format eq 'html' ) {
2187     $prefix = '';
2188     $suffix = '';
2189     $separator = '';
2190     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2191     $column =
2192       sub { my ($d,$a,$s,$w) = @_;
2193             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2194       };
2195   }
2196
2197
2198   sub {
2199     my @args = @_;
2200     my @result = ();
2201
2202     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2203       push @result,
2204         &{$column}( &{$f->{fields}->[$i]}(@args),
2205                     map { $f->{$_}->[$i] } qw(align span width)
2206                   );
2207     }
2208
2209     $prefix. join( $separator, @result ). $suffix;
2210   };
2211
2212 }
2213
2214 =item _items_pkg [ OPTIONS ]
2215
2216 Return line item hashes for each package item on this invoice. Nearly 
2217 equivalent to 
2218
2219 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2220
2221 The only OPTIONS accepted is 'section', which may point to a hashref 
2222 with a key named 'condensed', which may have a true value.  If it 
2223 does, this method tries to merge identical items into items with 
2224 'quantity' equal to the number of items (not the sum of their 
2225 separate quantities, for some reason).
2226
2227 =cut
2228
2229 sub _items_nontax {
2230   my $self = shift;
2231   grep { $_->pkgnum } $self->cust_bill_pkg;
2232 }
2233
2234 sub _items_pkg {
2235   my $self = shift;
2236   my %options = @_;
2237
2238   warn "$me _items_pkg searching for all package line items\n"
2239     if $DEBUG > 1;
2240
2241   my @cust_bill_pkg = $self->_items_nontax;
2242
2243   warn "$me _items_pkg filtering line items\n"
2244     if $DEBUG > 1;
2245   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2246
2247   if ($options{section} && $options{section}->{condensed}) {
2248
2249     warn "$me _items_pkg condensing section\n"
2250       if $DEBUG > 1;
2251
2252     my %itemshash = ();
2253     local $Storable::canonical = 1;
2254     foreach ( @items ) {
2255       my $item = { %$_ };
2256       delete $item->{ref};
2257       delete $item->{ext_description};
2258       my $key = freeze($item);
2259       $itemshash{$key} ||= 0;
2260       $itemshash{$key} ++; # += $item->{quantity};
2261     }
2262     @items = sort { $a->{description} cmp $b->{description} }
2263              map { my $i = thaw($_);
2264                    $i->{quantity} = $itemshash{$_};
2265                    $i->{amount} =
2266                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2267                    $i;
2268                  }
2269              keys %itemshash;
2270   }
2271
2272   warn "$me _items_pkg returning ". scalar(@items). " items\n"
2273     if $DEBUG > 1;
2274
2275   @items;
2276 }
2277
2278 sub _taxsort {
2279   return 0 unless $a->itemdesc cmp $b->itemdesc;
2280   return -1 if $b->itemdesc eq 'Tax';
2281   return 1 if $a->itemdesc eq 'Tax';
2282   return -1 if $b->itemdesc eq 'Other surcharges';
2283   return 1 if $a->itemdesc eq 'Other surcharges';
2284   $a->itemdesc cmp $b->itemdesc;
2285 }
2286
2287 sub _items_tax {
2288   my $self = shift;
2289   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2290   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2291
2292   if ( $self->conf->exists('always_show_tax') ) {
2293     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2294     if (0 == grep { $_->{description} eq $itemdesc } @items) {
2295       push @items,
2296         { 'description' => $itemdesc,
2297           'amount'      => 0.00 };
2298     }
2299   }
2300   @items;
2301 }
2302
2303 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2304
2305 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2306 list of hashrefs describing the line items they generate on the invoice.
2307
2308 OPTIONS may include:
2309
2310 format: the invoice format.
2311
2312 escape_function: the function used to escape strings.
2313
2314 DEPRECATED? (expensive, mostly unused?)
2315 format_function: the function used to format CDRs.
2316
2317 section: a hashref containing 'category' and/or 'locationnum'; if this 
2318 is present, only returns line items that belong to that category and/or
2319 location (whichever is defined).
2320
2321 multisection: a flag indicating that this is a multisection invoice,
2322 which does something complicated.
2323
2324 Returns a list of hashrefs, each of which may contain:
2325
2326 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
2327 ext_description, which is an arrayref of detail lines to show below 
2328 the package line.
2329
2330 =cut
2331
2332 sub _items_cust_bill_pkg {
2333   my $self = shift;
2334   my $conf = $self->conf;
2335   my $cust_bill_pkgs = shift;
2336   my %opt = @_;
2337
2338   my $format = $opt{format} || '';
2339   my $escape_function = $opt{escape_function} || sub { shift };
2340   my $format_function = $opt{format_function} || '';
2341   my $no_usage = $opt{no_usage} || '';
2342   my $unsquelched = $opt{unsquelched} || ''; #unused
2343   my ($section, $locationnum, $category);
2344   if ( $opt{section} ) {
2345     $category = $opt{section}->{category};
2346     $locationnum = $opt{section}->{locationnum};
2347   }
2348   my $summary_page = $opt{summary_page} || ''; #unused
2349   my $multisection = defined($category) || defined($locationnum);
2350   my $discount_show_always = 0;
2351
2352   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2353
2354   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2355                                    # and location labels
2356
2357   my @b = ();
2358   my ($s, $r, $u) = ( undef, undef, undef );
2359   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2360   {
2361
2362     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2363       if ( $_ && !$cust_bill_pkg->hidden ) {
2364         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2365         $_->{amount}      =~ s/^\-0\.00$/0.00/;
2366         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2367         push @b, { %$_ }
2368           if $_->{amount} != 0
2369           || $discount_show_always
2370           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2371           || (   $_->{_is_setup} && $_->{setup_show_zero} )
2372         ;
2373         $_ = undef;
2374       }
2375     }
2376
2377     if ( $locationnum ) {
2378       # this is a location section; skip packages that aren't at this
2379       # service location.
2380       next if $cust_bill_pkg->pkgnum == 0;
2381       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
2382               != $locationnum;
2383     }
2384
2385     # Consider display records for this item to determine if it belongs
2386     # in this section.  Note that if there are no display records, there
2387     # will be a default pseudo-record that includes all charge types 
2388     # and has no section name.
2389     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2390                                   ? $cust_bill_pkg->cust_bill_pkg_display
2391                                   : ( $cust_bill_pkg );
2392
2393     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2394          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2395       if $DEBUG > 1;
2396
2397     if ( defined($category) ) {
2398       # then this is a package category section; process all display records
2399       # that belong to this section.
2400       @cust_bill_pkg_display = grep { $_->section eq $category }
2401                                 @cust_bill_pkg_display;
2402     } else {
2403       # otherwise, process all display records that aren't usage summaries
2404       # (I don't think there should be usage summaries if you aren't using 
2405       # category sections, but this is the historical behavior)
2406       @cust_bill_pkg_display = grep { !$_->summary }
2407                                 @cust_bill_pkg_display;
2408     }
2409     foreach my $display (@cust_bill_pkg_display) {
2410
2411       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2412            $display->billpkgdisplaynum. "\n"
2413         if $DEBUG > 1;
2414
2415       my $type = $display->type;
2416
2417       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2418       $desc = substr($desc, 0, $maxlength). '...'
2419         if $format eq 'latex' && length($desc) > $maxlength;
2420
2421       my %details_opt = ( 'format'          => $format,
2422                           'escape_function' => $escape_function,
2423                           'format_function' => $format_function,
2424                           'no_usage'        => $opt{'no_usage'},
2425                         );
2426
2427       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2428
2429         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2430           if $DEBUG > 1;
2431
2432         if ( $cust_bill_pkg->setup != 0 ) {
2433           my $description = $desc;
2434           $description .= ' Setup'
2435             if $cust_bill_pkg->recur != 0
2436             || $discount_show_always
2437             || $cust_bill_pkg->recur_show_zero;
2438           push @b, {
2439             'description' => $description,
2440             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2441           };
2442         }
2443         if ( $cust_bill_pkg->recur != 0 ) {
2444           push @b, {
2445             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2446             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2447           };
2448         }
2449
2450       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2451
2452         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2453           if $DEBUG > 1;
2454  
2455         my $cust_pkg = $cust_bill_pkg->cust_pkg;
2456
2457         # which pkgpart to show for display purposes?
2458         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2459
2460         # start/end dates for invoice formats that do nonstandard 
2461         # things with them
2462         my %item_dates = ();
2463         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2464           unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2465
2466         if (    (!$type || $type eq 'S')
2467              && (    $cust_bill_pkg->setup != 0
2468                   || $cust_bill_pkg->setup_show_zero
2469                 )
2470            )
2471          {
2472
2473           warn "$me _items_cust_bill_pkg adding setup\n"
2474             if $DEBUG > 1;
2475
2476           my $description = $desc;
2477           $description .= ' Setup'
2478             if $cust_bill_pkg->recur != 0
2479             || $discount_show_always
2480             || $cust_bill_pkg->recur_show_zero;
2481
2482           my @d = ();
2483           my $svc_label;
2484           unless ( $cust_pkg->part_pkg->hide_svc_detail
2485                 || $cust_bill_pkg->hidden )
2486           {
2487
2488             my @svc_labels = map &{$escape_function}($_),
2489                         $cust_pkg->h_labels_short($self->_date, undef, 'I');
2490             push @d, @svc_labels
2491               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2492             $svc_label = $svc_labels[0];
2493
2494             my $lnum = $cust_main ? $cust_main->ship_locationnum
2495                                   : $self->prospect_main->locationnum;
2496             # show the location label if it's not the customer's default
2497             # location, and we're not grouping items by location already
2498             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2499               my $loc = $cust_pkg->location_label;
2500               $loc = substr($loc, 0, $maxlength). '...'
2501                 if $format eq 'latex' && length($loc) > $maxlength;
2502               push @d, &{$escape_function}($loc);
2503             }
2504
2505           } #unless hiding service details
2506
2507           push @d, $cust_bill_pkg->details(%details_opt)
2508             if $cust_bill_pkg->recur == 0;
2509
2510           if ( $cust_bill_pkg->hidden ) {
2511             $s->{amount}      += $cust_bill_pkg->setup;
2512             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2513             push @{ $s->{ext_description} }, @d;
2514           } else {
2515             $s = {
2516               _is_setup       => 1,
2517               description     => $description,
2518               pkgpart         => $pkgpart,
2519               pkgnum          => $cust_bill_pkg->pkgnum,
2520               amount          => $cust_bill_pkg->setup,
2521               setup_show_zero => $cust_bill_pkg->setup_show_zero,
2522               unit_amount     => $cust_bill_pkg->unitsetup,
2523               quantity        => $cust_bill_pkg->quantity,
2524               ext_description => \@d,
2525               svc_label       => ($svc_label || ''),
2526             };
2527           };
2528
2529         }
2530
2531         if (    ( !$type || $type eq 'R' || $type eq 'U' )
2532              && (
2533                      $cust_bill_pkg->recur != 0
2534                   || $cust_bill_pkg->setup == 0
2535                   || $discount_show_always
2536                   || $cust_bill_pkg->recur_show_zero
2537                 )
2538            )
2539         {
2540
2541           warn "$me _items_cust_bill_pkg adding recur/usage\n"
2542             if $DEBUG > 1;
2543
2544           my $is_summary = $display->summary;
2545           my $description = ($is_summary && $type && $type eq 'U')
2546                             ? "Usage charges" : $desc;
2547
2548           my $part_pkg = $cust_pkg->part_pkg;
2549
2550           #pry be a bit more efficient to look some of this conf stuff up
2551           # outside the loop
2552           unless (
2553             $conf->exists('disable_line_item_date_ranges')
2554               || $part_pkg->option('disable_line_item_date_ranges',1)
2555               || ! $cust_bill_pkg->sdate
2556               || ! $cust_bill_pkg->edate
2557           ) {
2558             my $time_period;
2559             my $date_style = '';
2560             $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2561                                          $self->agentnum
2562                                        )
2563               if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2564             $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2565                                             $self->agentnum
2566                                          );
2567             if ( defined($date_style) && $date_style eq 'month_of' ) {
2568               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2569             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2570               my $desc = $conf->config( 'cust_bill-line_item-date_description',
2571                                          $self->agentnum
2572                                       );
2573               $desc .= ' ' unless $desc =~ /\s$/;
2574               $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2575             } else {
2576               $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
2577                            " - ". time2str($date_format, $cust_bill_pkg->edate);
2578             }
2579             $description .= " ($time_period)";
2580           }
2581
2582           my @d = ();
2583           my @seconds = (); # for display of usage info
2584           my $svc_label = '';
2585
2586           #at least until cust_bill_pkg has "past" ranges in addition to
2587           #the "future" sdate/edate ones... see #3032
2588           my @dates = ( $self->_date );
2589           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2590           push @dates, $prev->sdate if $prev;
2591           push @dates, undef if !$prev;
2592
2593           unless ( $part_pkg->hide_svc_detail
2594                 || $cust_bill_pkg->itemdesc
2595                 || $cust_bill_pkg->hidden
2596                 || $is_summary && $type && $type eq 'U'
2597               )
2598           {
2599
2600             warn "$me _items_cust_bill_pkg adding service details\n"
2601               if $DEBUG > 1;
2602
2603             my @svc_labels = map &{$escape_function}($_),
2604                         $cust_pkg->h_labels_short(@dates, 'I');
2605             push @d, @svc_labels
2606               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2607             $svc_label = $svc_labels[0];
2608
2609             warn "$me _items_cust_bill_pkg done adding service details\n"
2610               if $DEBUG > 1;
2611
2612             my $lnum = $cust_main ? $cust_main->ship_locationnum
2613                                   : $self->prospect_main->locationnum;
2614             # show the location label if it's not the customer's default
2615             # location, and we're not grouping items by location already
2616             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2617               my $loc = $cust_pkg->location_label;
2618               $loc = substr($loc, 0, $maxlength). '...'
2619                 if $format eq 'latex' && length($loc) > $maxlength;
2620               push @d, &{$escape_function}($loc);
2621             }
2622
2623             # Display of seconds_since_sqlradacct:
2624             # On the invoice, when processing @detail_items, look for a field
2625             # named 'seconds'.  This will contain total seconds for each 
2626             # service, in the same order as @ext_description.  For services 
2627             # that don't support this it will show undef.
2628             if ( $conf->exists('svc_acct-usage_seconds') 
2629                  and ! $cust_bill_pkg->pkgpart_override ) {
2630               foreach my $cust_svc ( 
2631                   $cust_pkg->h_cust_svc(@dates, 'I') 
2632                 ) {
2633
2634                 # eval because not having any part_export_usage exports 
2635                 # is a fatal error, last_bill/_date because that's how 
2636                 # sqlradius_hour billing does it
2637                 my $sec = eval {
2638                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2639                 };
2640                 push @seconds, $sec;
2641               }
2642             } #if svc_acct-usage_seconds
2643
2644           }
2645
2646           unless ( $is_summary ) {
2647             warn "$me _items_cust_bill_pkg adding details\n"
2648               if $DEBUG > 1;
2649
2650             #instead of omitting details entirely in this case (unwanted side
2651             # effects), just omit CDRs
2652             $details_opt{'no_usage'} = 1
2653               if $type && $type eq 'R';
2654
2655             push @d, $cust_bill_pkg->details(%details_opt);
2656           }
2657
2658           warn "$me _items_cust_bill_pkg calculating amount\n"
2659             if $DEBUG > 1;
2660   
2661           my $amount = 0;
2662           if (!$type) {
2663             $amount = $cust_bill_pkg->recur;
2664           } elsif ($type eq 'R') {
2665             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2666           } elsif ($type eq 'U') {
2667             $amount = $cust_bill_pkg->usage;
2668           }
2669   
2670           my $unit_amount =
2671             ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2672                                               : $amount;
2673
2674           if ( !$type || $type eq 'R' ) {
2675
2676             warn "$me _items_cust_bill_pkg adding recur\n"
2677               if $DEBUG > 1;
2678
2679             if ( $cust_bill_pkg->hidden ) {
2680               $r->{amount}      += $amount;
2681               $r->{unit_amount} += $unit_amount;
2682               push @{ $r->{ext_description} }, @d;
2683             } else {
2684               $r = {
2685                 description     => $description,
2686                 pkgpart         => $pkgpart,
2687                 pkgnum          => $cust_bill_pkg->pkgnum,
2688                 amount          => $amount,
2689                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2690                 unit_amount     => $unit_amount,
2691                 quantity        => $cust_bill_pkg->quantity,
2692                 %item_dates,
2693                 ext_description => \@d,
2694                 svc_label       => ($svc_label || ''),
2695               };
2696               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2697             }
2698
2699           } else {  # $type eq 'U'
2700
2701             warn "$me _items_cust_bill_pkg adding usage\n"
2702               if $DEBUG > 1;
2703
2704             if ( $cust_bill_pkg->hidden ) {
2705               $u->{amount}      += $amount;
2706               $u->{unit_amount} += $unit_amount,
2707               push @{ $u->{ext_description} }, @d;
2708             } else {
2709               $u = {
2710                 description     => $description,
2711                 pkgpart         => $pkgpart,
2712                 pkgnum          => $cust_bill_pkg->pkgnum,
2713                 amount          => $amount,
2714                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2715                 unit_amount     => $unit_amount,
2716                 quantity        => $cust_bill_pkg->quantity,
2717                 %item_dates,
2718                 ext_description => \@d,
2719               };
2720             }
2721           }
2722
2723         } # recurring or usage with recurring charge
2724
2725       } else { #pkgnum tax or one-shot line item (??)
2726
2727         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2728           if $DEBUG > 1;
2729
2730         if ( $cust_bill_pkg->setup != 0 ) {
2731           push @b, {
2732             'description' => $desc,
2733             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2734           };
2735         }
2736         if ( $cust_bill_pkg->recur != 0 ) {
2737           push @b, {
2738             'description' => "$desc (".
2739                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2740                              time2str($date_format, $cust_bill_pkg->edate). ')',
2741             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2742           };
2743         }
2744
2745       }
2746
2747     }
2748
2749     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2750                                 && $conf->exists('discount-show-always'));
2751
2752   }
2753
2754   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2755     if ( $_  ) {
2756       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2757       $_->{amount}      =~ s/^\-0\.00$/0.00/;
2758       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2759       push @b, { %$_ }
2760         if $_->{amount} != 0
2761         || $discount_show_always
2762         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2763         || (   $_->{_is_setup} && $_->{setup_show_zero} )
2764     }
2765   }
2766
2767   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2768     if $DEBUG > 1;
2769
2770   @b;
2771
2772 }
2773
2774 =item _items_discounts_avail
2775
2776 Returns an array of line item hashrefs representing available term discounts
2777 for this invoice.  This makes the same assumptions that apply to term 
2778 discounts in general: that the package is billed monthly, at a flat rate, 
2779 with no usage charges.  A prorated first month will be handled, as will 
2780 a setup fee if the discount is allowed to apply to setup fees.
2781
2782 =cut
2783
2784 sub _items_discounts_avail {
2785   my $self = shift;
2786
2787   #maybe move this method from cust_bill when quotations support discount_plans 
2788   return () unless $self->can('discount_plans');
2789   my %plans = $self->discount_plans;
2790
2791   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2792   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2793
2794   map {
2795     my $months = $_;
2796     my $plan = $plans{$months};
2797
2798     my $term_total = sprintf('%.2f', $plan->discounted_total);
2799     my $percent = sprintf('%.0f', 
2800                           100 * (1 - $term_total / $plan->base_total) );
2801     my $permonth = sprintf('%.2f', $term_total / $months);
2802     my $detail = $self->mt('discount on item'). ' '.
2803                  join(', ', map { "#$_" } $plan->pkgnums)
2804       if $list_pkgnums;
2805
2806     # discounts for non-integer months don't work anyway
2807     $months = sprintf("%d", $months);
2808
2809     +{
2810       description => $self->mt('Save [_1]% by paying for [_2] months',
2811                                 $percent, $months),
2812       amount      => $self->mt('[_1] ([_2] per month)', 
2813                                 $term_total, $money_char.$permonth),
2814       ext_description => ($detail || ''),
2815     }
2816   } #map
2817   sort { $b <=> $a } keys %plans;
2818
2819 }
2820
2821 1;