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