Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / Template_Mixin.pm
1 package FS::Template_Mixin;
2
3 use strict;
4 use vars qw( $DEBUG $me
5              $money_char
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 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           } elsif ( $type eq 'R' ) {
2039             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2040               if $cust_bill_pkg->recur != 0
2041               || $cust_bill_pkg->recur_show_zero;
2042           } elsif ( $type eq 'U' ) {
2043             $subtotal{$locationnum}{$section} += $usage
2044               unless scalar(@extra_sections);
2045           } elsif ( !$type ) {
2046             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2047                                                + $cust_bill_pkg->recur;
2048           }
2049
2050         }
2051
2052       }
2053
2054   }
2055
2056   %pkg_category_cache = ();
2057
2058   # summary invoices need subtotals for all non-disabled package categories,
2059   # even if they're zero
2060   # but currently assume that there are no location sections, or at least
2061   # that the summary page doesn't care about them
2062   if ( $opt{summary} ) {
2063     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2064       $subtotal{''}{$category->categoryname} ||= 0;
2065     }
2066     $subtotal{''}{''} ||= 0;
2067   }
2068
2069   my @sections;
2070   foreach my $post_total (0,1) {
2071     my @these;
2072     my $s = $post_total ? \%late_subtotal : \%subtotal;
2073     foreach my $locationnum (keys %$s) {
2074       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2075         my $section = {
2076                         'subtotal'    => $s->{$locationnum}{$sectionname},
2077                         'post_total'  => $post_total,
2078                         'sort_weight' => 0,
2079                       };
2080         if ( $locationnum ) {
2081           $section->{'locationnum'} = $locationnum;
2082           my $location = FS::cust_location->by_key($locationnum);
2083           $section->{'description'} = &{ $escape }($location->location_label);
2084           # Better ideas? This will roughly group them by proximity, 
2085           # which alpha sorting on any of the address fields won't.
2086           # Sorting by locationnum is meaningless.
2087           # We have to sort on _something_ or the order may change 
2088           # randomly from one invoice to the next, which will confuse
2089           # people.
2090           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2091                                       $locationnum;
2092           $section->{'location'} = {
2093             map { $_ => &{ $escape }($location->get($_)) }
2094             $location->fields
2095           };
2096         } else {
2097           $section->{'category'} = $sectionname;
2098           $section->{'description'} = &{ $escape }($sectionname);
2099           if ( _pkg_category($_) ) {
2100             $section->{'sort_weight'} = _pkg_category($_)->weight;
2101             if ( _pkg_category($_)->condense ) {
2102               $section = { %$section, $self->_condense_section($opt{format}) };
2103             }
2104           }
2105         }
2106         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2107           # then it's a tax-only section
2108           $section->{'summarized'} = 'Y';
2109           $section->{'tax_section'} = 'Y';
2110         }
2111         push @these, $section;
2112       } # foreach $sectionname
2113     } #foreach $locationnum
2114     push @these, @extra_sections if $post_total == 0;
2115     # need an alpha sort for location sections, because postal codes can 
2116     # be non-numeric
2117     $sections[ $post_total ] = [ sort {
2118       $opt{'by_location'} ? 
2119         ($a->{sort_weight} cmp $b->{sort_weight}) :
2120         ($a->{sort_weight} <=> $b->{sort_weight})
2121       } @these ];
2122   } #foreach $post_total
2123
2124   return @sections; # early, late
2125 }
2126
2127 #helper subs for above
2128
2129 sub cust_pkg_hash {
2130   my $self = shift;
2131   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2132 }
2133
2134 sub _pkg_category {
2135   my $categoryname = shift;
2136   $pkg_category_cache{$categoryname} ||=
2137     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2138 }
2139
2140 my %condensed_format = (
2141   'label' => [ qw( Description Qty Amount ) ],
2142   'fields' => [
2143                 sub { shift->{description} },
2144                 sub { shift->{quantity} },
2145                 sub { my($href, %opt) = @_;
2146                       ($opt{dollar} || ''). $href->{amount};
2147                     },
2148               ],
2149   'align'  => [ qw( l r r ) ],
2150   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2151   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2152 );
2153
2154 sub _condense_section {
2155   my ( $self, $format ) = ( shift, shift );
2156   ( 'condensed' => 1,
2157     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2158       qw( description_generator
2159           header_generator
2160           total_generator
2161           total_line_generator
2162         )
2163   );
2164 }
2165
2166 sub _condensed_generator_defaults {
2167   my ( $self, $format ) = ( shift, shift );
2168   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2169 }
2170
2171 my %html_align = (
2172   'c' => 'center',
2173   'l' => 'left',
2174   'r' => 'right',
2175 );
2176
2177 sub _condensed_header_generator {
2178   my ( $self, $format ) = ( shift, shift );
2179
2180   my ( $f, $prefix, $suffix, $separator, $column ) =
2181     _condensed_generator_defaults($format);
2182
2183   if ($format eq 'latex') {
2184     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2185     $suffix = "\\\\\n\\hline";
2186     $separator = "&\n";
2187     $column =
2188       sub { my ($d,$a,$s,$w) = @_;
2189             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2190           };
2191   } elsif ( $format eq 'html' ) {
2192     $prefix = '<th></th>';
2193     $suffix = '';
2194     $separator = '';
2195     $column =
2196       sub { my ($d,$a,$s,$w) = @_;
2197             return qq!<th align="$html_align{$a}">$d</th>!;
2198       };
2199   }
2200
2201   sub {
2202     my @args = @_;
2203     my @result = ();
2204
2205     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2206       push @result,
2207         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2208     }
2209
2210     $prefix. join($separator, @result). $suffix;
2211   };
2212
2213 }
2214
2215 sub _condensed_description_generator {
2216   my ( $self, $format ) = ( shift, shift );
2217
2218   my ( $f, $prefix, $suffix, $separator, $column ) =
2219     _condensed_generator_defaults($format);
2220
2221   my $money_char = '$';
2222   if ($format eq 'latex') {
2223     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2224     $suffix = '\\\\';
2225     $separator = " & \n";
2226     $column =
2227       sub { my ($d,$a,$s,$w) = @_;
2228             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2229           };
2230     $money_char = '\\dollar';
2231   }elsif ( $format eq 'html' ) {
2232     $prefix = '"><td align="center"></td>';
2233     $suffix = '';
2234     $separator = '';
2235     $column =
2236       sub { my ($d,$a,$s,$w) = @_;
2237             return qq!<td align="$html_align{$a}">$d</td>!;
2238       };
2239     #$money_char = $conf->config('money_char') || '$';
2240     $money_char = '';  # this is madness
2241   }
2242
2243   sub {
2244     #my @args = @_;
2245     my $href = shift;
2246     my @result = ();
2247
2248     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2249       my $dollar = '';
2250       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2251       push @result,
2252         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2253                     map { $f->{$_}->[$i] } qw(align span width)
2254                   );
2255     }
2256
2257     $prefix. join( $separator, @result ). $suffix;
2258   };
2259
2260 }
2261
2262 sub _condensed_total_generator {
2263   my ( $self, $format ) = ( shift, shift );
2264
2265   my ( $f, $prefix, $suffix, $separator, $column ) =
2266     _condensed_generator_defaults($format);
2267   my $style = '';
2268
2269   if ($format eq 'latex') {
2270     $prefix = "& ";
2271     $suffix = "\\\\\n";
2272     $separator = " & \n";
2273     $column =
2274       sub { my ($d,$a,$s,$w) = @_;
2275             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2276           };
2277   }elsif ( $format eq 'html' ) {
2278     $prefix = '';
2279     $suffix = '';
2280     $separator = '';
2281     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2282     $column =
2283       sub { my ($d,$a,$s,$w) = @_;
2284             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2285       };
2286   }
2287
2288
2289   sub {
2290     my @args = @_;
2291     my @result = ();
2292
2293     #  my $r = &{$f->{fields}->[$i]}(@args);
2294     #  $r .= ' Total' unless $i;
2295
2296     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2297       push @result,
2298         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2299                     map { $f->{$_}->[$i] } qw(align span width)
2300                   );
2301     }
2302
2303     $prefix. join( $separator, @result ). $suffix;
2304   };
2305
2306 }
2307
2308 =item total_line_generator FORMAT
2309
2310 Returns a coderef used for generation of invoice total line items for this
2311 usage_class.  FORMAT is either html or latex
2312
2313 =cut
2314
2315 # should not be used: will have issues with hash element names (description vs
2316 # total_item and amount vs total_amount -- another array of functions?
2317
2318 sub _condensed_total_line_generator {
2319   my ( $self, $format ) = ( shift, shift );
2320
2321   my ( $f, $prefix, $suffix, $separator, $column ) =
2322     _condensed_generator_defaults($format);
2323   my $style = '';
2324
2325   if ($format eq 'latex') {
2326     $prefix = "& ";
2327     $suffix = "\\\\\n";
2328     $separator = " & \n";
2329     $column =
2330       sub { my ($d,$a,$s,$w) = @_;
2331             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2332           };
2333   }elsif ( $format eq 'html' ) {
2334     $prefix = '';
2335     $suffix = '';
2336     $separator = '';
2337     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2338     $column =
2339       sub { my ($d,$a,$s,$w) = @_;
2340             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2341       };
2342   }
2343
2344
2345   sub {
2346     my @args = @_;
2347     my @result = ();
2348
2349     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2350       push @result,
2351         &{$column}( &{$f->{fields}->[$i]}(@args),
2352                     map { $f->{$_}->[$i] } qw(align span width)
2353                   );
2354     }
2355
2356     $prefix. join( $separator, @result ). $suffix;
2357   };
2358
2359 }
2360
2361 =item _items_pkg [ OPTIONS ]
2362
2363 Return line item hashes for each package item on this invoice. Nearly 
2364 equivalent to 
2365
2366 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2367
2368 The only OPTIONS accepted is 'section', which may point to a hashref 
2369 with a key named 'condensed', which may have a true value.  If it 
2370 does, this method tries to merge identical items into items with 
2371 'quantity' equal to the number of items (not the sum of their 
2372 separate quantities, for some reason).
2373
2374 =cut
2375
2376 sub _items_nontax {
2377   my $self = shift;
2378   # The order of these is important.  Bundled line items will be merged into
2379   # the most recent non-hidden item, so it needs to be the one with:
2380   # - the same pkgnum
2381   # - the same start date
2382   # - no pkgpart_override
2383   #
2384   # So: sort by pkgnum,
2385   # then by sdate
2386   # then sort the base line item before any overrides
2387   # then sort hidden before non-hidden add-ons
2388   # then sort by override pkgpart (for consistency)
2389   sort { $a->pkgnum <=> $b->pkgnum        or
2390          $a->sdate  <=> $b->sdate         or
2391          ($a->pkgpart_override ? 0 : -1)  or
2392          ($b->pkgpart_override ? 0 : 1)   or
2393          $b->hidden cmp $a->hidden        or
2394          $a->pkgpart_override <=> $b->pkgpart_override
2395        }
2396   # and of course exclude taxes and fees
2397   grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2398 }
2399
2400 sub _items_fee {
2401   my $self = shift;
2402   my %options = @_;
2403   my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2404   my @items;
2405   foreach my $cust_bill_pkg (@cust_bill_pkg) {
2406     # cache this, so we don't look it up again in every section
2407     my $part_fee = $cust_bill_pkg->get('part_fee')
2408        || $cust_bill_pkg->part_fee;
2409     $cust_bill_pkg->set('part_fee', $part_fee);
2410     if (!$part_fee) {
2411       #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2412       warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2413       next;
2414     }
2415     if ( exists($options{section}) and exists($options{section}{category}) )
2416     {
2417       my $categoryname = $options{section}{category};
2418       # then filter for items that have that section
2419       if ( $part_fee->categoryname ne $categoryname ) {
2420         warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
2421         next;
2422       }
2423     } # otherwise include them all in the main section
2424     # XXX what to do when sectioning by location?
2425     
2426     my @ext_desc;
2427     my %base_invnums; # invnum => invoice date
2428     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
2429       if ($_->base_invnum) {
2430         my $base_bill = FS::cust_bill->by_key($_->base_invnum);
2431         my $base_date = $self->time2str_local('short', $base_bill->_date)
2432           if $base_bill;
2433         $base_invnums{$_->base_invnum} = $base_date || '';
2434       }
2435     }
2436     foreach (sort keys(%base_invnums)) {
2437       next if $_ == $self->invnum;
2438       push @ext_desc,
2439         $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_});
2440     }
2441     push @items,
2442       { feepart     => $cust_bill_pkg->feepart,
2443         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2444         description => $part_fee->itemdesc_locale($self->cust_main->locale),
2445         ext_description => \@ext_desc
2446         # sdate/edate?
2447       };
2448   }
2449   @items;
2450 }
2451
2452 sub _items_pkg {
2453   my $self = shift;
2454   my %options = @_;
2455
2456   warn "$me _items_pkg searching for all package line items\n"
2457     if $DEBUG > 1;
2458
2459   my @cust_bill_pkg = $self->_items_nontax;
2460
2461   warn "$me _items_pkg filtering line items\n"
2462     if $DEBUG > 1;
2463   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2464
2465   if ($options{section} && $options{section}->{condensed}) {
2466
2467     warn "$me _items_pkg condensing section\n"
2468       if $DEBUG > 1;
2469
2470     my %itemshash = ();
2471     local $Storable::canonical = 1;
2472     foreach ( @items ) {
2473       my $item = { %$_ };
2474       delete $item->{ref};
2475       delete $item->{ext_description};
2476       my $key = freeze($item);
2477       $itemshash{$key} ||= 0;
2478       $itemshash{$key} ++; # += $item->{quantity};
2479     }
2480     @items = sort { $a->{description} cmp $b->{description} }
2481              map { my $i = thaw($_);
2482                    $i->{quantity} = $itemshash{$_};
2483                    $i->{amount} =
2484                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2485                    $i;
2486                  }
2487              keys %itemshash;
2488   }
2489
2490   warn "$me _items_pkg returning ". scalar(@items). " items\n"
2491     if $DEBUG > 1;
2492
2493   @items;
2494 }
2495
2496 sub _taxsort {
2497   return 0 unless $a->itemdesc cmp $b->itemdesc;
2498   return -1 if $b->itemdesc eq 'Tax';
2499   return 1 if $a->itemdesc eq 'Tax';
2500   return -1 if $b->itemdesc eq 'Other surcharges';
2501   return 1 if $a->itemdesc eq 'Other surcharges';
2502   $a->itemdesc cmp $b->itemdesc;
2503 }
2504
2505 sub _items_tax {
2506   my $self = shift;
2507   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
2508     $self->cust_bill_pkg;
2509   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2510
2511   if ( $self->conf->exists('always_show_tax') ) {
2512     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2513     if (0 == grep { $_->{description} eq $itemdesc } @items) {
2514       push @items,
2515         { 'description' => $itemdesc,
2516           'amount'      => 0.00 };
2517     }
2518   }
2519   @items;
2520 }
2521
2522 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2523
2524 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2525 list of hashrefs describing the line items they generate on the invoice.
2526
2527 OPTIONS may include:
2528
2529 format: the invoice format.
2530
2531 escape_function: the function used to escape strings.
2532
2533 DEPRECATED? (expensive, mostly unused?)
2534 format_function: the function used to format CDRs.
2535
2536 section: a hashref containing 'category' and/or 'locationnum'; if this 
2537 is present, only returns line items that belong to that category and/or
2538 location (whichever is defined).
2539
2540 multisection: a flag indicating that this is a multisection invoice,
2541 which does something complicated.
2542
2543 Returns a list of hashrefs, each of which may contain:
2544
2545 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
2546 ext_description, which is an arrayref of detail lines to show below 
2547 the package line.
2548
2549 =cut
2550
2551 sub _items_cust_bill_pkg {
2552   my $self = shift;
2553   my $conf = $self->conf;
2554   my $cust_bill_pkgs = shift;
2555   my %opt = @_;
2556
2557   my $format = $opt{format} || '';
2558   my $escape_function = $opt{escape_function} || sub { shift };
2559   my $format_function = $opt{format_function} || '';
2560   my $no_usage = $opt{no_usage} || '';
2561   my $unsquelched = $opt{unsquelched} || ''; #unused
2562   my ($section, $locationnum, $category);
2563   if ( $opt{section} ) {
2564     $category = $opt{section}->{category};
2565     $locationnum = $opt{section}->{locationnum};
2566   }
2567   my $summary_page = $opt{summary_page} || ''; #unused
2568   my $multisection = defined($category) || defined($locationnum);
2569   my $discount_show_always = 0;
2570
2571   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2572
2573   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2574                                    # and location labels
2575
2576   my @b = ();
2577   my ($s, $r, $u) = ( undef, undef, undef );
2578   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2579   {
2580
2581     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2582       if ( $_ && !$cust_bill_pkg->hidden ) {
2583         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2584         $_->{amount}      =~ s/^\-0\.00$/0.00/;
2585         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2586         push @b, { %$_ }
2587           if $_->{amount} != 0
2588           || $discount_show_always
2589           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2590           || (   $_->{_is_setup} && $_->{setup_show_zero} )
2591         ;
2592         $_ = undef;
2593       }
2594     }
2595
2596     if ( $locationnum ) {
2597       # this is a location section; skip packages that aren't at this
2598       # service location.
2599       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
2600       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
2601               != $locationnum;
2602     }
2603
2604     # Consider display records for this item to determine if it belongs
2605     # in this section.  Note that if there are no display records, there
2606     # will be a default pseudo-record that includes all charge types 
2607     # and has no section name.
2608     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2609                                   ? $cust_bill_pkg->cust_bill_pkg_display
2610                                   : ( $cust_bill_pkg );
2611
2612     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2613          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2614       if $DEBUG > 1;
2615
2616     if ( defined($category) ) {
2617       # then this is a package category section; process all display records
2618       # that belong to this section.
2619       @cust_bill_pkg_display = grep { $_->section eq $category }
2620                                 @cust_bill_pkg_display;
2621     } else {
2622       # otherwise, process all display records that aren't usage summaries
2623       # (I don't think there should be usage summaries if you aren't using 
2624       # category sections, but this is the historical behavior)
2625       @cust_bill_pkg_display = grep { !$_->summary }
2626                                 @cust_bill_pkg_display;
2627     }
2628
2629     my $classname = ''; # package class name, will fill in later
2630
2631     foreach my $display (@cust_bill_pkg_display) {
2632
2633       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2634            $display->billpkgdisplaynum. "\n"
2635         if $DEBUG > 1;
2636
2637       my $type = $display->type;
2638
2639       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2640       $desc = substr($desc, 0, $maxlength). '...'
2641         if $format eq 'latex' && length($desc) > $maxlength;
2642
2643       my %details_opt = ( 'format'          => $format,
2644                           'escape_function' => $escape_function,
2645                           'format_function' => $format_function,
2646                           'no_usage'        => $opt{'no_usage'},
2647                         );
2648
2649       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2650
2651         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2652           if $DEBUG > 1;
2653         # quotation_pkgs are never fees, so don't worry about the case where
2654         # part_pkg is undefined
2655
2656         if ( $cust_bill_pkg->setup != 0 ) {
2657           my $description = $desc;
2658           $description .= ' Setup'
2659             if $cust_bill_pkg->recur != 0
2660             || $discount_show_always
2661             || $cust_bill_pkg->recur_show_zero;
2662           push @b, {
2663             'description' => $description,
2664             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2665           };
2666         }
2667         if ( $cust_bill_pkg->recur != 0 ) {
2668           push @b, {
2669             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2670             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2671           };
2672         }
2673
2674       } elsif ( $cust_bill_pkg->pkgnum > 0 ) { # and it's not a quotation_pkg
2675
2676         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2677           if $DEBUG > 1;
2678  
2679         my $cust_pkg = $cust_bill_pkg->cust_pkg;
2680         my $part_pkg = $cust_pkg->part_pkg;
2681
2682         # which pkgpart to show for display purposes?
2683         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2684
2685         # start/end dates for invoice formats that do nonstandard 
2686         # things with them
2687         my %item_dates = ();
2688         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2689           unless $part_pkg->option('disable_line_item_date_ranges',1);
2690
2691         # not normally used, but pass this to the template anyway
2692         $classname = $part_pkg->classname;
2693
2694         if (    (!$type || $type eq 'S')
2695              && (    $cust_bill_pkg->setup != 0
2696                   || $cust_bill_pkg->setup_show_zero
2697                 )
2698            )
2699          {
2700
2701           warn "$me _items_cust_bill_pkg adding setup\n"
2702             if $DEBUG > 1;
2703
2704           my $description = $desc;
2705           $description .= ' Setup'
2706             if $cust_bill_pkg->recur != 0
2707             || $discount_show_always
2708             || $cust_bill_pkg->recur_show_zero;
2709
2710           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2711                                                               $self->agentnum )
2712             if $part_pkg->is_prepaid #for prepaid, "display the validity period
2713                                      # triggered by the recurring charge freq
2714                                      # (RT#26274)
2715             && $cust_bill_pkg->recur == 0
2716             && ! $cust_bill_pkg->recur_show_zero;
2717
2718           my @d = ();
2719           my $svc_label;
2720
2721           # always pass the svc_label through to the template, even if 
2722           # not displaying it as an ext_description
2723           my @svc_labels = map &{$escape_function}($_),
2724                       $cust_pkg->h_labels_short($self->_date, undef, 'I');
2725
2726           $svc_label = $svc_labels[0];
2727
2728           unless ( $cust_pkg->part_pkg->hide_svc_detail
2729                 || $cust_bill_pkg->hidden )
2730           {
2731
2732             push @d, @svc_labels
2733               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2734             my $lnum = $cust_main ? $cust_main->ship_locationnum
2735                                   : $self->prospect_main->locationnum;
2736             # show the location label if it's not the customer's default
2737             # location, and we're not grouping items by location already
2738             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2739               my $loc = $cust_pkg->location_label;
2740               $loc = substr($loc, 0, $maxlength). '...'
2741                 if $format eq 'latex' && length($loc) > $maxlength;
2742               push @d, &{$escape_function}($loc);
2743             }
2744
2745           } #unless hiding service details
2746
2747           push @d, $cust_bill_pkg->details(%details_opt)
2748             if $cust_bill_pkg->recur == 0;
2749
2750           if ( $cust_bill_pkg->hidden ) {
2751             $s->{amount}      += $cust_bill_pkg->setup;
2752             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2753             push @{ $s->{ext_description} }, @d;
2754           } else {
2755             $s = {
2756               _is_setup       => 1,
2757               description     => $description,
2758               pkgpart         => $pkgpart,
2759               pkgnum          => $cust_bill_pkg->pkgnum,
2760               amount          => $cust_bill_pkg->setup,
2761               setup_show_zero => $cust_bill_pkg->setup_show_zero,
2762               unit_amount     => $cust_bill_pkg->unitsetup,
2763               quantity        => $cust_bill_pkg->quantity,
2764               ext_description => \@d,
2765               svc_label       => ($svc_label || ''),
2766               locationnum     => $cust_pkg->locationnum, # sure, why not?
2767             };
2768           };
2769
2770         }
2771
2772         if (    ( !$type || $type eq 'R' || $type eq 'U' )
2773              && (
2774                      $cust_bill_pkg->recur != 0
2775                   || $cust_bill_pkg->setup == 0
2776                   || $discount_show_always
2777                   || $cust_bill_pkg->recur_show_zero
2778                 )
2779            )
2780         {
2781
2782           warn "$me _items_cust_bill_pkg adding recur/usage\n"
2783             if $DEBUG > 1;
2784
2785           my $is_summary = $display->summary;
2786           my $description = $desc;
2787           if ( $type eq 'U' and defined($r) ) {
2788             # don't just show the same description as the recur line
2789             $description = $self->mt('Usage charges');
2790           }
2791
2792           my $part_pkg = $cust_pkg->part_pkg;
2793
2794           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2795                                                               $self->agentnum );
2796
2797           my @d = ();
2798           my @seconds = (); # for display of usage info
2799           my $svc_label = '';
2800
2801           #at least until cust_bill_pkg has "past" ranges in addition to
2802           #the "future" sdate/edate ones... see #3032
2803           my @dates = ( $self->_date );
2804           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2805           push @dates, $prev->sdate if $prev;
2806           push @dates, undef if !$prev;
2807
2808           my @svc_labels = map &{$escape_function}($_),
2809                       $cust_pkg->h_labels_short(@dates, 'I');
2810           $svc_label = $svc_labels[0];
2811
2812           # show service labels, unless...
2813                     # the package is set not to display them
2814           unless ( $part_pkg->hide_svc_detail
2815                     # or this is a tax-like line item
2816                 || $cust_bill_pkg->itemdesc
2817                     # or this is a hidden (bundled) line item
2818                 || $cust_bill_pkg->hidden
2819                     # or this is a usage summary line
2820                 || $is_summary && $type && $type eq 'U'
2821                     # or this is a usage line and there's a recurring line
2822                     # for the package in the same section (which will 
2823                     # have service labels already)
2824                 || ($type eq 'U' and defined($r))
2825               )
2826           {
2827
2828             warn "$me _items_cust_bill_pkg adding service details\n"
2829               if $DEBUG > 1;
2830
2831             push @d, @svc_labels
2832               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2833             warn "$me _items_cust_bill_pkg done adding service details\n"
2834               if $DEBUG > 1;
2835
2836             my $lnum = $cust_main ? $cust_main->ship_locationnum
2837                                   : $self->prospect_main->locationnum;
2838             # show the location label if it's not the customer's default
2839             # location, and we're not grouping items by location already
2840             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2841               my $loc = $cust_pkg->location_label;
2842               $loc = substr($loc, 0, $maxlength). '...'
2843                 if $format eq 'latex' && length($loc) > $maxlength;
2844               push @d, &{$escape_function}($loc);
2845             }
2846
2847             # Display of seconds_since_sqlradacct:
2848             # On the invoice, when processing @detail_items, look for a field
2849             # named 'seconds'.  This will contain total seconds for each 
2850             # service, in the same order as @ext_description.  For services 
2851             # that don't support this it will show undef.
2852             if ( $conf->exists('svc_acct-usage_seconds') 
2853                  and ! $cust_bill_pkg->pkgpart_override ) {
2854               foreach my $cust_svc ( 
2855                   $cust_pkg->h_cust_svc(@dates, 'I') 
2856                 ) {
2857
2858                 # eval because not having any part_export_usage exports 
2859                 # is a fatal error, last_bill/_date because that's how 
2860                 # sqlradius_hour billing does it
2861                 my $sec = eval {
2862                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2863                 };
2864                 push @seconds, $sec;
2865               }
2866             } #if svc_acct-usage_seconds
2867
2868           } # if we are showing service labels
2869
2870           unless ( $is_summary ) {
2871             warn "$me _items_cust_bill_pkg adding details\n"
2872               if $DEBUG > 1;
2873
2874             #instead of omitting details entirely in this case (unwanted side
2875             # effects), just omit CDRs
2876             $details_opt{'no_usage'} = 1
2877               if $type && $type eq 'R';
2878
2879             push @d, $cust_bill_pkg->details(%details_opt);
2880           }
2881
2882           warn "$me _items_cust_bill_pkg calculating amount\n"
2883             if $DEBUG > 1;
2884   
2885           my $amount = 0;
2886           if (!$type) {
2887             $amount = $cust_bill_pkg->recur;
2888           } elsif ($type eq 'R') {
2889             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2890           } elsif ($type eq 'U') {
2891             $amount = $cust_bill_pkg->usage;
2892           }
2893   
2894           if ( !$type || $type eq 'R' ) {
2895
2896             warn "$me _items_cust_bill_pkg adding recur\n"
2897               if $DEBUG > 1;
2898
2899             my $unit_amount =
2900               ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2901                                                 : $amount;
2902
2903             if ( $cust_bill_pkg->hidden ) {
2904               $r->{amount}      += $amount;
2905               $r->{unit_amount} += $unit_amount;
2906               push @{ $r->{ext_description} }, @d;
2907             } else {
2908               $r = {
2909                 description     => $description,
2910                 pkgpart         => $pkgpart,
2911                 pkgnum          => $cust_bill_pkg->pkgnum,
2912                 amount          => $amount,
2913                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2914                 unit_amount     => $unit_amount,
2915                 quantity        => $cust_bill_pkg->quantity,
2916                 %item_dates,
2917                 ext_description => \@d,
2918                 svc_label       => ($svc_label || ''),
2919                 locationnum     => $cust_pkg->locationnum,
2920               };
2921               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2922             }
2923
2924           } else {  # $type eq 'U'
2925
2926             warn "$me _items_cust_bill_pkg adding usage\n"
2927               if $DEBUG > 1;
2928
2929             if ( $cust_bill_pkg->hidden and defined($u) ) {
2930               # if this is a hidden package and there's already a usage
2931               # line for the bundle, add this package's total amount and
2932               # usage details to it
2933               $u->{amount}      += $amount;
2934               push @{ $u->{ext_description} }, @d;
2935             } elsif ( $amount ) {
2936               # create a new usage line
2937               $u = {
2938                 description     => $description,
2939                 pkgpart         => $pkgpart,
2940                 pkgnum          => $cust_bill_pkg->pkgnum,
2941                 amount          => $amount,
2942                 usage_item      => 1,
2943                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2944                 %item_dates,
2945                 ext_description => \@d,
2946                 locationnum     => $cust_pkg->locationnum,
2947               };
2948             } # else this has no usage, so don't create a usage section
2949           }
2950
2951         } # recurring or usage with recurring charge
2952
2953       } else { # taxes and fees
2954
2955         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2956           if $DEBUG > 1;
2957
2958         # items of this kind should normally not have sdate/edate.
2959         push @b, {
2960           'description' => $desc,
2961           'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
2962                                            + $cust_bill_pkg->recur)
2963         };
2964
2965       } # if quotation / package line item / other line item
2966
2967     } # foreach $display
2968
2969     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2970                                 && $conf->exists('discount-show-always'));
2971
2972   }
2973
2974   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2975     if ( $_  ) {
2976       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2977         if exists($_->{amount});
2978       $_->{amount}      =~ s/^\-0\.00$/0.00/;
2979       $_->{unit_amount} = sprintf('%.2f', $_->{unit_amount})
2980         if exists($_->{unit_amount});
2981
2982       push @b, { %$_ }
2983         if $_->{amount} != 0
2984         || $discount_show_always
2985         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2986         || (   $_->{_is_setup} && $_->{setup_show_zero} )
2987     }
2988   }
2989
2990   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2991     if $DEBUG > 1;
2992
2993   @b;
2994
2995 }
2996
2997 =item _items_discounts_avail
2998
2999 Returns an array of line item hashrefs representing available term discounts
3000 for this invoice.  This makes the same assumptions that apply to term 
3001 discounts in general: that the package is billed monthly, at a flat rate, 
3002 with no usage charges.  A prorated first month will be handled, as will 
3003 a setup fee if the discount is allowed to apply to setup fees.
3004
3005 =cut
3006
3007 sub _items_discounts_avail {
3008   my $self = shift;
3009
3010   #maybe move this method from cust_bill when quotations support discount_plans 
3011   return () unless $self->can('discount_plans');
3012   my %plans = $self->discount_plans;
3013
3014   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3015   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3016
3017   map {
3018     my $months = $_;
3019     my $plan = $plans{$months};
3020
3021     my $term_total = sprintf('%.2f', $plan->discounted_total);
3022     my $percent = sprintf('%.0f', 
3023                           100 * (1 - $term_total / $plan->base_total) );
3024     my $permonth = sprintf('%.2f', $term_total / $months);
3025     my $detail = $self->mt('discount on item'). ' '.
3026                  join(', ', map { "#$_" } $plan->pkgnums)
3027       if $list_pkgnums;
3028
3029     # discounts for non-integer months don't work anyway
3030     $months = sprintf("%d", $months);
3031
3032     +{
3033       description => $self->mt('Save [_1]% by paying for [_2] months',
3034                                 $percent, $months),
3035       amount      => $self->mt('[_1] ([_2] per month)', 
3036                                 $term_total, $money_char.$permonth),
3037       ext_description => ($detail || ''),
3038     }
3039   } #map
3040   sort { $b <=> $a } keys %plans;
3041
3042 }
3043
3044 1;