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