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