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