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