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