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