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