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