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