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