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