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