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