minor convenience for invoice templates, #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                       'tax_section' => 1,
759                     };
760   my $tax_weight = _pkg_category($tax_section->{description})
761                         ? _pkg_category($tax_section->{description})->weight
762                         : 0;
763   $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
764   $tax_section->{'sort_weight'} = $tax_weight;
765
766   my $adjusttotal = 0;
767   my $adjust_section = {
768     'description'    => $self->mt('Credits, Payments, and Adjustments'),
769     'adjust_section' => 1,
770     'subtotal'       => 0,   # adjusted below
771   };
772   my $adjust_weight = _pkg_category($adjust_section->{description})
773                         ? _pkg_category($adjust_section->{description})->weight
774                         : 0;
775   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
776   $adjust_section->{'sort_weight'} = $adjust_weight;
777
778   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
779   my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
780                      $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
781   $invoice_data{'multisection'} = $multisection;
782   my $late_sections;
783   my $extra_sections = [];
784   my $extra_lines = ();
785
786   # default section ('Charges')
787   my $default_section = { 'description' => '',
788                           'subtotal'    => '', 
789                           'no_subtotal' => 1,
790                         };
791
792   # Previous Charges section
793   # subtotal is the first return value from $self->previous
794   my $previous_section;
795   # if the invoice has major sections, or if we're summarizing previous 
796   # charges with a single line, or if we've been specifically told to put them
797   # in a section, create a section for previous charges:
798   if ( $multisection or
799        $conf->exists('previous_balance-summary_only') or
800        $conf->exists('previous_balance-section') ) {
801     
802     $previous_section =  { 'description' => $self->mt('Previous Charges'),
803                            'subtotal'    => $other_money_char.
804                                             sprintf('%.2f', $pr_total),
805                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
806                          };
807     $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
808       join(' / ', map { $cust_main->balance_date_range(@$_) }
809                   $self->_prior_month30s
810           )
811       if $conf->exists('invoice_include_aging');
812
813   } else {
814     # otherwise put them in the main section
815     $previous_section = $default_section;
816   }
817
818   if ( $multisection ) {
819     ($extra_sections, $extra_lines) =
820       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
821       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
822       && $self->can('_items_extra_usage_sections');
823
824     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
825
826     push @detail_items, @$extra_lines if $extra_lines;
827
828     # the code is written so that both methods can be used together, but
829     # we haven't yet changed the template to take advantage of that, so for 
830     # now, treat them as mutually exclusive.
831     my %section_method = ( by_category => 1 );
832     if ( $conf->exists($tc.'sections_by_location') ) {
833       %section_method = ( by_location => 1 );
834     }
835     my ($early, $late) =
836       $self->_items_sections( 'summary' => $summarypage,
837                               'escape'  => $escape_function_nonbsp,
838                               'extra_sections' => $extra_sections,
839                               'format'  => $format,
840                               %section_method
841                             );
842     push @sections, @$early;
843     $late_sections = $late;
844
845     if (    $conf->exists('svc_phone_sections')
846          && $self->can('_items_svc_phone_sections')
847        )
848     {
849       my ($phone_sections, $phone_lines) =
850         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
851       push @{$late_sections}, @$phone_sections;
852       push @detail_items, @$phone_lines;
853     }
854     if ( $conf->exists('voip-cust_accountcode_cdr')
855          && $cust_main->accountcode_cdr
856          && $self->can('_items_accountcode_cdr')
857        )
858     {
859       my ($accountcode_section, $accountcode_lines) =
860         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
861       if ( scalar(@$accountcode_lines) ) {
862           push @{$late_sections}, $accountcode_section;
863           push @detail_items, @$accountcode_lines;
864       }
865     }
866   } else {# not multisection
867     # make a default section
868     push @sections, $default_section;
869     # and calculate the finance charge total, since it won't get done otherwise.
870     # and the default section total
871     # XXX possibly finance_pkgclass should not be used in this manner?
872     my @finance_charges;
873     my @charges;
874     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
875       if ( $invoice_data{finance_section} and 
876         grep { $_->section eq $invoice_data{finance_section} }
877            $cust_bill_pkg->cust_bill_pkg_display ) {
878         # I think these are always setup fees, but just to be sure...
879         push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
880       } else {
881         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
882       }
883     }
884     $invoice_data{finance_amount} = 
885       sprintf('%.2f', sum( @finance_charges ) || 0);
886     $default_section->{subtotal} = $other_money_char.
887                                     sprintf('%.2f', sum( @charges ) || 0);
888   }
889
890   # previous invoice balances in the Previous Charges section if there
891   # is one, otherwise in the main detail section
892   # (except if summary_only is enabled, don't show them at all)
893   if ( $self->can('_items_previous') &&
894        $self->enable_previous &&
895        ! $conf->exists('previous_balance-summary_only') ) {
896
897     warn "$me adding previous balances\n"
898       if $DEBUG > 1;
899
900     foreach my $line_item ( $self->_items_previous ) {
901
902       my $detail = {
903         ref             => $line_item->{'pkgnum'},
904         pkgpart         => $line_item->{'pkgpart'},
905         quantity        => 1,
906         section         => $previous_section, # which might be $default_section
907         description     => &$escape_function($line_item->{'description'}),
908         ext_description => [ map { &$escape_function($_) } 
909                              @{ $line_item->{'ext_description'} || [] }
910                            ],
911         amount          => ( $old_latex ? '' : $money_char).
912                             $line_item->{'amount'},
913         product_code    => $line_item->{'pkgpart'} || 'N/A',
914       };
915
916       push @detail_items, $detail;
917       push @buf, [ $detail->{'description'},
918                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
919                  ];
920     }
921
922   }
923
924   if ( @pr_cust_bill && $self->enable_previous ) {
925     push @buf, ['','-----------'];
926     push @buf, [ $self->mt('Total Previous Balance'),
927                  $money_char. sprintf("%10.2f", $pr_total) ];
928     push @buf, ['',''];
929   }
930  
931   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
932       warn "$me adding DID summary\n"
933         if $DEBUG > 1;
934
935       my ($didsummary,$minutes) = $self->_did_summary;
936       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
937       push @detail_items, 
938        { 'description' => $didsummary_desc,
939            'ext_description' => [ $didsummary, $minutes ],
940        };
941   }
942
943   foreach my $section (@sections, @$late_sections) {
944
945     warn "$me adding section \n". Dumper($section)
946       if $DEBUG > 1;
947
948     # begin some normalization
949     $section->{'subtotal'} = $section->{'amount'}
950       if $multisection
951          && !exists($section->{subtotal})
952          && exists($section->{amount});
953
954     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
955       if ( $invoice_data{finance_section} &&
956            $section->{'description'} eq $invoice_data{finance_section} );
957
958     $section->{'subtotal'} = $other_money_char.
959                              sprintf('%.2f', $section->{'subtotal'})
960       if $multisection;
961
962     # continue some normalization
963     $section->{'amount'}   = $section->{'subtotal'}
964       if $multisection;
965
966
967     if ( $section->{'description'} ) {
968       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
969                    [ '', '' ],
970                  );
971     }
972
973     warn "$me   setting options\n"
974       if $DEBUG > 1;
975
976     my %options = ();
977     $options{'section'} = $section if $multisection;
978     $options{'format'} = $format;
979     $options{'escape_function'} = $escape_function;
980     $options{'no_usage'} = 1 unless $unsquelched;
981     $options{'unsquelched'} = $unsquelched;
982     $options{'summary_page'} = $summarypage;
983     $options{'skip_usage'} =
984       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
985
986     warn "$me   searching for line items\n"
987       if $DEBUG > 1;
988
989     foreach my $line_item ( $self->_items_pkg(%options) ) {
990
991       warn "$me     adding line item $line_item\n"
992         if $DEBUG > 1;
993
994       my $detail = {
995         ext_description => [],
996       };
997       $detail->{'ref'} = $line_item->{'pkgnum'};
998       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
999       $detail->{'quantity'} = $line_item->{'quantity'};
1000       $detail->{'section'} = $section;
1001       $detail->{'description'} = &$escape_function($line_item->{'description'});
1002       if ( exists $line_item->{'ext_description'} ) {
1003         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1004       }
1005       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1006                               $line_item->{'amount'};
1007       if ( exists $line_item->{'unit_amount'} ) {
1008         $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1009                                    $line_item->{'unit_amount'};
1010       }
1011       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1012
1013       $detail->{'sdate'} = $line_item->{'sdate'};
1014       $detail->{'edate'} = $line_item->{'edate'};
1015       $detail->{'seconds'} = $line_item->{'seconds'};
1016       $detail->{'svc_label'} = $line_item->{'svc_label'};
1017   
1018       push @detail_items, $detail;
1019       push @buf, ( [ $detail->{'description'},
1020                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1021                    ],
1022                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1023                  );
1024     }
1025
1026     if ( $section->{'description'} ) {
1027       push @buf, ( ['','-----------'],
1028                    [ $section->{'description'}. ' sub-total',
1029                       $section->{'subtotal'} # already formatted this 
1030                    ],
1031                    [ '', '' ],
1032                    [ '', '' ],
1033                  );
1034     }
1035   
1036   }
1037
1038   $invoice_data{current_less_finance} =
1039     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1040
1041   # if there's anything in the Previous Charges section, prepend it to the list
1042   if ( $pr_total and $previous_section ne $default_section ) {
1043     unshift @sections, $previous_section;
1044   }
1045
1046   warn "$me adding taxes\n"
1047     if $DEBUG > 1;
1048
1049   my @items_tax = $self->_items_tax;
1050   foreach my $tax ( @items_tax ) {
1051
1052     $taxtotal += $tax->{'amount'};
1053
1054     my $description = &$escape_function( $tax->{'description'} );
1055     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
1056
1057     if ( $multisection ) {
1058
1059       my $money = $old_latex ? '' : $money_char;
1060       push @detail_items, {
1061         ext_description => [],
1062         ref          => '',
1063         quantity     => '',
1064         description  => $description,
1065         amount       => $money. $amount,
1066         product_code => '',
1067         section      => $tax_section,
1068       };
1069
1070     } else {
1071
1072       push @total_items, {
1073         'total_item'   => $description,
1074         'total_amount' => $other_money_char. $amount,
1075       };
1076
1077     }
1078
1079     push @buf,[ $description,
1080                 $money_char. $amount,
1081               ];
1082
1083   }
1084   
1085   if ( @items_tax ) {
1086     my $total = {};
1087     $total->{'total_item'} = $self->mt('Sub-total');
1088     $total->{'total_amount'} =
1089       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1090
1091     if ( $multisection ) {
1092       $tax_section->{'subtotal'} = $other_money_char.
1093                                    sprintf('%.2f', $taxtotal);
1094       $tax_section->{'pretotal'} = 'New charges sub-total '.
1095                                    $total->{'total_amount'};
1096       push @sections, $tax_section if $taxtotal;
1097     }else{
1098       unshift @total_items, $total;
1099     }
1100   }
1101   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1102
1103   push @buf,['','-----------'];
1104   push @buf,[$self->mt( 
1105               (!$self->enable_previous)
1106                ? 'Total Charges'
1107                : 'Total New Charges'
1108              ),
1109              $money_char. sprintf("%10.2f",$self->charged) ];
1110   push @buf,['',''];
1111
1112
1113   ###
1114   # Totals
1115   ###
1116
1117   my %embolden_functions = (
1118     'latex'    => sub { return '\textbf{'. shift(). '}' },
1119     'html'     => sub { return '<b>'. shift(). '</b>' },
1120     'template' => sub { shift },
1121   );
1122   my $embolden_function = $embolden_functions{$format};
1123
1124   if ( $self->can('_items_total') ) { # quotations
1125
1126     $self->_items_total(\@total_items);
1127
1128     foreach ( @total_items ) {
1129       $_->{'total_item'}   = &$embolden_function( $_->{'total_item'} );
1130       $_->{'total_amount'} = &$embolden_function( $other_money_char.
1131                                                    $_->{'total_amount'}
1132                                                 );
1133     }
1134
1135   } else { #normal invoice case
1136
1137     # calculate total, possibly including total owed on previous
1138     # invoices
1139     my $total = {};
1140     my $item = 'Total';
1141     $item = $conf->config('previous_balance-exclude_from_total')
1142          || 'Total New Charges'
1143       if $conf->exists('previous_balance-exclude_from_total');
1144     my $amount = $self->charged;
1145     if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1146       $amount += $pr_total;
1147     }
1148
1149     $total->{'total_item'} = &$embolden_function($self->mt($item));
1150     $total->{'total_amount'} =
1151       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
1152     if ( $multisection ) {
1153       if ( $adjust_section->{'sort_weight'} ) {
1154         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1155           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
1156       } else {
1157         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1158           $other_money_char.  sprintf('%.2f', $self->charged );
1159       } 
1160     } else {
1161       push @total_items, $total;
1162     }
1163     push @buf,['','-----------'];
1164     push @buf,[$item,
1165                $money_char.
1166                sprintf( '%10.2f', $amount )
1167               ];
1168     push @buf,['',''];
1169
1170     # if we're showing previous invoices, also show previous
1171     # credits and payments 
1172     if ( $self->enable_previous 
1173           and $self->can('_items_credits')
1174           and $self->can('_items_payments') )
1175       {
1176       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1177     
1178       # credits
1179       my $credittotal = 0;
1180       foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1181
1182         my $total;
1183         $total->{'total_item'} = &$escape_function($credit->{'description'});
1184         $credittotal += $credit->{'amount'};
1185         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1186         $adjusttotal += $credit->{'amount'};
1187         if ( $multisection ) {
1188           my $money = $old_latex ? '' : $money_char;
1189           push @detail_items, {
1190             ext_description => [],
1191             ref          => '',
1192             quantity     => '',
1193             description  => &$escape_function($credit->{'description'}),
1194             amount       => $money. $credit->{'amount'},
1195             product_code => '',
1196             section      => $adjust_section,
1197           };
1198         } else {
1199           push @total_items, $total;
1200         }
1201
1202       }
1203       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1204
1205       #credits (again)
1206       foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1207         push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1208       }
1209
1210       # payments
1211       my $paymenttotal = 0;
1212       foreach my $payment ( $self->_items_payments ) {
1213         my $total = {};
1214         $total->{'total_item'} = &$escape_function($payment->{'description'});
1215         $paymenttotal += $payment->{'amount'};
1216         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1217         $adjusttotal += $payment->{'amount'};
1218         if ( $multisection ) {
1219           my $money = $old_latex ? '' : $money_char;
1220           push @detail_items, {
1221             ext_description => [],
1222             ref          => '',
1223             quantity     => '',
1224             description  => &$escape_function($payment->{'description'}),
1225             amount       => $money. $payment->{'amount'},
1226             product_code => '',
1227             section      => $adjust_section,
1228           };
1229         }else{
1230           push @total_items, $total;
1231         }
1232         push @buf, [ $payment->{'description'},
1233                      $money_char. sprintf("%10.2f", $payment->{'amount'}),
1234                    ];
1235       }
1236       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1237     
1238       if ( $multisection ) {
1239         $adjust_section->{'subtotal'} = $other_money_char.
1240                                         sprintf('%.2f', $adjusttotal);
1241         push @sections, $adjust_section
1242           unless $adjust_section->{sort_weight};
1243       }
1244
1245       # create Balance Due message
1246       { 
1247         my $total;
1248         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1249         $total->{'total_amount'} =
1250           &$embolden_function(
1251             $other_money_char. sprintf('%.2f', #why? $summarypage 
1252                                                #  ? $self->charged +
1253                                                #    $self->billing_balance
1254                                                #  :
1255                                                    $self->owed + $pr_total
1256                                       )
1257           );
1258         if ( $multisection && !$adjust_section->{sort_weight} ) {
1259           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1260                                            $total->{'total_amount'};
1261         }else{
1262           push @total_items, $total;
1263         }
1264         push @buf,['','-----------'];
1265         push @buf,[$self->balance_due_msg, $money_char. 
1266           sprintf("%10.2f", $balance_due ) ];
1267       }
1268
1269       if ( $conf->exists('previous_balance-show_credit')
1270           and $cust_main->balance < 0 ) {
1271         my $credit_total = {
1272           'total_item'    => &$embolden_function($self->credit_balance_msg),
1273           'total_amount'  => &$embolden_function(
1274             $other_money_char. sprintf('%.2f', -$cust_main->balance)
1275           ),
1276         };
1277         if ( $multisection ) {
1278           $adjust_section->{'posttotal'} .= $newline_token .
1279             $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1280         }
1281         else {
1282           push @total_items, $credit_total;
1283         }
1284         push @buf,['','-----------'];
1285         push @buf,[$self->credit_balance_msg, $money_char. 
1286           sprintf("%10.2f", -$cust_main->balance ) ];
1287       }
1288     }
1289
1290   } #end of default total adding ! can('_items_total')
1291
1292   if ( $multisection ) {
1293     if (    $conf->exists('svc_phone_sections')
1294          && $self->can('_items_svc_phone_sections')
1295        )
1296     {
1297       my $total;
1298       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1299       $total->{'total_amount'} =
1300         &$embolden_function(
1301           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1302         );
1303       my $last_section = pop @sections;
1304       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1305                                      $total->{'total_amount'};
1306       push @sections, $last_section;
1307     }
1308     push @sections, @$late_sections
1309       if $unsquelched;
1310   }
1311
1312   # make a discounts-available section, even without multisection
1313   if ( $conf->exists('discount-show_available') 
1314        and my @discounts_avail = $self->_items_discounts_avail ) {
1315     my $discount_section = {
1316       'description' => $self->mt('Discounts Available'),
1317       'subtotal'    => '',
1318       'no_subtotal' => 1,
1319     };
1320
1321     push @sections, $discount_section;
1322     push @detail_items, map { +{
1323         'ref'         => '', #should this be something else?
1324         'section'     => $discount_section,
1325         'description' => &$escape_function( $_->{description} ),
1326         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1327         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1328     } } @discounts_avail;
1329   }
1330
1331   my @summary_subtotals;
1332   # the templates say "$_->{tax_section} || !$_->{summarized}"
1333   # except 'summarized' is only true when tax_section is true, so this 
1334   # is always true, so what's the deal?
1335   foreach my $s (@sections) {
1336     # not to include in the "summary of new charges" block:
1337     # finance charges, adjustments, previous charges, 
1338     # and itemized phone usage sections
1339     if ( $s eq $adjust_section   or
1340          ($s eq $previous_section and $s ne $default_section) or
1341          ($invoice_data{'finance_section'} and 
1342           $invoice_data{'finance_section'} eq $s->{description}) or
1343          $s->{'description'} =~ /^\d+ $/ ) {
1344       next;
1345     }
1346     push @summary_subtotals, $s;
1347   }
1348   $invoice_data{summary_subtotals} = \@summary_subtotals;
1349
1350   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1351   # the invoice variables
1352   return \%invoice_data if ( $params{'diag'} );
1353
1354   # All sections and items are built; now fill in templates.
1355   my @includelist = ();
1356   push @includelist, 'summary' if $summarypage;
1357   foreach my $include ( @includelist ) {
1358
1359     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1360     my @inc_src;
1361
1362     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1363
1364       @inc_src = $conf->config($inc_file, $agentnum);
1365
1366     } else {
1367
1368       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1369
1370       my $convert_map = $convert_maps{$format}{$include};
1371
1372       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1373                        s/--\@\]/$delimiters{$format}[1]/g;
1374                        $_;
1375                      } 
1376                  &$convert_map( $conf->config($inc_file, $agentnum) );
1377
1378     }
1379
1380     my $inc_tt = new Text::Template (
1381       TYPE       => 'ARRAY',
1382       SOURCE     => [ map "$_\n", @inc_src ],
1383       DELIMITERS => $delimiters{$format},
1384     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1385
1386     unless ( $inc_tt->compile() ) {
1387       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1388       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1389       die $error;
1390     }
1391
1392     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1393
1394     $invoice_data{$include} =~ s/\n+$//
1395       if ($format eq 'latex');
1396   }
1397
1398   $invoice_lines = 0;
1399   my $wasfunc = 0;
1400   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1401     /invoice_lines\((\d*)\)/;
1402     $invoice_lines += $1 || scalar(@buf);
1403     $wasfunc=1;
1404   }
1405   die "no invoice_lines() functions in template?"
1406     if ( $format eq 'template' && !$wasfunc );
1407
1408   if ($format eq 'template') {
1409
1410     if ( $invoice_lines ) {
1411       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1412       $invoice_data{'total_pages'}++
1413         if scalar(@buf) % $invoice_lines;
1414     }
1415
1416     #setup subroutine for the template
1417     $invoice_data{invoice_lines} = sub {
1418       my $lines = shift || scalar(@buf);
1419       map { 
1420         scalar(@buf)
1421           ? shift @buf
1422           : [ '', '' ];
1423       }
1424       ( 1 .. $lines );
1425     };
1426
1427     my $lines;
1428     my @collect;
1429     while (@buf) {
1430       push @collect, split("\n",
1431         $text_template->fill_in( HASH => \%invoice_data )
1432       );
1433       $invoice_data{'page'}++;
1434     }
1435     map "$_\n", @collect;
1436
1437   } else { # this is where we actually create the invoice
1438
1439     warn "filling in template for invoice ". $self->invnum. "\n"
1440       if $DEBUG;
1441     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1442       if $DEBUG > 1;
1443
1444     $text_template->fill_in(HASH => \%invoice_data);
1445   }
1446 }
1447
1448 sub notice_name { '('.shift->table.')'; }
1449
1450 sub template_conf { 'invoice_'; }
1451
1452 # helper routine for generating date ranges
1453 sub _prior_month30s {
1454   my $self = shift;
1455   my @ranges = (
1456    [ 1,       2592000 ], # 0-30 days ago
1457    [ 2592000, 5184000 ], # 30-60 days ago
1458    [ 5184000, 7776000 ], # 60-90 days ago
1459    [ 7776000, 0       ], # 90+   days ago
1460   );
1461
1462   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1463           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1464       ] }
1465   @ranges;
1466 }
1467
1468 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1469
1470 Returns an postscript invoice, as a scalar.
1471
1472 Options can be passed as a hashref (recommended) or as a list of time, template
1473 and then any key/value pairs for any other options.
1474
1475 I<time> an optional value used to control the printing of overdue messages.  The
1476 default is now.  It isn't the date of the invoice; that's the `_date' field.
1477 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1478 L<Time::Local> and L<Date::Parse> for conversion functions.
1479
1480 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1481
1482 =cut
1483
1484 sub print_ps {
1485   my $self = shift;
1486
1487   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1488   my $ps = generate_ps($file);
1489   unlink($logofile);
1490   unlink($barcodefile) if $barcodefile;
1491
1492   $ps;
1493 }
1494
1495 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1496
1497 Returns an PDF invoice, as a scalar.
1498
1499 Options can be passed as a hashref (recommended) or as a list of time, template
1500 and then any key/value pairs for any other options.
1501
1502 I<time> an optional value used to control the printing of overdue messages.  The
1503 default is now.  It isn't the date of the invoice; that's the `_date' field.
1504 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1505 L<Time::Local> and L<Date::Parse> for conversion functions.
1506
1507 I<template>, if specified, is the name of a suffix for alternate invoices.
1508
1509 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1510
1511 =cut
1512
1513 sub print_pdf {
1514   my $self = shift;
1515
1516   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1517   my $pdf = generate_pdf($file);
1518   unlink($logofile);
1519   unlink($barcodefile) if $barcodefile;
1520
1521   $pdf;
1522 }
1523
1524 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1525
1526 Returns an HTML invoice, as a scalar.
1527
1528 I<time> an optional value used to control the printing of overdue messages.  The
1529 default is now.  It isn't the date of the invoice; that's the `_date' field.
1530 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1531 L<Time::Local> and L<Date::Parse> for conversion functions.
1532
1533 I<template>, if specified, is the name of a suffix for alternate invoices.
1534
1535 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1536
1537 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1538 when emailing the invoice as part of a multipart/related MIME email.
1539
1540 =cut
1541
1542 sub print_html {
1543   my $self = shift;
1544   my %params;
1545   if ( ref($_[0]) ) {
1546     %params = %{ shift() }; 
1547   }else{
1548     $params{'time'} = shift;
1549     $params{'template'} = shift;
1550     $params{'cid'} = shift;
1551   }
1552
1553   $params{'format'} = 'html';
1554   
1555   $self->print_generic( %params );
1556 }
1557
1558 # quick subroutine for print_latex
1559 #
1560 # There are ten characters that LaTeX treats as special characters, which
1561 # means that they do not simply typeset themselves: 
1562 #      # $ % & ~ _ ^ \ { }
1563 #
1564 # TeX ignores blanks following an escaped character; if you want a blank (as
1565 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1566
1567 sub _latex_escape {
1568   my $value = shift;
1569   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1570   $value =~ s/([<>])/\$$1\$/g;
1571   $value;
1572 }
1573
1574 sub _html_escape {
1575   my $value = shift;
1576   encode_entities($value);
1577   $value;
1578 }
1579
1580 sub _html_escape_nbsp {
1581   my $value = _html_escape(shift);
1582   $value =~ s/ +/&nbsp;/g;
1583   $value;
1584 }
1585
1586 #utility methods for print_*
1587
1588 sub _translate_old_latex_format {
1589   warn "_translate_old_latex_format called\n"
1590     if $DEBUG; 
1591
1592   my @template = ();
1593   while ( @_ ) {
1594     my $line = shift;
1595   
1596     if ( $line =~ /^%%Detail\s*$/ ) {
1597   
1598       push @template, q![@--!,
1599                       q!  foreach my $_tr_line (@detail_items) {!,
1600                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1601                       q!      $_tr_line->{'description'} .= !, 
1602                       q!        "\\tabularnewline\n~~".!,
1603                       q!        join( "\\tabularnewline\n~~",!,
1604                       q!          @{$_tr_line->{'ext_description'}}!,
1605                       q!        );!,
1606                       q!    }!;
1607
1608       while ( ( my $line_item_line = shift )
1609               !~ /^%%EndDetail\s*$/                            ) {
1610         $line_item_line =~ s/'/\\'/g;    # nice LTS
1611         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1612         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1613         push @template, "    \$OUT .= '$line_item_line';";
1614       }
1615
1616       push @template, '}',
1617                       '--@]';
1618       #' doh, gvim
1619     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1620
1621       push @template, '[@--',
1622                       '  foreach my $_tr_line (@total_items) {';
1623
1624       while ( ( my $total_item_line = shift )
1625               !~ /^%%EndTotalDetails\s*$/                      ) {
1626         $total_item_line =~ s/'/\\'/g;    # nice LTS
1627         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1628         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1629         push @template, "    \$OUT .= '$total_item_line';";
1630       }
1631
1632       push @template, '}',
1633                       '--@]';
1634
1635     } else {
1636       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1637       push @template, $line;  
1638     }
1639   
1640   }
1641
1642   if ($DEBUG) {
1643     warn "$_\n" foreach @template;
1644   }
1645
1646   (@template);
1647 }
1648
1649 sub terms {
1650   my $self = shift;
1651   my $conf = $self->conf;
1652
1653   #check for an invoice-specific override
1654   return $self->invoice_terms if $self->invoice_terms;
1655   
1656   #check for a customer- specific override
1657   my $cust_main = $self->cust_main;
1658   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1659
1660   #use configured default
1661   $conf->config('invoice_default_terms') || '';
1662 }
1663
1664 sub due_date {
1665   my $self = shift;
1666   my $duedate = '';
1667   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1668     $duedate = $self->_date() + ( $1 * 86400 );
1669   }
1670   $duedate;
1671 }
1672
1673 sub due_date2str {
1674   my $self = shift;
1675   $self->due_date ? time2str(shift, $self->due_date) : '';
1676 }
1677
1678 sub balance_due_msg {
1679   my $self = shift;
1680   my $msg = $self->mt('Balance Due');
1681   return $msg unless $self->terms;
1682   if ( $self->due_date ) {
1683     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1684       $self->due_date2str($date_format);
1685   } elsif ( $self->terms ) {
1686     $msg .= ' - '. $self->terms;
1687   }
1688   $msg;
1689 }
1690
1691 sub balance_due_date {
1692   my $self = shift;
1693   my $conf = $self->conf;
1694   my $duedate = '';
1695   if (    $conf->exists('invoice_default_terms') 
1696        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1697     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1698   }
1699   $duedate;
1700 }
1701
1702 sub credit_balance_msg { 
1703   my $self = shift;
1704   $self->mt('Credit Balance Remaining')
1705 }
1706
1707 =item _date_pretty
1708
1709 Returns a string with the date, for example: "3/20/2008"
1710
1711 =cut
1712
1713 sub _date_pretty {
1714   my $self = shift;
1715   time2str($date_format, $self->_date);
1716 }
1717
1718 =item _items_sections OPTIONS
1719
1720 Generate section information for all items appearing on this invoice.
1721 This will only be called for multi-section invoices.
1722
1723 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
1724 related display records (L<FS::cust_bill_pkg_display>) and organize 
1725 them into two groups ("early" and "late" according to whether they come 
1726 before or after the total), then into sections.  A subtotal is calculated 
1727 for each section.
1728
1729 Section descriptions are returned in sort weight order.  Each consists 
1730 of a hash containing:
1731
1732 description: the package category name, escaped
1733 subtotal: the total charges in that section
1734 tax_section: a flag indicating that the section contains only tax charges
1735 summarized: same as tax_section, for some reason
1736 sort_weight: the package category's sort weight
1737
1738 If 'condense' is set on the display record, it also contains everything 
1739 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1740 coderefs to generate parts of the invoice.  This is not advised.
1741
1742 The method returns two arrayrefs, one of "early" sections and one of "late"
1743 sections.
1744
1745 OPTIONS may include:
1746
1747 by_location: a flag to divide the invoice into sections by location.  
1748 Each section hash will have a 'location' element containing a hashref of 
1749 the location fields (see L<FS::cust_location>).  The section description
1750 will be the location label, but the template can use any of the location 
1751 fields to create a suitable label.
1752
1753 by_category: a flag to divide the invoice into sections using display 
1754 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
1755 behavior.  Each section hash will have a 'category' element containing
1756 the section name from the display record (which probably equals the 
1757 category name of the package, but may not in some cases).
1758
1759 summary: a flag indicating that this is a summary-format invoice.
1760 Turning this on has the following effects:
1761 - Ignores display items with the 'summary' flag.
1762 - Places all sections in the "early" group even if they have post_total.
1763 - Creates sections for all non-disabled package categories, even if they 
1764 have no charges on this invoice, as well as a section with no name.
1765
1766 escape: an escape function to use for section titles.
1767
1768 extra_sections: an arrayref of additional sections to return after the 
1769 sorted list.  If there are any of these, section subtotals exclude 
1770 usage charges.
1771
1772 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
1773 passed through to C<_condense_section()>.
1774
1775 =cut
1776
1777 use vars qw(%pkg_category_cache);
1778 sub _items_sections {
1779   my $self = shift;
1780   my %opt = @_;
1781   
1782   my $escape = $opt{escape};
1783   my @extra_sections = @{ $opt{extra_sections} || [] };
1784
1785   # $subtotal{$locationnum}{$categoryname} = amount.
1786   # if we're not using by_location, $locationnum is undef.
1787   # if we're not using by_category, you guessed it, $categoryname is undef.
1788   # if we're not using either one, we shouldn't be here in the first place...
1789   my %subtotal = ();
1790   my %late_subtotal = ();
1791   my %not_tax = ();
1792
1793   # About tax items + multisection invoices:
1794   # If either invoice_*summary option is enabled, AND there is a 
1795   # package category with the name of the tax, then there will be 
1796   # a display record assigning the tax item to that category.
1797   #
1798   # However, the taxes are always placed in the "Taxes, Surcharges,
1799   # and Fees" section regardless of that.  The only effect of the 
1800   # display record is to create a subtotal for the summary page.
1801
1802   # cache these
1803   my $pkg_hash = $self->cust_pkg_hash;
1804
1805   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1806   {
1807
1808       my $usage = $cust_bill_pkg->usage;
1809
1810       my $locationnum;
1811       if ( $opt{by_location} ) {
1812         if ( $cust_bill_pkg->pkgnum ) {
1813           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1814         } else {
1815           $locationnum = '';
1816         }
1817       } else {
1818         $locationnum = undef;
1819       }
1820
1821       # as in _items_cust_pkg, if a line item has no display records,
1822       # cust_bill_pkg_display() returns a default record for it
1823
1824       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1825         next if ( $display->summary && $opt{summary} );
1826
1827         my $section = $display->section;
1828         my $type    = $display->type;
1829         $section = undef unless $opt{by_category};
1830
1831         $not_tax{$locationnum}{$section} = 1
1832           unless $cust_bill_pkg->pkgnum == 0;
1833
1834         # there's actually a very important piece of logic buried in here:
1835         # incrementing $late_subtotal{$section} CREATES 
1836         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
1837         # to define the list of late sections, and likewise keys(%subtotal).
1838         # When _items_cust_bill_pkg is called to generate line items for 
1839         # real, it will be called with 'section' => $section for each 
1840         # of these.
1841         if ( $display->post_total && !$opt{summary} ) {
1842           if (! $type || $type eq 'S') {
1843             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1844               if $cust_bill_pkg->setup != 0
1845               || $cust_bill_pkg->setup_show_zero;
1846           }
1847
1848           if (! $type) {
1849             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1850               if $cust_bill_pkg->recur != 0
1851               || $cust_bill_pkg->recur_show_zero;
1852           }
1853
1854           if ($type && $type eq 'R') {
1855             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1856               if $cust_bill_pkg->recur != 0
1857               || $cust_bill_pkg->recur_show_zero;
1858           }
1859           
1860           if ($type && $type eq 'U') {
1861             $late_subtotal{$locationnum}{$section} += $usage
1862               unless scalar(@extra_sections);
1863           }
1864
1865         } else { # it's a pre-total (normal) section
1866
1867           # skip tax items unless they're explicitly included in a section
1868           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1869
1870           if (! $type || $type eq 'S') {
1871             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1872               if $cust_bill_pkg->setup != 0
1873               || $cust_bill_pkg->setup_show_zero;
1874           }
1875
1876           if (! $type) {
1877             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1878               if $cust_bill_pkg->recur != 0
1879               || $cust_bill_pkg->recur_show_zero;
1880           }
1881
1882           if ($type && $type eq 'R') {
1883             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1884               if $cust_bill_pkg->recur != 0
1885               || $cust_bill_pkg->recur_show_zero;
1886           }
1887           
1888           if ($type && $type eq 'U') {
1889             $subtotal{$locationnum}{$section} += $usage
1890               unless scalar(@extra_sections);
1891           }
1892
1893         }
1894
1895       }
1896
1897   }
1898
1899   %pkg_category_cache = ();
1900
1901   # summary invoices need subtotals for all non-disabled package categories,
1902   # even if they're zero
1903   # but currently assume that there are no location sections, or at least
1904   # that the summary page doesn't care about them
1905   if ( $opt{summary} ) {
1906     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1907       $subtotal{''}{$category->categoryname} ||= 0;
1908     }
1909     $subtotal{''}{''} ||= 0;
1910   }
1911
1912   my @sections;
1913   foreach my $post_total (0,1) {
1914     my @these;
1915     my $s = $post_total ? \%late_subtotal : \%subtotal;
1916     foreach my $locationnum (keys %$s) {
1917       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1918         my $section = {
1919                         'subtotal'    => $s->{$locationnum}{$sectionname},
1920                         'post_total'  => $post_total,
1921                         'sort_weight' => 0,
1922                       };
1923         if ( $locationnum ) {
1924           $section->{'locationnum'} = $locationnum;
1925           my $location = FS::cust_location->by_key($locationnum);
1926           $section->{'description'} = &{ $escape }($location->location_label);
1927           # Better ideas? This will roughly group them by proximity, 
1928           # which alpha sorting on any of the address fields won't.
1929           # Sorting by locationnum is meaningless.
1930           # We have to sort on _something_ or the order may change 
1931           # randomly from one invoice to the next, which will confuse
1932           # people.
1933           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1934                                       $locationnum;
1935           $section->{'location'} = {
1936             map { $_ => &{ $escape }($location->get($_)) }
1937             $location->fields
1938           };
1939         } else {
1940           $section->{'category'} = $sectionname;
1941           $section->{'description'} = &{ $escape }($sectionname);
1942           if ( _pkg_category($_) ) {
1943             $section->{'sort_weight'} = _pkg_category($_)->weight;
1944             if ( _pkg_category($_)->condense ) {
1945               $section = { %$section, $self->_condense_section($opt{format}) };
1946             }
1947           }
1948         }
1949         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1950           # then it's a tax-only section
1951           $section->{'summarized'} = 'Y';
1952           $section->{'tax_section'} = 'Y';
1953         }
1954         push @these, $section;
1955       } # foreach $sectionname
1956     } #foreach $locationnum
1957     push @these, @extra_sections if $post_total == 0;
1958     # need an alpha sort for location sections, because postal codes can 
1959     # be non-numeric
1960     $sections[ $post_total ] = [ sort {
1961       $opt{'by_location'} ? 
1962         ($a->{sort_weight} cmp $b->{sort_weight}) :
1963         ($a->{sort_weight} <=> $b->{sort_weight})
1964       } @these ];
1965   } #foreach $post_total
1966
1967   return @sections; # early, late
1968 }
1969
1970 #helper subs for above
1971
1972 sub cust_pkg_hash {
1973   my $self = shift;
1974   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
1975 }
1976
1977 sub _pkg_category {
1978   my $categoryname = shift;
1979   $pkg_category_cache{$categoryname} ||=
1980     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1981 }
1982
1983 my %condensed_format = (
1984   'label' => [ qw( Description Qty Amount ) ],
1985   'fields' => [
1986                 sub { shift->{description} },
1987                 sub { shift->{quantity} },
1988                 sub { my($href, %opt) = @_;
1989                       ($opt{dollar} || ''). $href->{amount};
1990                     },
1991               ],
1992   'align'  => [ qw( l r r ) ],
1993   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
1994   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
1995 );
1996
1997 sub _condense_section {
1998   my ( $self, $format ) = ( shift, shift );
1999   ( 'condensed' => 1,
2000     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2001       qw( description_generator
2002           header_generator
2003           total_generator
2004           total_line_generator
2005         )
2006   );
2007 }
2008
2009 sub _condensed_generator_defaults {
2010   my ( $self, $format ) = ( shift, shift );
2011   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2012 }
2013
2014 my %html_align = (
2015   'c' => 'center',
2016   'l' => 'left',
2017   'r' => 'right',
2018 );
2019
2020 sub _condensed_header_generator {
2021   my ( $self, $format ) = ( shift, shift );
2022
2023   my ( $f, $prefix, $suffix, $separator, $column ) =
2024     _condensed_generator_defaults($format);
2025
2026   if ($format eq 'latex') {
2027     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2028     $suffix = "\\\\\n\\hline";
2029     $separator = "&\n";
2030     $column =
2031       sub { my ($d,$a,$s,$w) = @_;
2032             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2033           };
2034   } elsif ( $format eq 'html' ) {
2035     $prefix = '<th></th>';
2036     $suffix = '';
2037     $separator = '';
2038     $column =
2039       sub { my ($d,$a,$s,$w) = @_;
2040             return qq!<th align="$html_align{$a}">$d</th>!;
2041       };
2042   }
2043
2044   sub {
2045     my @args = @_;
2046     my @result = ();
2047
2048     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2049       push @result,
2050         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2051     }
2052
2053     $prefix. join($separator, @result). $suffix;
2054   };
2055
2056 }
2057
2058 sub _condensed_description_generator {
2059   my ( $self, $format ) = ( shift, shift );
2060
2061   my ( $f, $prefix, $suffix, $separator, $column ) =
2062     _condensed_generator_defaults($format);
2063
2064   my $money_char = '$';
2065   if ($format eq 'latex') {
2066     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2067     $suffix = '\\\\';
2068     $separator = " & \n";
2069     $column =
2070       sub { my ($d,$a,$s,$w) = @_;
2071             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2072           };
2073     $money_char = '\\dollar';
2074   }elsif ( $format eq 'html' ) {
2075     $prefix = '"><td align="center"></td>';
2076     $suffix = '';
2077     $separator = '';
2078     $column =
2079       sub { my ($d,$a,$s,$w) = @_;
2080             return qq!<td align="$html_align{$a}">$d</td>!;
2081       };
2082     #$money_char = $conf->config('money_char') || '$';
2083     $money_char = '';  # this is madness
2084   }
2085
2086   sub {
2087     #my @args = @_;
2088     my $href = shift;
2089     my @result = ();
2090
2091     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2092       my $dollar = '';
2093       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2094       push @result,
2095         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2096                     map { $f->{$_}->[$i] } qw(align span width)
2097                   );
2098     }
2099
2100     $prefix. join( $separator, @result ). $suffix;
2101   };
2102
2103 }
2104
2105 sub _condensed_total_generator {
2106   my ( $self, $format ) = ( shift, shift );
2107
2108   my ( $f, $prefix, $suffix, $separator, $column ) =
2109     _condensed_generator_defaults($format);
2110   my $style = '';
2111
2112   if ($format eq 'latex') {
2113     $prefix = "& ";
2114     $suffix = "\\\\\n";
2115     $separator = " & \n";
2116     $column =
2117       sub { my ($d,$a,$s,$w) = @_;
2118             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2119           };
2120   }elsif ( $format eq 'html' ) {
2121     $prefix = '';
2122     $suffix = '';
2123     $separator = '';
2124     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2125     $column =
2126       sub { my ($d,$a,$s,$w) = @_;
2127             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2128       };
2129   }
2130
2131
2132   sub {
2133     my @args = @_;
2134     my @result = ();
2135
2136     #  my $r = &{$f->{fields}->[$i]}(@args);
2137     #  $r .= ' Total' unless $i;
2138
2139     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2140       push @result,
2141         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2142                     map { $f->{$_}->[$i] } qw(align span width)
2143                   );
2144     }
2145
2146     $prefix. join( $separator, @result ). $suffix;
2147   };
2148
2149 }
2150
2151 =item total_line_generator FORMAT
2152
2153 Returns a coderef used for generation of invoice total line items for this
2154 usage_class.  FORMAT is either html or latex
2155
2156 =cut
2157
2158 # should not be used: will have issues with hash element names (description vs
2159 # total_item and amount vs total_amount -- another array of functions?
2160
2161 sub _condensed_total_line_generator {
2162   my ( $self, $format ) = ( shift, shift );
2163
2164   my ( $f, $prefix, $suffix, $separator, $column ) =
2165     _condensed_generator_defaults($format);
2166   my $style = '';
2167
2168   if ($format eq 'latex') {
2169     $prefix = "& ";
2170     $suffix = "\\\\\n";
2171     $separator = " & \n";
2172     $column =
2173       sub { my ($d,$a,$s,$w) = @_;
2174             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2175           };
2176   }elsif ( $format eq 'html' ) {
2177     $prefix = '';
2178     $suffix = '';
2179     $separator = '';
2180     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2181     $column =
2182       sub { my ($d,$a,$s,$w) = @_;
2183             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2184       };
2185   }
2186
2187
2188   sub {
2189     my @args = @_;
2190     my @result = ();
2191
2192     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2193       push @result,
2194         &{$column}( &{$f->{fields}->[$i]}(@args),
2195                     map { $f->{$_}->[$i] } qw(align span width)
2196                   );
2197     }
2198
2199     $prefix. join( $separator, @result ). $suffix;
2200   };
2201
2202 }
2203
2204 =item _items_pkg [ OPTIONS ]
2205
2206 Return line item hashes for each package item on this invoice. Nearly 
2207 equivalent to 
2208
2209 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2210
2211 The only OPTIONS accepted is 'section', which may point to a hashref 
2212 with a key named 'condensed', which may have a true value.  If it 
2213 does, this method tries to merge identical items into items with 
2214 'quantity' equal to the number of items (not the sum of their 
2215 separate quantities, for some reason).
2216
2217 =cut
2218
2219 sub _items_nontax {
2220   my $self = shift;
2221   grep { $_->pkgnum } $self->cust_bill_pkg;
2222 }
2223
2224 sub _items_pkg {
2225   my $self = shift;
2226   my %options = @_;
2227
2228   warn "$me _items_pkg searching for all package line items\n"
2229     if $DEBUG > 1;
2230
2231   my @cust_bill_pkg = $self->_items_nontax;
2232
2233   warn "$me _items_pkg filtering line items\n"
2234     if $DEBUG > 1;
2235   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2236
2237   if ($options{section} && $options{section}->{condensed}) {
2238
2239     warn "$me _items_pkg condensing section\n"
2240       if $DEBUG > 1;
2241
2242     my %itemshash = ();
2243     local $Storable::canonical = 1;
2244     foreach ( @items ) {
2245       my $item = { %$_ };
2246       delete $item->{ref};
2247       delete $item->{ext_description};
2248       my $key = freeze($item);
2249       $itemshash{$key} ||= 0;
2250       $itemshash{$key} ++; # += $item->{quantity};
2251     }
2252     @items = sort { $a->{description} cmp $b->{description} }
2253              map { my $i = thaw($_);
2254                    $i->{quantity} = $itemshash{$_};
2255                    $i->{amount} =
2256                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2257                    $i;
2258                  }
2259              keys %itemshash;
2260   }
2261
2262   warn "$me _items_pkg returning ". scalar(@items). " items\n"
2263     if $DEBUG > 1;
2264
2265   @items;
2266 }
2267
2268 sub _taxsort {
2269   return 0 unless $a->itemdesc cmp $b->itemdesc;
2270   return -1 if $b->itemdesc eq 'Tax';
2271   return 1 if $a->itemdesc eq 'Tax';
2272   return -1 if $b->itemdesc eq 'Other surcharges';
2273   return 1 if $a->itemdesc eq 'Other surcharges';
2274   $a->itemdesc cmp $b->itemdesc;
2275 }
2276
2277 sub _items_tax {
2278   my $self = shift;
2279   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2280   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2281
2282   if ( $self->conf->exists('always_show_tax') ) {
2283     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2284     if (0 == grep { $_->{description} eq $itemdesc } @items) {
2285       push @items,
2286         { 'description' => $itemdesc,
2287           'amount'      => 0.00 };
2288     }
2289   }
2290   @items;
2291 }
2292
2293 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2294
2295 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2296 list of hashrefs describing the line items they generate on the invoice.
2297
2298 OPTIONS may include:
2299
2300 format: the invoice format.
2301
2302 escape_function: the function used to escape strings.
2303
2304 DEPRECATED? (expensive, mostly unused?)
2305 format_function: the function used to format CDRs.
2306
2307 section: a hashref containing 'category' and/or 'locationnum'; if this 
2308 is present, only returns line items that belong to that category and/or
2309 location (whichever is defined).
2310
2311 multisection: a flag indicating that this is a multisection invoice,
2312 which does something complicated.
2313
2314 Returns a list of hashrefs, each of which may contain:
2315
2316 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
2317 ext_description, which is an arrayref of detail lines to show below 
2318 the package line.
2319
2320 =cut
2321
2322 sub _items_cust_bill_pkg {
2323   my $self = shift;
2324   my $conf = $self->conf;
2325   my $cust_bill_pkgs = shift;
2326   my %opt = @_;
2327
2328   my $format = $opt{format} || '';
2329   my $escape_function = $opt{escape_function} || sub { shift };
2330   my $format_function = $opt{format_function} || '';
2331   my $no_usage = $opt{no_usage} || '';
2332   my $unsquelched = $opt{unsquelched} || ''; #unused
2333   my ($section, $locationnum, $category);
2334   if ( $opt{section} ) {
2335     $category = $opt{section}->{category};
2336     $locationnum = $opt{section}->{locationnum};
2337   }
2338   my $summary_page = $opt{summary_page} || ''; #unused
2339   my $multisection = defined($category) || defined($locationnum);
2340   my $discount_show_always = 0;
2341
2342   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2343
2344   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2345                                    # and location labels
2346
2347   my @b = ();
2348   my ($s, $r, $u) = ( undef, undef, undef );
2349   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2350   {
2351
2352     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2353       if ( $_ && !$cust_bill_pkg->hidden ) {
2354         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2355         $_->{amount}      =~ s/^\-0\.00$/0.00/;
2356         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2357         push @b, { %$_ }
2358           if $_->{amount} != 0
2359           || $discount_show_always
2360           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2361           || (   $_->{_is_setup} && $_->{setup_show_zero} )
2362         ;
2363         $_ = undef;
2364       }
2365     }
2366
2367     if ( $locationnum ) {
2368       # this is a location section; skip packages that aren't at this
2369       # service location.
2370       next if $cust_bill_pkg->pkgnum == 0;
2371       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
2372               != $locationnum;
2373     }
2374
2375     # Consider display records for this item to determine if it belongs
2376     # in this section.  Note that if there are no display records, there
2377     # will be a default pseudo-record that includes all charge types 
2378     # and has no section name.
2379     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2380                                   ? $cust_bill_pkg->cust_bill_pkg_display
2381                                   : ( $cust_bill_pkg );
2382
2383     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2384          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2385       if $DEBUG > 1;
2386
2387     if ( defined($category) ) {
2388       # then this is a package category section; process all display records
2389       # that belong to this section.
2390       @cust_bill_pkg_display = grep { $_->section eq $category }
2391                                 @cust_bill_pkg_display;
2392     } else {
2393       # otherwise, process all display records that aren't usage summaries
2394       # (I don't think there should be usage summaries if you aren't using 
2395       # category sections, but this is the historical behavior)
2396       @cust_bill_pkg_display = grep { !$_->summary }
2397                                 @cust_bill_pkg_display;
2398     }
2399     foreach my $display (@cust_bill_pkg_display) {
2400
2401       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2402            $display->billpkgdisplaynum. "\n"
2403         if $DEBUG > 1;
2404
2405       my $type = $display->type;
2406
2407       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2408       $desc = substr($desc, 0, $maxlength). '...'
2409         if $format eq 'latex' && length($desc) > $maxlength;
2410
2411       my %details_opt = ( 'format'          => $format,
2412                           'escape_function' => $escape_function,
2413                           'format_function' => $format_function,
2414                           'no_usage'        => $opt{'no_usage'},
2415                         );
2416
2417       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2418
2419         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2420           if $DEBUG > 1;
2421
2422         if ( $cust_bill_pkg->setup != 0 ) {
2423           my $description = $desc;
2424           $description .= ' Setup'
2425             if $cust_bill_pkg->recur != 0
2426             || $discount_show_always
2427             || $cust_bill_pkg->recur_show_zero;
2428           push @b, {
2429             'description' => $description,
2430             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2431           };
2432         }
2433         if ( $cust_bill_pkg->recur != 0 ) {
2434           push @b, {
2435             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2436             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2437           };
2438         }
2439
2440       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2441
2442         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2443           if $DEBUG > 1;
2444  
2445         my $cust_pkg = $cust_bill_pkg->cust_pkg;
2446
2447         # which pkgpart to show for display purposes?
2448         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2449
2450         # start/end dates for invoice formats that do nonstandard 
2451         # things with them
2452         my %item_dates = ();
2453         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2454           unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2455
2456         if (    (!$type || $type eq 'S')
2457              && (    $cust_bill_pkg->setup != 0
2458                   || $cust_bill_pkg->setup_show_zero
2459                 )
2460            )
2461          {
2462
2463           warn "$me _items_cust_bill_pkg adding setup\n"
2464             if $DEBUG > 1;
2465
2466           my $description = $desc;
2467           $description .= ' Setup'
2468             if $cust_bill_pkg->recur != 0
2469             || $discount_show_always
2470             || $cust_bill_pkg->recur_show_zero;
2471
2472           my @d = ();
2473           my $svc_label;
2474           unless ( $cust_pkg->part_pkg->hide_svc_detail
2475                 || $cust_bill_pkg->hidden )
2476           {
2477
2478             my @svc_labels = map &{$escape_function}($_),
2479                         $cust_pkg->h_labels_short($self->_date, undef, 'I');
2480             push @d, @svc_labels
2481               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2482             $svc_label = $svc_labels[0];
2483
2484             my $lnum = $cust_main ? $cust_main->ship_locationnum
2485                                   : $self->prospect_main->locationnum;
2486             # show the location label if it's not the customer's default
2487             # location, and we're not grouping items by location already
2488             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2489               my $loc = $cust_pkg->location_label;
2490               $loc = substr($loc, 0, $maxlength). '...'
2491                 if $format eq 'latex' && length($loc) > $maxlength;
2492               push @d, &{$escape_function}($loc);
2493             }
2494
2495           } #unless hiding service details
2496
2497           push @d, $cust_bill_pkg->details(%details_opt)
2498             if $cust_bill_pkg->recur == 0;
2499
2500           if ( $cust_bill_pkg->hidden ) {
2501             $s->{amount}      += $cust_bill_pkg->setup;
2502             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2503             push @{ $s->{ext_description} }, @d;
2504           } else {
2505             $s = {
2506               _is_setup       => 1,
2507               description     => $description,
2508               pkgpart         => $pkgpart,
2509               pkgnum          => $cust_bill_pkg->pkgnum,
2510               amount          => $cust_bill_pkg->setup,
2511               setup_show_zero => $cust_bill_pkg->setup_show_zero,
2512               unit_amount     => $cust_bill_pkg->unitsetup,
2513               quantity        => $cust_bill_pkg->quantity,
2514               ext_description => \@d,
2515               svc_label       => ($svc_label || ''),
2516             };
2517           };
2518
2519         }
2520
2521         if (    ( !$type || $type eq 'R' || $type eq 'U' )
2522              && (
2523                      $cust_bill_pkg->recur != 0
2524                   || $cust_bill_pkg->setup == 0
2525                   || $discount_show_always
2526                   || $cust_bill_pkg->recur_show_zero
2527                 )
2528            )
2529         {
2530
2531           warn "$me _items_cust_bill_pkg adding recur/usage\n"
2532             if $DEBUG > 1;
2533
2534           my $is_summary = $display->summary;
2535           my $description = $desc;
2536           if ( $type eq 'U' and ($is_summary or $cust_bill_pkg->hidden) ) {
2537             $description = $self->mt('Usage charges');
2538           }
2539
2540           my $part_pkg = $cust_pkg->part_pkg;
2541
2542           #pry be a bit more efficient to look some of this conf stuff up
2543           # outside the loop
2544           unless (
2545             $conf->exists('disable_line_item_date_ranges')
2546               || $part_pkg->option('disable_line_item_date_ranges',1)
2547               || ! $cust_bill_pkg->sdate
2548               || ! $cust_bill_pkg->edate
2549           ) {
2550             my $time_period;
2551             my $date_style = '';
2552             $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2553                                          $self->agentnum
2554                                        )
2555               if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2556             $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2557                                             $self->agentnum
2558                                          );
2559             if ( defined($date_style) && $date_style eq 'month_of' ) {
2560               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2561             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2562               my $desc = $conf->config( 'cust_bill-line_item-date_description',
2563                                          $self->agentnum
2564                                       );
2565               $desc .= ' ' unless $desc =~ /\s$/;
2566               $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2567             } else {
2568               $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
2569                            " - ". time2str($date_format, $cust_bill_pkg->edate);
2570             }
2571             $description .= " ($time_period)";
2572           }
2573
2574           my @d = ();
2575           my @seconds = (); # for display of usage info
2576           my $svc_label = '';
2577
2578           #at least until cust_bill_pkg has "past" ranges in addition to
2579           #the "future" sdate/edate ones... see #3032
2580           my @dates = ( $self->_date );
2581           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2582           push @dates, $prev->sdate if $prev;
2583           push @dates, undef if !$prev;
2584
2585           unless ( $part_pkg->hide_svc_detail
2586                 || $cust_bill_pkg->itemdesc
2587                 || $cust_bill_pkg->hidden
2588                 || $is_summary && $type && $type eq 'U'
2589               )
2590           {
2591
2592             warn "$me _items_cust_bill_pkg adding service details\n"
2593               if $DEBUG > 1;
2594
2595             my @svc_labels = map &{$escape_function}($_),
2596                         $cust_pkg->h_labels_short(@dates, 'I');
2597             push @d, @svc_labels
2598               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2599             $svc_label = $svc_labels[0];
2600
2601             warn "$me _items_cust_bill_pkg done adding service details\n"
2602               if $DEBUG > 1;
2603
2604             my $lnum = $cust_main ? $cust_main->ship_locationnum
2605                                   : $self->prospect_main->locationnum;
2606             # show the location label if it's not the customer's default
2607             # location, and we're not grouping items by location already
2608             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2609               my $loc = $cust_pkg->location_label;
2610               $loc = substr($loc, 0, $maxlength). '...'
2611                 if $format eq 'latex' && length($loc) > $maxlength;
2612               push @d, &{$escape_function}($loc);
2613             }
2614
2615             # Display of seconds_since_sqlradacct:
2616             # On the invoice, when processing @detail_items, look for a field
2617             # named 'seconds'.  This will contain total seconds for each 
2618             # service, in the same order as @ext_description.  For services 
2619             # that don't support this it will show undef.
2620             if ( $conf->exists('svc_acct-usage_seconds') 
2621                  and ! $cust_bill_pkg->pkgpart_override ) {
2622               foreach my $cust_svc ( 
2623                   $cust_pkg->h_cust_svc(@dates, 'I') 
2624                 ) {
2625
2626                 # eval because not having any part_export_usage exports 
2627                 # is a fatal error, last_bill/_date because that's how 
2628                 # sqlradius_hour billing does it
2629                 my $sec = eval {
2630                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2631                 };
2632                 push @seconds, $sec;
2633               }
2634             } #if svc_acct-usage_seconds
2635
2636           }
2637
2638           unless ( $is_summary ) {
2639             warn "$me _items_cust_bill_pkg adding details\n"
2640               if $DEBUG > 1;
2641
2642             #instead of omitting details entirely in this case (unwanted side
2643             # effects), just omit CDRs
2644             $details_opt{'no_usage'} = 1
2645               if $type && $type eq 'R';
2646
2647             push @d, $cust_bill_pkg->details(%details_opt);
2648           }
2649
2650           warn "$me _items_cust_bill_pkg calculating amount\n"
2651             if $DEBUG > 1;
2652   
2653           my $amount = 0;
2654           if (!$type) {
2655             $amount = $cust_bill_pkg->recur;
2656           } elsif ($type eq 'R') {
2657             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2658           } elsif ($type eq 'U') {
2659             $amount = $cust_bill_pkg->usage;
2660           }
2661   
2662           my $unit_amount =
2663             ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2664                                               : $amount;
2665
2666           if ( !$type || $type eq 'R' ) {
2667
2668             warn "$me _items_cust_bill_pkg adding recur\n"
2669               if $DEBUG > 1;
2670
2671             if ( $cust_bill_pkg->hidden ) {
2672               $r->{amount}      += $amount;
2673               $r->{unit_amount} += $unit_amount;
2674               push @{ $r->{ext_description} }, @d;
2675             } else {
2676               $r = {
2677                 description     => $description,
2678                 pkgpart         => $pkgpart,
2679                 pkgnum          => $cust_bill_pkg->pkgnum,
2680                 amount          => $amount,
2681                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2682                 unit_amount     => $unit_amount,
2683                 quantity        => $cust_bill_pkg->quantity,
2684                 %item_dates,
2685                 ext_description => \@d,
2686                 svc_label       => ($svc_label || ''),
2687               };
2688               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2689             }
2690
2691           } else {  # $type eq 'U'
2692
2693             warn "$me _items_cust_bill_pkg adding usage\n"
2694               if $DEBUG > 1;
2695
2696             if ( $cust_bill_pkg->hidden and defined($u) ) {
2697               # if this is a hidden package and there's already a usage
2698               # line for the bundle, add this package's total amount and
2699               # usage details to it
2700               $u->{amount}      += $amount;
2701               $u->{unit_amount} += $unit_amount,
2702               push @{ $u->{ext_description} }, @d;
2703             } elsif ( $amount ) {
2704               # create a new usage line
2705               $u = {
2706                 description     => $description,
2707                 pkgpart         => $pkgpart,
2708                 pkgnum          => $cust_bill_pkg->pkgnum,
2709                 amount          => $amount,
2710                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2711                 unit_amount     => $unit_amount,
2712                 quantity        => $cust_bill_pkg->quantity,
2713                 %item_dates,
2714                 ext_description => \@d,
2715               };
2716             } # else this has no usage, so don't create a usage section
2717           }
2718
2719         } # recurring or usage with recurring charge
2720
2721       } else { #pkgnum tax or one-shot line item (??)
2722
2723         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2724           if $DEBUG > 1;
2725
2726         if ( $cust_bill_pkg->setup != 0 ) {
2727           push @b, {
2728             'description' => $desc,
2729             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2730           };
2731         }
2732         if ( $cust_bill_pkg->recur != 0 ) {
2733           push @b, {
2734             'description' => "$desc (".
2735                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2736                              time2str($date_format, $cust_bill_pkg->edate). ')',
2737             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2738           };
2739         }
2740
2741       }
2742
2743     }
2744
2745     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2746                                 && $conf->exists('discount-show-always'));
2747
2748   }
2749
2750   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2751     if ( $_  ) {
2752       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2753       $_->{amount}      =~ s/^\-0\.00$/0.00/;
2754       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2755       push @b, { %$_ }
2756         if $_->{amount} != 0
2757         || $discount_show_always
2758         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2759         || (   $_->{_is_setup} && $_->{setup_show_zero} )
2760     }
2761   }
2762
2763   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2764     if $DEBUG > 1;
2765
2766   @b;
2767
2768 }
2769
2770 =item _items_discounts_avail
2771
2772 Returns an array of line item hashrefs representing available term discounts
2773 for this invoice.  This makes the same assumptions that apply to term 
2774 discounts in general: that the package is billed monthly, at a flat rate, 
2775 with no usage charges.  A prorated first month will be handled, as will 
2776 a setup fee if the discount is allowed to apply to setup fees.
2777
2778 =cut
2779
2780 sub _items_discounts_avail {
2781   my $self = shift;
2782
2783   #maybe move this method from cust_bill when quotations support discount_plans 
2784   return () unless $self->can('discount_plans');
2785   my %plans = $self->discount_plans;
2786
2787   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2788   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2789
2790   map {
2791     my $months = $_;
2792     my $plan = $plans{$months};
2793
2794     my $term_total = sprintf('%.2f', $plan->discounted_total);
2795     my $percent = sprintf('%.0f', 
2796                           100 * (1 - $term_total / $plan->base_total) );
2797     my $permonth = sprintf('%.2f', $term_total / $months);
2798     my $detail = $self->mt('discount on item'). ' '.
2799                  join(', ', map { "#$_" } $plan->pkgnums)
2800       if $list_pkgnums;
2801
2802     # discounts for non-integer months don't work anyway
2803     $months = sprintf("%d", $months);
2804
2805     +{
2806       description => $self->mt('Save [_1]% by paying for [_2] months',
2807                                 $percent, $months),
2808       amount      => $self->mt('[_1] ([_2] per month)', 
2809                                 $term_total, $money_char.$permonth),
2810       ext_description => ($detail || ''),
2811     }
2812   } #map
2813   sort { $b <=> $a } keys %plans;
2814
2815 }
2816
2817 1;