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