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