RT# 81961 Repair broken links in POD documentation
[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); #can't import first, it conflicts with cust_main.first
11 use Date::Format;
12 use Date::Language;
13 use Time::Local qw( timelocal );
14 use Text::Template 1.20;
15 use File::Temp 0.14;
16 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
17 use IO::Scalar;
18 use HTML::Entities;
19 use Cwd;
20 use FS::UID;
21 use FS::Misc qw( send_email );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::Conf;
24 use FS::Misc qw( generate_ps generate_pdf );
25 use FS::pkg_category;
26 use FS::pkg_class;
27 use FS::invoice_mode;
28 use FS::L10N;
29 use FS::Log;
30
31 $DEBUG = 0;
32 $me = '[FS::Template_Mixin]';
33 FS::UID->install_callback( sub { 
34   my $conf = new FS::Conf; #global
35   $money_char  = $conf->config('money_char')  || '$';  
36   $date_format = $conf->config('date_format') || '%x'; #/YY
37 } );
38
39 =item conf [ MODE ]
40
41 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
42
43 If the "mode" pseudo-field is set on the object, the configuration handle
44 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
45 locale).
46
47 =cut
48
49 sub conf {
50   my $self = shift;
51   my $mode = $self->get('mode');
52   if ($self->{_conf} and !defined($mode)) {
53     return $self->{_conf};
54   }
55
56   my $cust_main = $self->cust_main;
57   my $locale = $cust_main ? $cust_main->locale : '';
58   my $conf;
59   if ( $mode ) {
60     if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
61       $mode = $mode->modenum;
62     } elsif ( $mode =~ /\D/ ) {
63       die "invalid invoice mode $mode";
64     }
65     $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
66     if (!$conf) {
67       $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
68       # it doesn't have a locale, but system conf still might
69       $conf->set('locale' => $locale) if $conf;
70     }
71   }
72   # if $mode is unspecified, or if there is no invoice_conf matching this mode
73   # and locale, then use the system config only (but with the locale)
74   $conf ||= FS::Conf->new({ 'locale' => $locale });
75   # cache it
76   return $self->{_conf} = $conf;
77 }
78
79 =item print_text OPTIONS
80
81 Returns an text invoice, as a list of lines.
82
83 Options can be passed as a hash.
84
85 I<time>, if specified, is used to control the printing of overdue messages.  The
86 default is now.  It isn't the date of the invoice; that's the `_date' field.
87 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
88 L<Time::Local> and L<Date::Parse> for conversion functions.
89
90 I<template>, if specified, is the name of a suffix for alternate invoices.
91
92 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
93
94 =cut
95
96 sub print_text {
97   my $self = shift;
98   my %params;
99   if ( ref($_[0]) ) {
100     %params = %{ shift() };
101   } else {
102     %params = @_;
103   }
104
105   $params{'format'} = 'template'; # for some reason
106
107   $self->print_generic( %params );
108 }
109
110 =item print_latex HASHREF
111
112 Internal method - returns a filename of a filled-in LaTeX template for this
113 invoice (Note: add ".tex" to get the actual filename), and a filename of
114 an associated logo (with the .eps extension included).
115
116 See print_ps and print_pdf for methods that return PostScript and PDF output.
117
118 Options can be passed as a hash.
119
120 I<time>, if specified, is used to control the printing of overdue messages.  The
121 default is now.  It isn't the date of the invoice; that's the `_date' field.
122 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
123 L<Time::Local> and L<Date::Parse> for conversion functions.
124
125 I<template>, if specified, is the name of a suffix for alternate invoices.  
126 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
127 customize invoice templates for different purposes.
128
129 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
130
131 =cut
132
133 sub print_latex {
134   my $self = shift;
135   my %params;
136
137   if ( ref($_[0]) ) {
138     %params = %{ shift() };
139   } else {
140     %params = @_;
141   }
142
143   $params{'format'} = 'latex';
144   my $conf = $self->conf;
145
146   # this needs to go away
147   my $template = $params{'template'};
148   # and this especially
149   $template ||= $self->_agent_template
150     if $self->can('_agent_template');
151
152   #the new way
153   $self->set('mode', $params{mode})
154     if $params{mode};
155
156   my $pkey = $self->primary_key;
157   my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
158
159   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
160   my $lh = new File::Temp(
161     TEMPLATE => $tmp_template,
162     DIR      => $dir,
163     SUFFIX   => '.eps',
164     UNLINK   => 0,
165   ) or die "can't open temp file: $!\n";
166
167   my $agentnum = $self->agentnum;
168
169   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
170     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
171       or die "can't write temp file: $!\n";
172   } else {
173     print $lh $conf->config_binary('logo.eps', $agentnum)
174       or die "can't write temp file: $!\n";
175   }
176   close $lh;
177   $params{'logo_file'} = $lh->filename;
178
179   if( $conf->exists('invoice-barcode') 
180         && $self->can('invoice_barcode')
181         && $self->invnum ) { # don't try to barcode statements
182       my $png_file = $self->invoice_barcode($dir);
183       my $eps_file = $png_file;
184       $eps_file =~ s/\.png$/.eps/g;
185       $png_file =~ /(barcode.*png)/;
186       $png_file = $1;
187       $eps_file =~ /(barcode.*eps)/;
188       $eps_file = $1;
189
190       my $curr_dir = cwd();
191       chdir($dir); 
192       # after painfuly long experimentation, it was determined that sam2p won't
193       # accept : and other chars in the path, no matter how hard I tried to
194       # escape them, hence the chdir (and chdir back, just to be safe)
195       system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
196         or die "sam2p failed: $!\n";
197       unlink($png_file);
198       chdir($curr_dir);
199
200       $params{'barcode_file'} = $eps_file;
201   }
202
203   my @filled_in = $self->print_generic( %params );
204   
205   my $fh = new File::Temp( TEMPLATE => $tmp_template,
206                            DIR      => $dir,
207                            SUFFIX   => '.tex',
208                            UNLINK   => 0,
209                          ) or die "can't open temp file: $!\n";
210   binmode($fh, ':utf8'); # language support
211   print $fh join('', @filled_in );
212   close $fh;
213
214   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
215   return ($1, $params{'logo_file'}, $params{'barcode_file'});
216
217 }
218
219 sub agentnum {
220   my $self = shift;
221   my $cust_main = $self->cust_main;
222   $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
223 }
224
225 =item print_generic OPTION => VALUE ...
226
227 Internal method - returns a filled-in template for this invoice as a scalar.
228
229 See print_ps and print_pdf for methods that return PostScript and PDF output.
230
231 Required options
232
233 =over 4
234
235 =item format
236
237 The B<format> option is required and should be set to html, latex (print and PDF) or template (plaintext).
238
239 =back
240
241 Additional options
242
243 =over 4
244
245 =item notice_name
246
247 Overrides "Invoice" as the name of the sent document.
248
249 =item today
250
251 Used to control the printing of overdue messages.  The
252 default is now.  It isn't the date of the invoice; that's the `_date' field.
253 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
254 L<Time::Local> and L<Date::Parse> for conversion functions.
255
256 =item logo_file
257
258 Logo file (path to temporary EPS file on the local filesystem)
259
260 =item cid
261
262 CID for inline (emailed) images (logo)
263
264 =item locale
265
266 Override customer's locale
267
268 =item unsquelch_cdr
269
270 Overrides any per customer cdr squelching when true
271
272 =item no_number
273
274 Supress the (invoice, quotation, statement, etc.) number
275
276 =item no_date
277
278 Supress the date
279
280 =item no_coupon
281
282 Supress the payment coupon
283
284 =item barcode_file
285
286 Barcode file (path to temporary EPS file on the local filesystem)
287
288 =item barcode_img
289
290 Flag indicating the barcode image should be a link (normal HTML dipaly)
291
292 =item barcode_cid
293
294 Barcode CID for inline (emailed) images
295
296 =item preref_callback
297
298 Coderef run for each line item, code should return HTML to be displayed
299 before that line item (quotations only)
300
301 =item template
302
303 Deprecated.  Used as a suffix for a configuration template.  Please
304 don't use this, it deprecated in favor of more flexible alternatives.
305
306 =back
307
308 =cut
309
310 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
311 # (alignment in text invoice?) problems to change them all to '%.2f' ?
312 # yes: fixed width/plain text printing will be borked
313 sub print_generic {
314   my( $self, %params ) = @_;
315   my $conf = $self->conf;
316
317   my $today = $params{today} ? $params{today} : time;
318   warn "$me print_generic called on $self with suffix $params{template}\n"
319     if $DEBUG;
320
321   my $format = $params{format};
322   die "Unknown format: $format"
323     unless $format =~ /^(latex|html|template)$/;
324
325   my $cust_main = $self->cust_main || $self->prospect_main;
326   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
327     unless $cust_main->payname
328         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
329
330   my $locale = $params{'locale'} || $cust_main->locale;
331
332   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
333                      'html'     => [ '<%=', '%>' ],
334                      'template' => [ '{', '}' ],
335                    );
336
337   warn "$me print_generic creating template\n"
338     if $DEBUG > 1;
339
340   # set the notice name here, and nowhere else.
341   my $notice_name =  $params{notice_name}
342                   || $conf->config('notice_name')
343                   || $self->notice_name;
344
345   #create the template
346   my $template = $params{template} ? $params{template} : $self->_agent_template;
347   my $templatefile = $self->template_conf. $format;
348   $templatefile .= "_$template"
349     if length($template) && $conf->exists($templatefile."_$template");
350
351   # the base template
352   my @invoice_template = map "$_\n", $conf->config($templatefile)
353     or die "cannot load config data $templatefile";
354
355   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
356     #change this to a die when the old code is removed
357     # it's been almost ten years, changing it to a die on the next release.
358     warn "old-style invoice template $templatefile; ".
359          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
360          #$old_latex = 'true';
361          #@invoice_template = _translate_old_latex_format(@invoice_template);
362   } 
363
364   warn "$me print_generic creating T:T object\n"
365     if $DEBUG > 1;
366
367   my $text_template = new Text::Template(
368     TYPE => 'ARRAY',
369     SOURCE => \@invoice_template,
370     DELIMITERS => $delimiters{$format},
371   );
372
373   warn "$me print_generic compiling T:T object\n"
374     if $DEBUG > 1;
375
376   $text_template->compile()
377     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
378
379
380   # additional substitution could possibly cause breakage in existing templates
381   my %convert_maps = ( 
382     'latex' => {
383                  'notes'         => sub { map "$_", @_ },
384                  'footer'        => sub { map "$_", @_ },
385                  'smallfooter'   => sub { map "$_", @_ },
386                  'returnaddress' => sub { map "$_", @_ },
387                  'coupon'        => sub { map "$_", @_ },
388                  'summary'       => sub { map "$_", @_ },
389                },
390     'html'  => {
391                  'notes' =>
392                    sub {
393                      map { 
394                        s/%%(.*)$/<!-- $1 -->/g;
395                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
396                        s/\\begin\{enumerate\}/<ol>/g;
397                        s/\\item /  <li>/g;
398                        s/\\end\{enumerate\}/<\/ol>/g;
399                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
400                        s/\\\\\*/<br>/g;
401                        s/\\dollar ?/\$/g;
402                        s/\\#/#/g;
403                        s/~/&nbsp;/g;
404                        $_;
405                      }  @_
406                    },
407                  'footer' =>
408                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
409                  'smallfooter' =>
410                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
411                  'returnaddress' =>
412                    sub {
413                      map { 
414                        s/~/&nbsp;/g;
415                        s/\\\\\*?\s*$/<BR>/;
416                        s/\\hyphenation\{[\w\s\-]+}//;
417                        s/\\([&])/$1/g;
418                        $_;
419                      }  @_
420                    },
421                  'coupon'        => sub { "" },
422                  'summary'       => sub { "" },
423                },
424     'template' => {
425                  'notes' =>
426                    sub {
427                      map { 
428                        s/%%.*$//g;
429                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
430                        s/\\begin\{enumerate\}//g;
431                        s/\\item /  * /g;
432                        s/\\end\{enumerate\}//g;
433                        s/\\textbf\{(.*)\}/$1/g;
434                        s/\\\\\*/ /;
435                        s/\\dollar ?/\$/g;
436                        $_;
437                      }  @_
438                    },
439                  'footer' =>
440                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
441                  'smallfooter' =>
442                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
443                  'returnaddress' =>
444                    sub {
445                      map { 
446                        s/~/ /g;
447                        s/\\\\\*?\s*$/\n/;             # dubious
448                        s/\\hyphenation\{[\w\s\-]+}//;
449                        $_;
450                      }  @_
451                    },
452                  'coupon'        => sub { "" },
453                  'summary'       => sub { "" },
454                },
455   );
456
457
458   # hashes for differing output formats
459   my %nbsps = ( 'latex'    => '~',
460                 'html'     => '',    # '&nbps;' would be nice
461                 'template' => '',    # not used
462               );
463   my $nbsp = $nbsps{$format};
464
465   my %escape_functions = ( 'latex'    => \&_latex_escape,
466                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
467                            'template' => sub { shift },
468                          );
469   my $escape_function = $escape_functions{$format};
470   my $escape_function_nonbsp = ($format eq 'html')
471                                  ? \&_html_escape : $escape_function;
472
473   my %newline_tokens = (  'latex'     => '\\\\',
474                           'html'      => '<br>',
475                           'template'  => "\n",
476                         );
477   my $newline_token = $newline_tokens{$format};
478
479   warn "$me generating template variables\n"
480     if $DEBUG > 1;
481
482   # generate template variables
483   my $returnaddress;
484
485   if (
486          defined( $conf->config_orbase( "invoice_${format}returnaddress",
487                                         $template
488                                       )
489                 )
490        && length( $conf->config_orbase( "invoice_${format}returnaddress",
491                                         $template
492                                       )
493                 )
494   ) {
495
496     $returnaddress = join("\n",
497       $conf->config_orbase("invoice_${format}returnaddress", $template)
498     );
499
500   } elsif ( grep /\S/,
501             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
502
503     my $convert_map = $convert_maps{$format}{'returnaddress'};
504     $returnaddress =
505       join( "\n",
506             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
507                                                  $template
508                                                )
509                          )
510           );
511   } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
512
513     my $convert_map = $convert_maps{$format}{'returnaddress'};
514     $returnaddress = join( "\n", &$convert_map(
515                                    map { s/( {2,})/'~' x length($1)/eg;
516                                          s/$/\\\\\*/;
517                                          $_
518                                        }
519                                      ( $conf->config('company_name', $cust_main->agentnum),
520                                        $conf->config('company_address', $cust_main->agentnum),
521                                      )
522                                  )
523                      );
524
525   } else {
526
527     my $warning = "Couldn't find a return address; ".
528                   "do you need to set the company_address configuration value?";
529     warn "$warning\n";
530     $returnaddress = $nbsp;
531     #$returnaddress = $warning;
532
533   }
534
535   warn "$me generating invoice data\n"
536     if $DEBUG > 1;
537
538   my $agentnum = $cust_main->agentnum;
539
540   my %invoice_data = (
541
542     #invoice from info
543     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
544     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
545     'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
546     'returnaddress'   => $returnaddress,
547     'agent'           => &$escape_function($cust_main->agent->agent),
548
549     #invoice/quotation info
550     'no_number'       => $params{'no_number'},
551     'invnum'          => ( $params{'no_number'} ? '' : $self->invnum ),
552     'quotationnum'    => $self->quotationnum,
553     'no_date'         => $params{'no_date'},
554     '_date'           => ( $params{'no_date'} ? '' : $self->_date ),
555       # workaround for inconsistent behavior in the early plain text 
556       # templates; see RT#28271
557     'date'            => ( $params{'no_date'}
558                              ? ''
559                              : ($format eq 'template'
560                                ? $self->_date
561                                : $self->time2str_local('long', $self->_date, $format)
562                                )
563                          ),
564     'today'           => $self->time2str_local('long', $today, $format),
565     'terms'           => $self->terms,
566     'template'        => $template, #params{'template'},
567     'notice_name'     => $notice_name, # escape?
568     'current_charges' => sprintf("%.2f", $self->charged),
569     'duedate'         => $self->due_date2str('rdate'), #date_format?
570     'duedate_long'    => $self->due_date2str('long'),
571
572     #customer info
573     'custnum'         => $cust_main->display_custnum,
574     'prospectnum'     => $cust_main->prospectnum,
575     'agent_custid'    => &$escape_function($cust_main->agent_custid),
576     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
577       payname company address1 address2 city state zip fax
578     )),
579
580     #global config
581     'ship_enable'     => $cust_main->invoice_ship_address || $conf->exists('invoice-ship_address'),
582     'unitprices'      => $conf->exists('invoice-unitprice'),
583     'smallernotes'    => $conf->exists('invoice-smallernotes'),
584     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
585     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
586    
587     #layout info -- would be fancy to calc some of this and bury the template
588     #               here in the code
589     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
590     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
591     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
592     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
593     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
594     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
595     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
596     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
597     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
598     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
599
600     # better hang on to conf_dir for a while (for old templates)
601     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
602
603     #these are only used when doing paged plaintext
604     'page'            => 1,
605     'total_pages'     => 1,
606
607   );
608  
609   #localization
610   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
611   # prototype here to silence warnings
612   $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
613
614   my $min_sdate = 999999999999;
615   my $max_edate = 0;
616   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
617     next unless $cust_bill_pkg->pkgnum > 0;
618     $min_sdate = $cust_bill_pkg->sdate
619       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
620     $max_edate = $cust_bill_pkg->edate
621       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
622   }
623
624   $invoice_data{'bill_period'} = '';
625   $invoice_data{'bill_period'} =
626       $self->time2str_local('%e %h', $min_sdate, $format) 
627       . " to " .
628       $self->time2str_local('%e %h', $max_edate, $format)
629     if ($max_edate != 0 && $min_sdate != 999999999999);
630
631   $invoice_data{finance_section} = '';
632   if ( $conf->config('finance_pkgclass') ) {
633     my $pkg_class =
634       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
635     $invoice_data{finance_section} = $pkg_class->categoryname;
636   } 
637   $invoice_data{finance_amount} = '0.00';
638   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
639
640   my $countrydefault = $conf->config('countrydefault') || 'US';
641   foreach ( qw( address1 address2 city state zip country fax) ){
642     my $method = 'ship_'.$_;
643     $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
644   }
645   if ( length($cust_main->ship_company) ) {
646     $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
647   } else {
648     $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
649   }
650   $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
651   $invoice_data{'ship_country'} = ''
652     if ( $invoice_data{'ship_country'} eq $countrydefault );
653   
654   $invoice_data{'cid'} = $params{'cid'}
655     if $params{'cid'};
656
657   if ( $cust_main->country eq $countrydefault ) {
658     $invoice_data{'country'} = '';
659   } else {
660     $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
661   }
662
663   my @address = ();
664   $invoice_data{'address'} = \@address;
665   push @address,
666     $cust_main->payname.
667       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
668         ? " (P.O. #". $cust_main->payinfo. ")"
669         : ''
670       )
671   ;
672   push @address, $cust_main->company
673     if $cust_main->company;
674   push @address, $cust_main->address1;
675   push @address, $cust_main->address2
676     if $cust_main->address2;
677   push @address,
678     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
679   push @address, $invoice_data{'country'}
680     if $invoice_data{'country'};
681   push @address, ''
682     while (scalar(@address) < 5);
683
684   $invoice_data{'logo_file'} = $params{'logo_file'}
685     if $params{'logo_file'};
686   $invoice_data{'barcode_file'} = $params{'barcode_file'}
687     if $params{'barcode_file'};
688   $invoice_data{'barcode_img'} = $params{'barcode_img'}
689     if $params{'barcode_img'};
690   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
691     if $params{'barcode_cid'};
692
693   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
694 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
695   #my $balance_due = $self->owed + $pr_total - $cr_total;
696   my $balance_due = $self->owed;
697   if ( $self->enable_previous ) {
698     $balance_due += $pr_total;
699   }
700   # otherwise the previous balance is not shown, so including it in the
701   # balance due is just confusing
702
703   # the sum of amount owed on all invoices
704   # (this is used in the summary & on the payment coupon)
705   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
706
707   # flag telling this invoice to have a first-page summary
708   my $summarypage = '';
709
710   if ( $self->custnum && $self->invnum ) {
711     # XXX should be an FS::cust_bill method to set the defaults, instead
712     # of checking the type here
713
714     # info from customer's last invoice before this one, for some 
715     # summary formats
716     $invoice_data{'last_bill'} = {};
717  
718     my $last_bill = $self->previous_bill;
719     if ( $last_bill ) {
720
721       # "balance_date_range" unfortunately is unsuitable for this, since it
722       # cares about application dates.  We want to know the sum of all 
723       # _top-level transactions_ dated before the last invoice.
724       #
725       # still do this for the "Previous Balance" line of the summary block
726       my @sql =
727         map "$_ WHERE _date <= ? AND custnum = ?", (
728           "SELECT      COALESCE( SUM(charged), 0 ) FROM cust_bill",
729           "SELECT -1 * COALESCE( SUM(amount),  0 ) FROM cust_credit",
730           "SELECT -1 * COALESCE( SUM(paid),    0 ) FROM cust_pay",
731           "SELECT      COALESCE( SUM(refund),  0 ) FROM cust_refund",
732         );
733
734       # the customer's current balance immediately after generating the last 
735       # bill
736
737       my $last_bill_balance = $last_bill->charged;
738       foreach (@sql) {
739         my $delta = FS::Record->scalar_sql(
740           $_,
741           $last_bill->_date - 1,
742           $self->custnum,
743         );
744         $last_bill_balance += $delta;
745       }
746
747       $last_bill_balance = sprintf("%.2f", $last_bill_balance);
748
749       warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
750         $last_bill->invnum,
751         $self->time2str_local('%D', $last_bill->_date),
752         $last_bill_balance
753       ) if $DEBUG > 0;
754       # ("true_previous_balance" is a terrible name, but at least it's no
755       # longer stored in the database)
756       $invoice_data{'true_previous_balance'} = $last_bill_balance;
757
758       # Now, get all applications of credits/payments dated on or after the
759       # previous bill, to invoices before the current bill. (The
760       # credit/payment date restriction prevents these from intersecting
761       # the "Previous Balance" set.)
762       # These are "adjustments". The past due balance will be shown as
763       # Previous Balance - Adjustments.
764       my $adjustments = 0;
765       @sql = map {
766         "SELECT COALESCE(SUM(y.amount),0) FROM $_ JOIN cust_bill USING (invnum)
767          WHERE cust_bill._date < ?
768            AND x._date >= ?
769            AND cust_bill.custnum = ?"
770         } "cust_credit AS x JOIN cust_credit_bill y USING (crednum)",
771           "cust_pay    AS x JOIN cust_bill_pay    y USING (paynum)"
772       ;
773       foreach (@sql) {
774         my $delta = FS::Record->scalar_sql(
775           $_,
776           $self->_date,
777           $last_bill->_date,
778           $self->custnum,
779         );
780         $adjustments += $delta;
781       }
782       $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments);
783
784       warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
785                    $invoice_data{'balance_adjustments'}
786       ) if $DEBUG > 0;
787
788       # the sum of amount owed on all previous invoices
789       # ($pr_total is used elsewhere but not as $previous_balance)
790       $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
791
792       $invoice_data{'last_bill'}{'_date'} = $last_bill->_date; #unformatted
793       my (@payments, @credits);
794       # for formats that itemize previous payments
795       foreach my $cust_pay ( qsearch('cust_pay', {
796                               'custnum' => $self->custnum,
797                               '_date'   => { op => '>=',
798                                              value => $last_bill->_date }
799                              } ) )
800       {
801         next if $cust_pay->_date > $self->_date;
802         push @payments, {
803             '_date'       => $cust_pay->_date,
804             'date'        => $self->time2str_local('long', $cust_pay->_date, $format),
805             'payinfo'     => $cust_pay->payby_payinfo_pretty,
806             'amount'      => sprintf('%.2f', $cust_pay->paid),
807         };
808         # not concerned about applications
809       }
810       foreach my $cust_credit ( qsearch('cust_credit', {
811                               'custnum' => $self->custnum,
812                               '_date'   => { op => '>=',
813                                              value => $last_bill->_date }
814                              } ) )
815       {
816         next if $cust_credit->_date > $self->_date;
817         push @credits, {
818             '_date'       => $cust_credit->_date,
819             'date'        => $self->time2str_local('long', $cust_credit->_date, $format),
820             'creditreason'=> $cust_credit->reason,
821             'amount'      => sprintf('%.2f', $cust_credit->amount),
822         };
823       }
824       $invoice_data{'previous_payments'} = \@payments;
825       $invoice_data{'previous_credits'}  = \@credits;
826     } else {
827       # there is no $last_bill
828       $invoice_data{'true_previous_balance'} =
829       $invoice_data{'balance_adjustments'}   =
830       $invoice_data{'previous_balance'}      = '0.00';
831       $invoice_data{'previous_payments'} = [];
832       $invoice_data{'previous_credits'} = [];
833     }
834  
835     if ( $conf->config_bool('invoice_usesummary', $agentnum) ) {
836       $invoice_data{'summarypage'} = $summarypage = 1;
837     }
838
839   } # if this is an invoice
840
841   warn "$me substituting variables in notes, footer, smallfooter\n"
842     if $DEBUG > 1;
843
844   my $tc = $self->template_conf;
845   my @include = ( [ $tc,        'notes' ],
846                   [ 'invoice_', 'footer' ],
847                   [ 'invoice_', 'smallfooter', ],
848                   [ 'invoice_', 'watermark' ],
849                 );
850   push @include, [ $tc,        'coupon', ]
851     unless $params{'no_coupon'};
852
853   foreach my $i (@include) {
854
855     # load the configuration for this sub-template
856
857     my($base, $include) = @$i;
858
859     my $inc_file = $conf->key_orbase("$base$format$include", $template);
860
861     my @inc_src = $conf->config($inc_file, $agentnum);
862     if (!@inc_src) {
863       my $converter = $convert_maps{$format}{$include};
864       if ( $converter ) {
865         # then attempt to convert LaTeX to the requested format
866         $inc_file = $conf->key_orbase($base.'latex'.$include, $template);
867         @inc_src = &$converter( $conf->config($inc_file, $agentnum) );
868         foreach (@inc_src) {
869           # this isn't included in the convert_maps
870           my ($open, $close) = @{ $delimiters{$format} };
871           s/\[\@--/$open/g;
872           s/--\@\]/$close/g;
873         }
874       }
875     } # else @inc_src is empty and that's fine
876
877     # make a Text::Template out of it
878
879     my $inc_tt = new Text::Template (
880       TYPE       => 'ARRAY',
881       SOURCE     => [ map "$_\n", @inc_src ],
882       DELIMITERS => $delimiters{$format},
883     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
884
885     unless ( $inc_tt->compile() ) {
886       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
887       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
888       die $error;
889     }
890
891     # fill in variables
892
893     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
894
895     $invoice_data{$include} =~ s/\n+$//
896       if ($format eq 'latex');
897   }
898
899   # let invoices use either of these as needed
900   $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
901     ? $cust_main->payinfo : '';
902   $invoice_data{'po_line'} = 
903     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
904       ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
905       : $nbsp;
906
907   my %money_chars = ( 'latex'    => '',
908                       'html'     => $conf->config('money_char') || '$',
909                       'template' => '',
910                     );
911   my $money_char = $money_chars{$format};
912
913   # extremely dubious
914   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
915                             'html'     => $conf->config('money_char') || '$',
916                             'template' => '',
917                           );
918   my $other_money_char = $other_money_chars{$format};
919   $invoice_data{'dollar'} = $other_money_char;
920
921   my %minus_signs = ( 'latex'    => '$-$',
922                       'html'     => '&minus;',
923                       'template' => '- ' );
924   my $minus = $minus_signs{$format};
925
926   my @detail_items = ();
927   my @total_items = ();
928   my @buf = ();
929   my @sections = ();
930
931   $invoice_data{'detail_items'} = \@detail_items;
932   $invoice_data{'total_items'} = \@total_items;
933   $invoice_data{'buf'} = \@buf;
934   $invoice_data{'sections'} = \@sections;
935
936   warn "$me generating sections\n"
937     if $DEBUG > 1;
938
939   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
940   my $multisection = $self->has_sections;
941   if ( $multisection ) {
942     $invoice_data{multisection} = $conf->config($tc.'sections_method') || 1;
943   }
944   my $section_with_taxes = 1
945     if $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum);
946   my $late_sections;
947   my $extra_sections = [];
948   my $extra_lines = ();
949
950   # default section ('Charges')
951   my $default_section = { 'description' => '',
952                           'subtotal'    => '', 
953                           'no_subtotal' => 1,
954                         };
955
956   # Previous Charges section
957   # subtotal is the first return value from $self->previous
958   my $previous_section;
959   # if the invoice has major sections, or if we're summarizing previous 
960   # charges with a single line, or if we've been specifically told to put them
961   # in a section, create a section for previous charges:
962   if ( $multisection or
963        $conf->exists('previous_balance-summary_only') or
964        $conf->exists('previous_balance-section') ) {
965     
966     $previous_section =  { 'description' => $self->mt('Previous Charges'),
967                            'subtotal'    => $other_money_char.
968                                             sprintf('%.2f', $pr_total),
969                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
970                          };
971     $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
972       join(' / ', map { $cust_main->balance_date_range(@$_) }
973                   $self->_prior_month30s
974           )
975       if $conf->exists('invoice_include_aging');
976
977   } else {
978     # otherwise put them in the main section
979     $previous_section = $default_section;
980   }
981
982   my $adjust_section = {
983     'description'    => $self->mt('Credits, Payments, and Adjustments'),
984     'adjust_section' => 1,
985     'subtotal'       => 0,   # adjusted below
986   };
987   my $adjust_weight = _pkg_category($adjust_section->{description})
988                         ? _pkg_category($adjust_section->{description})->weight
989                         : 0;
990   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
991   # Note: 'sort_weight' here is actually a flag telling whether there is an
992   # explicit package category for the adjust section. If so, certain behavior
993   # happens.
994   $adjust_section->{'sort_weight'} = $adjust_weight;
995
996
997   if ( $multisection ) {
998     ($extra_sections, $extra_lines) =
999       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
1000       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
1001       && $self->can('_items_extra_usage_sections');
1002
1003     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
1004
1005     push @detail_items, @$extra_lines if $extra_lines;
1006
1007     # the code is written so that both methods can be used together, but
1008     # we haven't yet changed the template to take advantage of that, so for 
1009     # now, treat them as mutually exclusive.
1010     my %section_method = ( by_category => 1 );
1011     if ( $conf->config($tc.'sections_method') eq 'location' ) {
1012       %section_method = ( by_location => 1 );
1013     }
1014     my ($early, $late) =
1015       $self->_items_sections( 'summary' => $summarypage,
1016                               'escape'  => $escape_function_nonbsp,
1017                               'extra_sections' => $extra_sections,
1018                               'format'  => $format,
1019                               %section_method
1020                             );
1021     push @sections, @$early;
1022     $late_sections = $late;
1023
1024     if (    $conf->exists('svc_phone_sections')
1025          && $self->can('_items_svc_phone_sections')
1026        )
1027     {
1028       my ($phone_sections, $phone_lines) =
1029         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
1030       push @{$late_sections}, @$phone_sections;
1031       push @detail_items, @$phone_lines;
1032     }
1033     if ( $conf->exists('voip-cust_accountcode_cdr')
1034          && $cust_main->accountcode_cdr
1035          && $self->can('_items_accountcode_cdr')
1036        )
1037     {
1038       my ($accountcode_section, $accountcode_lines) =
1039         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
1040       if ( scalar(@$accountcode_lines) ) {
1041           push @{$late_sections}, $accountcode_section;
1042           push @detail_items, @$accountcode_lines;
1043       }
1044     }
1045   } else {# not multisection
1046     # make a default section
1047     push @sections, $default_section;
1048     # and calculate the finance charge total, since it won't get done otherwise.
1049     # and the default section total
1050     # XXX possibly finance_pkgclass should not be used in this manner?
1051     my @finance_charges;
1052     my @charges;
1053     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1054       if ( $invoice_data{finance_section} and 
1055         grep { $_->section eq $invoice_data{finance_section} }
1056            $cust_bill_pkg->cust_bill_pkg_display ) {
1057         # I think these are always setup fees, but just to be sure...
1058         push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1059       } else {
1060         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1061       }
1062     }
1063     $invoice_data{finance_amount} = 
1064       sprintf('%.2f', sum( @finance_charges ) || 0);
1065     $default_section->{subtotal} = $other_money_char.
1066                                     sprintf('%.2f', sum( @charges ) || 0);
1067   }
1068
1069   # start setting up summary subtotals
1070   my @summary_subtotals;
1071   my $method = $conf->config('summary_subtotals_method');
1072   if ( ( ref($self) ne 'FS::quotation' ) and $method and $method ne $conf->config($tc.'sections_method') ) {
1073     # then re-section them by the correct method
1074     my %section_method = ( by_category => 1 );
1075     if ( $conf->config('summary_subtotals_method') eq 'location' ) {
1076       %section_method = ( by_location => 1 );
1077     }
1078     my ($early, $late) =
1079       $self->_items_sections( 'summary' => $summarypage,
1080                               'escape'  => $escape_function_nonbsp,
1081                               'extra_sections' => $extra_sections,
1082                               'format'  => $format,
1083                               %section_method
1084                             );
1085     foreach ( @$early ) {
1086       next if $_->{subtotal} == 0;
1087       $_->{subtotal} = $other_money_char.sprintf('%.2f', $_->{subtotal});
1088       push @summary_subtotals, $_;
1089     }
1090   } else {
1091     # subtotal sectioning is the same as for the actual invoice sections
1092     @summary_subtotals = @sections;
1093   }
1094
1095   # Hereafter, push sections to both @sections and @summary_subtotals
1096   # if they belong in both places (e.g. tax section).  Late sections are
1097   # never in @summary_subtotals.
1098
1099   # previous invoice balances in the Previous Charges section if there
1100   # is one, otherwise in the main detail section
1101   # (except if summary_only is enabled, don't show them at all)
1102   if ( $self->can('_items_previous') &&
1103        $self->enable_previous &&
1104        ! $conf->exists('previous_balance-summary_only') ) {
1105
1106     warn "$me adding previous balances\n"
1107       if $DEBUG > 1;
1108
1109     foreach my $line_item ( $self->_items_previous ) {
1110
1111       my $detail = {
1112         ref             => $line_item->{'pkgnum'},
1113         pkgpart         => $line_item->{'pkgpart'},
1114         #quantity        => 1, # not really correct
1115         section         => $previous_section, # which might be $default_section
1116         description     => &$escape_function($line_item->{'description'}),
1117         ext_description => [ map { &$escape_function($_) } 
1118                              @{ $line_item->{'ext_description'} || [] }
1119                            ],
1120         amount          => $money_char . $line_item->{'amount'},
1121         product_code    => $line_item->{'pkgpart'} || 'N/A',
1122       };
1123
1124       push @detail_items, $detail;
1125       push @buf, [ $detail->{'description'},
1126                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1127                  ];
1128     }
1129
1130   }
1131
1132   if ( @pr_cust_bill && $self->enable_previous ) {
1133     push @buf, ['','-----------'];
1134     push @buf, [ $self->mt('Total Previous Balance'),
1135                  $money_char. sprintf("%10.2f", $pr_total) ];
1136     push @buf, ['',''];
1137   }
1138  
1139   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
1140       warn "$me adding DID summary\n"
1141         if $DEBUG > 1;
1142
1143       my ($didsummary,$minutes) = $self->_did_summary;
1144       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
1145       push @detail_items, 
1146        { 'description' => $didsummary_desc,
1147            'ext_description' => [ $didsummary, $minutes ],
1148        };
1149   }
1150
1151   foreach my $section (@sections, @$late_sections) {
1152
1153     # begin some normalization
1154     $section->{'subtotal'} = $section->{'amount'}
1155       if $multisection
1156          && !exists($section->{subtotal})
1157          && exists($section->{amount});
1158
1159     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1160       if ( $invoice_data{finance_section} &&
1161            $section->{'description'} eq $invoice_data{finance_section} );
1162
1163     if ( $multisection ) {
1164
1165       if ( ref($section->{'subtotal'}) ) {
1166
1167         $section->{'subtotal'} =
1168           sprintf("$other_money_char%.2f to $other_money_char%.2f",
1169                     $section->{'subtotal'}[0],
1170                     $section->{'subtotal'}[1]
1171                  );
1172
1173       } else {
1174
1175         $section->{'subtotal'} = $other_money_char.
1176                                  sprintf('%.2f', $section->{'subtotal'})
1177
1178       }
1179
1180       # continue some normalization
1181       $section->{'amount'}   = $section->{'subtotal'}
1182
1183     }
1184
1185     if ( $section->{'description'} ) {
1186       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1187                    [ '', '' ],
1188                  );
1189     }
1190
1191     warn "$me   setting options\n"
1192       if $DEBUG > 1;
1193
1194     my %options = ();
1195     $options{'section'} = $section if $multisection;
1196     $options{'section_with_taxes'} = 1
1197       if $multisection
1198       && $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum);
1199     $options{'format'} = $format;
1200     $options{'escape_function'} = $escape_function;
1201     $options{'no_usage'} = 1 unless $unsquelched;
1202     $options{'unsquelched'} = $unsquelched;
1203     $options{'summary_page'} = $summarypage;
1204     $options{'skip_usage'} =
1205       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1206     $options{'preref_callback'} = $params{'preref_callback'};
1207     $options{'disable_line_item_date_ranges'} =
1208       $conf->exists('disable_line_item_date_ranges');
1209
1210     warn "$me   searching for line items\n"
1211       if $DEBUG > 1;
1212
1213     my %section_tax_lines;
1214     my %seen_tax_lines;
1215
1216     foreach my $line_item ( $self->_items_pkg(%options),
1217                             $self->_items_fee(%options) ) {
1218
1219       warn "$me     adding line item ".
1220            join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
1221         if $DEBUG > 1;
1222
1223       push @buf, ( [ $line_item->{'description'},
1224                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1225                    ],
1226                    map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1227                  );
1228
1229       $line_item->{'ref'} = $line_item->{'pkgnum'};
1230       $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1231       $line_item->{'section'} = $section;
1232       $line_item->{'description'} = &$escape_function($line_item->{'description'});
1233       $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1234
1235       if ( length($line_item->{'unit_amount'}) ) {
1236         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1237       }
1238       $line_item->{'ext_description'} ||= [];
1239
1240       if ( $section_with_taxes && ref $line_item->{pkg_tax} ) {
1241         for my $line_tax ( @{$ line_item->{pkg_tax} } ) {
1242
1243           # It is rarely possible for the same tax record to be presented here
1244           # multiple times.  See cust_bill_pkg::_pkg_tax_list for more info
1245           next if $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} };
1246           $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} } = 1;
1247
1248           $section_tax_lines{ $line_tax->{taxname} } += $line_tax->{amount};
1249         }
1250       }
1251
1252       push @detail_items, $line_item;
1253     }
1254
1255     # If conf flag invoice_sections_with_taxes:
1256     # - Add @detail_items for taxes into each section
1257     # - Update section subtotal to include taxes
1258     if ( $section_with_taxes && %section_tax_lines ) {
1259       for my $taxname ( keys %section_tax_lines ) {
1260
1261         push @detail_items, {
1262           section => $section,
1263           amount  => sprintf($money_char."%.2f",$section_tax_lines{$taxname}),
1264           description => &$escape_function($taxname),
1265         };
1266
1267         # Append taxes to total.  If line format resembles "$5.00 to $12.00"
1268         # append to the second value.
1269         if ($section->{subtotal} =~ /to/) {
1270           my @subtotal = split /\s/, $section->{subtotal};
1271           $subtotal[2] =~ s/[^\d\.]//g;
1272           $subtotal[2] = sprintf(
1273             $money_char."%.2f",
1274             ( $subtotal[2] + $section_tax_lines{$taxname} )
1275           );
1276           $section->{subtotal} = join ' ', @subtotal;
1277         } else {
1278         $section->{subtotal} =~ s/[^\d\.]//g;
1279           $section->{subtotal} = sprintf(
1280             $money_char . "%.2f",
1281             ( $section->{subtotal} + $section_tax_lines{$taxname} )
1282           );
1283         }
1284
1285       }
1286     }
1287
1288     if ( $section->{'description'} ) {
1289       push @buf, ( ['','-----------'],
1290                    [ $section->{'description'}. ' sub-total',
1291                       $section->{'subtotal'} # already formatted this 
1292                    ],
1293                    [ '', '' ],
1294                    [ '', '' ],
1295                  );
1296     }
1297   
1298   }
1299
1300   $invoice_data{current_less_finance} =
1301     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1302
1303   # if there's anything in the Previous Charges section, prepend it to the list
1304   if ( $pr_total and $previous_section ne $default_section ) {
1305     unshift @sections, $previous_section;
1306     # but not @summary_subtotals
1307   }
1308
1309   warn "$me adding taxes\n"
1310     if $DEBUG > 1;
1311
1312   # create a tax section if we don't yet have one
1313   my $tax_description = 'Taxes, Surcharges, and Fees';
1314   my $tax_section =
1315     List::Util::first { $_->{description} eq $tax_description } @sections;
1316   if (!$tax_section) {
1317     $tax_section = { 'description' => $tax_description };
1318     push @sections, $tax_section if $multisection;
1319   }
1320   $tax_section->{tax_section} = 1; # mark this section as containing taxes
1321   # if this is an existing tax section, we're merging the tax items into it.
1322   # grab the taxtotal that's already there, strip the money symbol if any
1323   my $taxtotal = $tax_section->{'subtotal'} || 0;
1324   $taxtotal =~ s/^\Q$other_money_char\E//;
1325
1326   # this does nothing
1327   #my $tax_weight = _pkg_category($tax_section->{description})
1328   #                      ? _pkg_category($tax_section->{description})->weight
1329   #                      : 0;
1330   #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
1331   #$tax_section->{'sort_weight'} = $tax_weight;
1332
1333   my @items_tax = $self->_items_tax;
1334   foreach my $tax ( @items_tax ) {
1335
1336     $taxtotal += $tax->{'amount'};
1337
1338     my $description = &$escape_function( $tax->{'description'} );
1339     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
1340
1341     if ( $multisection ) {
1342
1343       push @detail_items, {
1344         ext_description => [],
1345         ref          => '',
1346         quantity     => '',
1347         description  => $description,
1348         amount       => $money_char. $amount,
1349         product_code => '',
1350         section      => $tax_section,
1351       };
1352
1353     } else {
1354
1355       push @total_items, {
1356         'total_item'   => $description,
1357         'total_amount' => $other_money_char. $amount,
1358       };
1359
1360     }
1361
1362     push @buf,[ $description,
1363                 $money_char. $amount,
1364               ];
1365
1366   }
1367  
1368   if ( @items_tax ) {
1369     my $total = {};
1370     $total->{'total_item'} = $self->mt('Sub-total');
1371     $total->{'total_amount'} =
1372       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1373
1374     if ( $multisection ) {
1375       if ( $taxtotal > 0 ) {
1376         # there are taxes, so prepare the section to be displayed.
1377         # $taxtotal already includes any line items that were already in the
1378         # section (fees, taxes that are charged as packages for some reason).
1379         # also set 'summarized' to false so that this isn't a summary-only
1380         # section.
1381         $tax_section->{'subtotal'} = $other_money_char.
1382                                      sprintf('%.2f', $taxtotal);
1383         $tax_section->{'pretotal'} = 'New charges sub-total '.
1384                                      $total->{'total_amount'};
1385         $tax_section->{'description'} = $self->mt($tax_description);
1386         $tax_section->{'summarized'} = '';
1387
1388         if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
1389
1390           # remove tax section if taxes are itemized within other sections
1391           @sections = grep{ $_ ne $tax_section } @sections;
1392
1393         } elsif ( !grep $tax_section, @sections ) {
1394
1395           # append it if it's not already there
1396           push @sections, $tax_section;
1397           push @summary_subtotals, $tax_section;
1398
1399         }
1400
1401       }
1402
1403     } else {
1404       unshift @total_items, $total;
1405     }
1406   }
1407   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1408
1409   ###
1410   # Totals
1411   ###
1412
1413   my %embolden_functions = (
1414     'latex'    => sub { return '\textbf{'. shift(). '}' },
1415     'html'     => sub { return '<b>'. shift(). '</b>' },
1416     'template' => sub { shift },
1417   );
1418   my $embolden_function = $embolden_functions{$format};
1419
1420   if ( $multisection ) {
1421
1422     if ( $adjust_section->{'sort_weight'} ) {
1423       $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1424         $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
1425     } else{
1426       $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1427         $other_money_char.  sprintf('%.2f', $self->charged );
1428     }
1429
1430   }
1431   
1432   if ( $self->can('_items_total') ) { # should always be true now
1433
1434     # even for multisection, need plain text version
1435
1436     my @new_total_items = $self->_items_total;
1437
1438     push @buf,['','-----------'];
1439
1440     foreach ( @new_total_items ) {
1441       my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'});
1442       $_->{'total_item'}   = &$embolden_function( $item );
1443
1444       if ( ref($amount) ) {
1445         $_->{'total_amount'} = &$embolden_function(
1446                                  $other_money_char.$amount->[0]. ' to '.
1447                                  $other_money_char.$amount->[1]
1448                                );
1449       } else {
1450       $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
1451       }
1452
1453       # but if it's multisection, don't append to @total_items. the adjust
1454       # section has all this stuff
1455       push @total_items, $_ if !$multisection;
1456       push @buf, [ $item, $money_char.sprintf('%10.2f',$amount) ];
1457     }
1458
1459     push @buf, [ '', '' ];
1460
1461     # if we're showing previous invoices, also show previous
1462     # credits and payments 
1463     if ( $self->enable_previous 
1464           and $self->can('_items_credits')
1465           and $self->can('_items_payments') )
1466       {
1467     
1468       # credits
1469       my $credittotal = 0;
1470       foreach my $credit (
1471         $self->_items_credits( 'template' => $template, 'trim_len' => 40 )
1472       ) {
1473
1474         my $total;
1475         $total->{'total_item'} = &$escape_function($credit->{'description'});
1476         $credittotal += $credit->{'amount'};
1477         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1478         if ( $multisection ) {
1479           push @detail_items, {
1480             ext_description => [],
1481             ref          => '',
1482             quantity     => '',
1483             description  => &$escape_function($credit->{'description'}),
1484             amount       => $money_char . $credit->{'amount'},
1485             product_code => '',
1486             section      => $adjust_section,
1487           };
1488         } else {
1489           push @total_items, $total;
1490         }
1491
1492       }
1493       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1494
1495       #credits (again)
1496       foreach my $credit (
1497         $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1498       ) {
1499         push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1500       }
1501
1502       # payments
1503       my $paymenttotal = 0;
1504       foreach my $payment (
1505         $self->_items_payments( 'template' => $template )
1506       ) {
1507         my $total = {};
1508         $total->{'total_item'} = &$escape_function($payment->{'description'});
1509         $paymenttotal += $payment->{'amount'};
1510         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1511         if ( $multisection ) {
1512           push @detail_items, {
1513             ext_description => [],
1514             ref          => '',
1515             quantity     => '',
1516             description  => &$escape_function($payment->{'description'}),
1517             amount       => $money_char . $payment->{'amount'},
1518             product_code => '',
1519             section      => $adjust_section,
1520           };
1521         }else{
1522           push @total_items, $total;
1523         }
1524         push @buf, [ $payment->{'description'},
1525                      $money_char. sprintf("%10.2f", $payment->{'amount'}),
1526                    ];
1527       }
1528       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1529     
1530       if ( $multisection ) {
1531         $adjust_section->{'subtotal'} = $other_money_char.
1532                                         sprintf('%.2f', $credittotal + $paymenttotal);
1533
1534         #why this? because {sort_weight} forces the adjust_section to appear
1535         #in @extra_sections instead of @sections. obviously.
1536         push @sections, $adjust_section
1537           unless $adjust_section->{sort_weight};
1538         # do not summarize; adjustments there are shown according to 
1539         # different rules
1540       }
1541
1542       # create Balance Due message
1543       { 
1544         my $total;
1545         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1546         $total->{'total_amount'} =
1547           &$embolden_function(
1548             $other_money_char. sprintf('%.2f', #why? $summarypage 
1549                                                #  ? $self->charged +
1550                                                #    $self->billing_balance
1551                                                #  :
1552                                                    $self->owed + $pr_total
1553                                       )
1554           );
1555         if ( $multisection && !$adjust_section->{sort_weight} ) {
1556           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1557                                            $total->{'total_amount'};
1558         } else {
1559           push @total_items, $total;
1560         }
1561         push @buf,['','-----------'];
1562         push @buf,[$self->balance_due_msg, $money_char. 
1563           sprintf("%10.2f", $balance_due ) ];
1564       }
1565
1566       if ( $conf->exists('previous_balance-show_credit')
1567           and $cust_main->balance < 0 ) {
1568         my $credit_total = {
1569           'total_item'    => &$embolden_function($self->credit_balance_msg),
1570           'total_amount'  => &$embolden_function(
1571             $other_money_char. sprintf('%.2f', -$cust_main->balance)
1572           ),
1573         };
1574         if ( $multisection ) {
1575           $adjust_section->{'posttotal'} .= $newline_token .
1576             $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1577         }
1578         else {
1579           push @total_items, $credit_total;
1580         }
1581         push @buf,['','-----------'];
1582         push @buf,[$self->credit_balance_msg, $money_char. 
1583           sprintf("%10.2f", -$cust_main->balance ) ];
1584       }
1585     }
1586
1587   } #end of default total adding ! can('_items_total')
1588
1589   if ( $multisection ) {
1590     if (    $conf->exists('svc_phone_sections')
1591          && $self->can('_items_svc_phone_sections')
1592        )
1593     {
1594       my $total;
1595       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1596       $total->{'total_amount'} =
1597         &$embolden_function(
1598           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1599         );
1600       my $last_section = pop @sections;
1601       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1602                                      $total->{'total_amount'};
1603       push @sections, $last_section;
1604     }
1605     push @sections, @$late_sections
1606       if $unsquelched;
1607   }
1608
1609   # make a discounts-available section, even without multisection
1610   if ( $conf->exists('discount-show_available') 
1611        and my @discounts_avail = $self->_items_discounts_avail ) {
1612     my $discount_section = {
1613       'description' => $self->mt('Discounts Available'),
1614       'subtotal'    => '',
1615       'no_subtotal' => 1,
1616     };
1617
1618     push @sections, $discount_section; # do not summarize
1619     push @detail_items, map { +{
1620         'ref'         => '', #should this be something else?
1621         'section'     => $discount_section,
1622         'description' => &$escape_function( $_->{description} ),
1623         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1624         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1625     } } @discounts_avail;
1626   }
1627
1628   # not adding any more sections after this
1629   $invoice_data{summary_subtotals} = \@summary_subtotals;
1630
1631   # usage subtotals
1632   if ( $conf->exists('usage_class_summary')
1633        and $self->can('_items_usage_class_summary') ) {
1634     my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char);
1635     if ( @usage_subtotals ) {
1636       unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1637       unshift @detail_items, @usage_subtotals;
1638     }
1639   }
1640
1641   # invoice history "section" (not really a section)
1642   # not to be included in any subtotals, completely independent of 
1643   # everything...
1644   if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
1645     my %history;
1646     my %monthorder;
1647     foreach my $cust_bill ( $cust_main->cust_bill ) {
1648       # XXX hardcoded format, and currently only 'charged'; add other fields
1649       # if they become necessary
1650       my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1651       $history{$date} ||= 0;
1652       $history{$date} += $cust_bill->charged;
1653       # just so we have a numeric sort key
1654       $monthorder{$date} ||= $cust_bill->_date;
1655     }
1656     my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1657                         keys %history;
1658     my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1659     $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1660   }
1661
1662   # service locations: another option for template customization
1663   my %location_info;
1664   foreach my $item (@detail_items) {
1665     if ( $item->{locationnum} ) {
1666       $location_info{ $item->{locationnum} } ||= {
1667         FS::cust_location->by_key( $item->{locationnum} )->location_hash
1668       };
1669     }
1670   }
1671   $invoice_data{location_info} = \%location_info;
1672
1673   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1674   # the invoice variables
1675   return \%invoice_data if ( $params{'diag'} );
1676
1677   # All sections and items are built; now fill in templates.
1678   my @includelist = ();
1679   push @includelist, 'summary' if $summarypage;
1680   foreach my $include ( @includelist ) {
1681
1682     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1683     my @inc_src;
1684
1685     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1686
1687       @inc_src = $conf->config($inc_file, $agentnum);
1688
1689     } else {
1690
1691       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1692
1693       my $convert_map = $convert_maps{$format}{$include};
1694
1695       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1696                        s/--\@\]/$delimiters{$format}[1]/g;
1697                        $_;
1698                      } 
1699                  &$convert_map( $conf->config($inc_file, $agentnum) );
1700
1701     }
1702
1703     my $inc_tt = new Text::Template (
1704       TYPE       => 'ARRAY',
1705       SOURCE     => [ map "$_\n", @inc_src ],
1706       DELIMITERS => $delimiters{$format},
1707     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1708
1709     unless ( $inc_tt->compile() ) {
1710       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1711       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1712       die $error;
1713     }
1714
1715     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1716
1717     $invoice_data{$include} =~ s/\n+$//
1718       if ($format eq 'latex');
1719   }
1720
1721   $invoice_lines = 0;
1722   my $wasfunc = 0;
1723   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1724     /invoice_lines\((\d*)\)/;
1725     $invoice_lines += $1 || scalar(@buf);
1726     $wasfunc=1;
1727   }
1728   die "no invoice_lines() functions in template?"
1729     if ( $format eq 'template' && !$wasfunc );
1730
1731   if ( $invoice_lines ) {
1732     $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1733     $invoice_data{'total_pages'}++
1734       if scalar(@buf) % $invoice_lines;
1735   }
1736
1737   #setup subroutine for the template
1738   $invoice_data{invoice_lines} = sub {
1739     my $lines = shift || scalar(@buf);
1740     map { 
1741       scalar(@buf)
1742         ? shift @buf
1743         : [ '', '' ];
1744     }
1745     ( 1 .. $lines );
1746   };
1747
1748   if ($format eq 'template') {
1749
1750     my $lines;
1751     my @collect;
1752     while (@buf) {
1753       push @collect, split("\n",
1754         $text_template->fill_in( HASH => \%invoice_data )
1755       );
1756       $invoice_data{'page'}++;
1757     }
1758     map "$_\n", @collect;
1759
1760   } else { # this is where we actually create the invoice
1761
1762     if ( $params{no_addresses} ) {
1763       delete $invoice_data{$_} foreach qw(
1764         payname company address1 address2 city state zip country
1765       );
1766       $invoice_data{returnaddress} = '~';
1767     }
1768
1769     warn "filling in template for invoice ". $self->invnum. "\n"
1770       if $DEBUG;
1771     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1772       if $DEBUG > 1;
1773
1774     $text_template->fill_in(HASH => \%invoice_data);
1775   }
1776 }
1777
1778 sub notice_name { '('.shift->table.')'; }
1779
1780 sub template_conf { 'invoice_'; }
1781
1782 # helper routine for generating date ranges
1783 sub _prior_month30s {
1784   my $self = shift;
1785   my @ranges = (
1786    [ 1,       2592000 ], # 0-30 days ago
1787    [ 2592000, 5184000 ], # 30-60 days ago
1788    [ 5184000, 7776000 ], # 60-90 days ago
1789    [ 7776000, 0       ], # 90+   days ago
1790   );
1791
1792   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1793           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1794       ] }
1795   @ranges;
1796 }
1797
1798 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1799
1800 Returns an postscript invoice, as a scalar.
1801
1802 Options can be passed as a hashref (recommended) or as a list of time, template
1803 and then any key/value pairs for any other options.
1804
1805 I<time> an optional value used to control the printing of overdue messages.  The
1806 default is now.  It isn't the date of the invoice; that's the `_date' field.
1807 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1808 L<Time::Local> and L<Date::Parse> for conversion functions.
1809
1810 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1811
1812 =cut
1813
1814 sub print_ps {
1815   my $self = shift;
1816
1817   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1818   my $ps = generate_ps($file);
1819   unlink($logofile);
1820   unlink($barcodefile) if $barcodefile;
1821
1822   $ps;
1823 }
1824
1825 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1826
1827 Returns an PDF invoice, as a scalar.
1828
1829 Options can be passed as a hashref (recommended) or as a list of time, template
1830 and then any key/value pairs for any other options.
1831
1832 I<time> an optional value used to control the printing of overdue messages.  The
1833 default is now.  It isn't the date of the invoice; that's the `_date' field.
1834 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1835 L<Time::Local> and L<Date::Parse> for conversion functions.
1836
1837 I<template>, if specified, is the name of a suffix for alternate invoices.
1838
1839 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1840
1841 =cut
1842
1843 sub print_pdf {
1844   my $self = shift;
1845
1846   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1847   my $pdf = generate_pdf($file);
1848   unlink($logofile);
1849   unlink($barcodefile) if $barcodefile;
1850
1851   $pdf;
1852 }
1853
1854 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1855
1856 Returns an HTML invoice, as a scalar.
1857
1858 I<time> an optional value used to control the printing of overdue messages.  The
1859 default is now.  It isn't the date of the invoice; that's the `_date' field.
1860 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1861 L<Time::Local> and L<Date::Parse> for conversion functions.
1862
1863 I<template>, if specified, is the name of a suffix for alternate invoices.
1864
1865 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1866
1867 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1868 when emailing the invoice as part of a multipart/related MIME email.
1869
1870 =cut
1871
1872 sub print_html {
1873   my $self = shift;
1874   my %params;
1875   if ( ref($_[0]) ) {
1876     %params = %{ shift() }; 
1877   } else {
1878     %params = @_;
1879   }
1880   $params{'format'} = 'html';
1881   
1882   $self->print_generic( %params );
1883 }
1884
1885 # quick subroutine for print_latex
1886 #
1887 # There are ten characters that LaTeX treats as special characters, which
1888 # means that they do not simply typeset themselves: 
1889 #      # $ % & ~ _ ^ \ { }
1890 #
1891 # TeX ignores blanks following an escaped character; if you want a blank (as
1892 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1893
1894 sub _latex_escape {
1895   my $value = shift;
1896   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1897   $value =~ s/([<>])/\$$1\$/g;
1898   $value;
1899 }
1900
1901 sub _html_escape {
1902   my $value = shift;
1903   encode_entities($value);
1904   $value;
1905 }
1906
1907 sub _html_escape_nbsp {
1908   my $value = _html_escape(shift);
1909   $value =~ s/ +/&nbsp;/g;
1910   $value;
1911 }
1912
1913 #utility methods for print_*
1914
1915 sub _translate_old_latex_format {
1916   warn "_translate_old_latex_format called\n"
1917     if $DEBUG; 
1918
1919   my @template = ();
1920   while ( @_ ) {
1921     my $line = shift;
1922   
1923     if ( $line =~ /^%%Detail\s*$/ ) {
1924   
1925       push @template, q![@--!,
1926                       q!  foreach my $_tr_line (@detail_items) {!,
1927                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1928                       q!      $_tr_line->{'description'} .= !, 
1929                       q!        "\\tabularnewline\n~~".!,
1930                       q!        join( "\\tabularnewline\n~~",!,
1931                       q!          @{$_tr_line->{'ext_description'}}!,
1932                       q!        );!,
1933                       q!    }!;
1934
1935       while ( ( my $line_item_line = shift )
1936               !~ /^%%EndDetail\s*$/                            ) {
1937         $line_item_line =~ s/'/\\'/g;    # nice LTS
1938         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1939         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1940         push @template, "    \$OUT .= '$line_item_line';";
1941       }
1942
1943       push @template, '}',
1944                       '--@]';
1945       #' doh, gvim
1946     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1947
1948       push @template, '[@--',
1949                       '  foreach my $_tr_line (@total_items) {';
1950
1951       while ( ( my $total_item_line = shift )
1952               !~ /^%%EndTotalDetails\s*$/                      ) {
1953         $total_item_line =~ s/'/\\'/g;    # nice LTS
1954         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1955         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1956         push @template, "    \$OUT .= '$total_item_line';";
1957       }
1958
1959       push @template, '}',
1960                       '--@]';
1961
1962     } else {
1963       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1964       push @template, $line;  
1965     }
1966   
1967   }
1968
1969   if ($DEBUG) {
1970     warn "$_\n" foreach @template;
1971   }
1972
1973   (@template);
1974 }
1975
1976 =item terms
1977
1978 =cut
1979
1980 sub terms {
1981   my $self = shift;
1982   my $conf = $self->conf;
1983
1984   #check for an invoice-specific override
1985   return $self->invoice_terms if $self->invoice_terms;
1986   
1987   #check for a customer- specific override
1988   my $cust_main = $self->cust_main;
1989   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1990
1991   my $agentnum = '';
1992   if ( $cust_main ) {
1993     $agentnum = $cust_main->agentnum;
1994   } elsif ( my $prospect_main = $self->prospect_main ) {
1995     $agentnum = $prospect_main->agentnum;
1996   }
1997
1998   #use configured default
1999   $conf->config('invoice_default_terms', $agentnum) || '';
2000 }
2001
2002 =item due_date
2003
2004 =cut
2005
2006 sub due_date {
2007   my $self = shift;
2008   my $duedate = '';
2009   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2010     $duedate = $self->_date() + ( $1 * 86400 );
2011   } elsif ( $self->terms =~ /^End of Month$/ ) {
2012     my ($mon,$year) = (localtime($self->_date) )[4,5];
2013     $mon++;
2014     until ( $mon < 12 ) { $mon -= 12; $year++; }
2015     my $nextmonth_first = timelocal(0,0,0,1,$mon,$year);
2016     $duedate = $nextmonth_first - 86400;
2017   }
2018   $duedate;
2019 }
2020
2021 =item due_date2str
2022
2023 =cut
2024
2025 sub due_date2str {
2026   my $self = shift;
2027   $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
2028 }
2029
2030 =item invoice_pay_by_msg
2031
2032   displays the invoice_pay_by_msg or default Please pay by [_1] if empty.
2033
2034 =cut
2035
2036 sub invoice_pay_by_msg {
2037   my $self = shift;
2038   my $msg = '';
2039   my $please_pay_by =
2040         $self->conf->config('invoice_pay_by_msg', $self->agentnum)
2041         || 'Please pay by [_1]';
2042   $msg .= ' - ' . $self->mt($please_pay_by, $self->due_date2str('short')) . ' ';
2043
2044   $msg;
2045 }
2046
2047 =item balance_due_msg
2048
2049 =cut
2050
2051 sub balance_due_msg {
2052   my $self = shift;
2053   my $msg = $self->mt('Balance Due');
2054   return $msg unless $self->terms; # huh?
2055   if ( !$self->conf->exists('invoice_show_prior_due_date')
2056        || $self->has_sections ) {
2057     # if enabled, the due date is shown with Total New Charges (see
2058     # _items_total) and not here
2059     # (yes, or if invoice_sections is enabled; this is just for compatibility)
2060     if ( $self->due_date ) {
2061       $msg .= $self->invoice_pay_by_msg
2062        unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum);
2063     } elsif ( $self->terms ) {
2064       $msg .= ' - '. $self->mt($self->terms);
2065     }
2066   }
2067   $msg;
2068 }
2069
2070 =item balance_due_date
2071
2072 =cut
2073
2074 sub balance_due_date {
2075   my $self = shift;
2076   my $conf = $self->conf;
2077   my $duedate = '';
2078   my $terms = $self->terms;
2079   if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2080     $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
2081   }
2082   $duedate;
2083 }
2084
2085 sub credit_balance_msg { 
2086   my $self = shift;
2087   $self->mt('Credit Balance Remaining')
2088 }
2089
2090 =item _date_pretty
2091
2092 Returns a string with the date, for example: "3/20/2008", localized for the
2093 customer.  Use _date_pretty_unlocalized for non-end-customer display use.
2094
2095 =cut
2096
2097 sub _date_pretty {
2098   my $self = shift;
2099   $self->time2str_local('short', $self->_date);
2100 }
2101
2102 =item _date_pretty_unlocalized
2103
2104 Returns a string with the date, for example: "3/20/2008", in the format
2105 configured for the back-office.  Use _date_pretty for end-customer display use.
2106
2107 =cut
2108
2109 sub _date_pretty_unlocalized {
2110   my $self = shift;
2111   time2str($date_format, $self->_date);
2112 }
2113
2114 =item email HASHREF
2115
2116 Emails this template.
2117
2118 Options are passed as a hashref.  Available options:
2119
2120 =over 4
2121
2122 =item from
2123
2124 If specified, overrides the default From: address.
2125
2126 =item notice_name
2127
2128 If specified, overrides the name of the sent document ("Invoice" or "Quotation")
2129
2130 =item template
2131
2132 (Deprecated) If specified, is the name of a suffix for alternate template files.
2133
2134 =back
2135
2136 Options accepted by generate_email can also be used.
2137
2138 =cut
2139
2140 sub email {
2141   my $self = shift;
2142   my $opt = shift || {};
2143   if ($opt and !ref($opt)) {
2144     die ref($self). '->email called with positional parameters';
2145   }
2146
2147   return if $self->hide;
2148
2149   my $error = send_email(
2150     $self->generate_email(
2151       'subject'     => $self->email_subject($opt->{template}),
2152       %$opt, # template, etc.
2153     )
2154   );
2155
2156   die "can't email: $error\n" if $error;
2157 }
2158
2159 =item generate_email OPTION => VALUE ...
2160
2161 Options:
2162
2163 =over 4
2164
2165 =item from
2166
2167 sender address, required
2168
2169 =item template
2170
2171 alternate template name, optional
2172
2173 =item subject
2174
2175 email subject, optional
2176
2177 =item notice_name
2178
2179 notice name instead of "Invoice", optional
2180
2181 =back
2182
2183 Returns an argument list to be passed to L<FS::Misc/send_email>.
2184
2185 =cut
2186
2187 use MIME::Entity;
2188 use Encode;
2189
2190 sub generate_email {
2191
2192   my $self = shift;
2193   my %args = @_;
2194   my $conf = $self->conf;
2195
2196   my $me = '[FS::Template_Mixin::generate_email]';
2197
2198   my %return = (
2199     'from'      => $args{'from'},
2200     'subject'   => ($args{'subject'} || $self->email_subject),
2201     'custnum'   => $self->custnum,
2202     'msgtype'   => 'invoice',
2203   );
2204
2205   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
2206
2207   my $cust_main = $self->cust_main;
2208
2209   if (ref($args{'to'}) eq 'ARRAY') {
2210     $return{'to'} = $args{'to'};
2211   } elsif ( $cust_main ) {
2212     $return{'to'} = [ $cust_main->invoicing_list_emailonly ];
2213   }
2214
2215   my $tc = $self->template_conf;
2216
2217   my @text; # array of lines
2218   my $html; # a big string
2219   my @related_parts; # will contain the text/HTML alternative, and images
2220   my $related; # will contain the multipart/related object
2221
2222   if ( $conf->exists($tc. 'email_pdf') ) {
2223     if ( my $msgnum = $conf->config($tc.'email_pdf_msgnum') ) {
2224
2225       warn "$me using '${tc}email_pdf_msgnum' in multipart message"
2226         if $DEBUG;
2227
2228       my $msg_template = FS::msg_template->by_key($msgnum)
2229         or die "${tc}email_pdf_msgnum $msgnum not found\n";
2230       my %prepared = $msg_template->prepare(
2231         cust_main => $self->cust_main,
2232         object    => $self
2233       );
2234
2235       @text = split(/(?=\n)/, $prepared{'text_body'});
2236       $html = $prepared{'html_body'};
2237
2238     } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) {
2239
2240       warn "$me using '${tc}email_pdf_note' in multipart message"
2241         if $DEBUG;
2242       @text = $conf->config($tc.'email_pdf_note');
2243       $html = join('<BR>', @text);
2244   
2245     } # else use the plain text invoice
2246   }
2247
2248   if (!@text) {
2249
2250     if ( $conf->exists($tc.'template') ) {
2251
2252       warn "$me generating plain text invoice"
2253         if $DEBUG;
2254
2255       @text = $self->print_text(\%args);
2256
2257     } else {
2258
2259       warn "$me no plain text version exists; sending empty message body"
2260         if $DEBUG;
2261
2262     }
2263
2264   }
2265
2266   my $text_part = build MIME::Entity (
2267     'Type'        => 'text/plain',
2268     'Encoding'    => 'quoted-printable',
2269     'Charset'     => 'UTF-8',
2270     #'Encoding'    => '7bit',
2271     'Data'        => [
2272       map
2273         { Encode::encode('UTF-8', $_, Encode::FB_WARN | Encode::LEAVE_SRC ) }
2274         @text
2275     ],
2276     'Disposition' => 'inline',
2277   );
2278
2279   if (!$html) {
2280
2281     if ( $conf->exists($tc.'html') ) {
2282       warn "$me generating HTML invoice"
2283         if $DEBUG;
2284
2285       $args{'from'} =~ /\@([\w\.\-]+)/;
2286       my $from = $1 || 'example.com';
2287       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2288
2289       my $logo;
2290       my $agentnum = $cust_main ? $cust_main->agentnum
2291                                 : $self->prospect_main->agentnum;
2292       if ( defined($args{'template'}) && length($args{'template'})
2293            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
2294          )
2295       {
2296         $logo = 'logo_'. $args{'template'}. '.png';
2297       } else {
2298         $logo = "logo.png";
2299       }
2300       my $image_data = $conf->config_binary( $logo, $agentnum);
2301
2302       push @related_parts, build MIME::Entity
2303         'Type'       => 'image/png',
2304         'Encoding'   => 'base64',
2305         'Data'       => $image_data,
2306         'Filename'   => 'logo.png',
2307         'Content-ID' => "<$content_id>",
2308       ;
2309    
2310       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
2311         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2312         push @related_parts, build MIME::Entity
2313           'Type'       => 'image/png',
2314           'Encoding'   => 'base64',
2315           'Data'       => $self->invoice_barcode(0),
2316           'Filename'   => 'barcode.png',
2317           'Content-ID' => "<$barcode_content_id>",
2318         ;
2319         $args{'barcode_cid'} = $barcode_content_id;
2320       }
2321
2322       $html = $self->print_html({ 'cid'=>$content_id, %args });
2323     }
2324
2325   }
2326
2327   if ( $html ) {
2328
2329     warn "$me creating HTML/text multipart message"
2330       if $DEBUG;
2331
2332     $return{'nobody'} = 1;
2333
2334     my $alternative = build MIME::Entity
2335       'Type'        => 'multipart/alternative',
2336       #'Encoding'    => '7bit',
2337       'Disposition' => 'inline'
2338     ;
2339
2340     if ( @text ) {
2341       $alternative->add_part($text_part);
2342     }
2343
2344     $alternative->attach(
2345       'Type'        => 'text/html',
2346       'Encoding'    => 'quoted-printable',
2347       'Data'        => [ '<html>',
2348                          '  <head>',
2349                          '    <title>',
2350                          '      '. encode_entities($return{'subject'}), 
2351                          '    </title>',
2352                          '  </head>',
2353                          '  <body bgcolor="#e8e8e8">',
2354                          Encode::encode(
2355                            'UTF-8',
2356                            $html,
2357                            Encode::FB_WARN | Encode::LEAVE_SRC
2358                          ),
2359                          '  </body>',
2360                          '</html>',
2361                        ],
2362       'Disposition' => 'inline',
2363       #'Filename'    => 'invoice.pdf',
2364     );
2365
2366     unshift @related_parts, $alternative;
2367
2368     $related = build MIME::Entity 'Type'     => 'multipart/related',
2369                                   'Encoding' => '7bit';
2370
2371     #false laziness w/Misc::send_email
2372     $related->head->replace('Content-type',
2373       $related->mime_type.
2374       '; boundary="'. $related->head->multipart_boundary. '"'.
2375       '; type=multipart/alternative'
2376     );
2377
2378     $related->add_part($_) foreach @related_parts;
2379
2380   }
2381
2382   my @otherparts = ();
2383   if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
2384
2385     if ( $conf->config('voip-cdr_email_attach') eq 'zip' ) {
2386
2387       my $data = join('', map "$_\n",
2388                    $self->call_details(prepend_billed_number=>1)
2389                  );
2390
2391       my $zip = new Archive::Zip;
2392       my $file = $zip->addString( $data, 'usage-'.$self->invnum.'.csv' );
2393       $file->desiredCompressionMethod( COMPRESSION_DEFLATED );
2394
2395       my $zipdata = '';
2396       my $SH = IO::Scalar->new(\$zipdata);
2397       my $status = $zip->writeToFileHandle($SH);
2398       die "Error zipping CDR attachment: $!" unless $status == AZ_OK;
2399
2400       push @otherparts, build MIME::Entity
2401         'Type'        => 'application/zip',
2402         'Encoding'    => 'base64',
2403         'Data'        => $zipdata,
2404         'Disposition' => 'attachment',
2405         'Filename'    => 'usage-'. $self->invnum. '.zip',
2406       ;
2407
2408     } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
2409  
2410       push @otherparts, build MIME::Entity
2411         'Type'        => 'text/csv',
2412         'Encoding'    => '7bit',
2413         'Data'        => [ map { "$_\n" }
2414                              $self->call_details('prepend_billed_number' => 1)
2415                          ],
2416         'Disposition' => 'attachment',
2417         'Filename'    => 'usage-'. $self->invnum. '.csv',
2418       ;
2419
2420     }
2421
2422   }
2423
2424   if ( $conf->exists($tc.'email_pdf') ) {
2425
2426     #attaching pdf too:
2427     # multipart/mixed
2428     #   multipart/related
2429     #     multipart/alternative
2430     #       text/plain
2431     #       text/html
2432     #     image/png
2433     #   application/pdf
2434
2435     my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
2436     push @otherparts, $pdf;
2437   }
2438
2439   if (@otherparts) {
2440     $return{'content-type'} = 'multipart/mixed'; # of the outer container
2441     if ( $html ) {
2442       $return{'mimeparts'} = [ $related, @otherparts ];
2443       $return{'type'} = 'multipart/related'; # of the first part
2444     } else {
2445       $return{'mimeparts'} = [ $text_part, @otherparts ];
2446       $return{'type'} = 'text/plain';
2447     }
2448   } elsif ( $html ) { # no PDF or CSV, strip the outer container
2449     $return{'mimeparts'} = \@related_parts;
2450     $return{'content-type'} = 'multipart/related';
2451     $return{'type'} = 'multipart/alternative';
2452   } else { # no HTML either
2453     $return{'body'} = \@text;
2454     $return{'content-type'} = 'text/plain';
2455   }
2456
2457   %return;
2458
2459 }
2460
2461 =item mimebuild_pdf
2462
2463 Returns a list suitable for passing to MIME::Entity->build(), representing
2464 this quotation or invoice as PDF attachment.
2465
2466 =cut
2467
2468 sub mimebuild_pdf {
2469   my $self = shift;
2470   (
2471     'Type'        => 'application/pdf',
2472     'Encoding'    => 'base64',
2473     'Data'        => [ $self->print_pdf(@_) ],
2474     'Disposition' => 'attachment',
2475     'Filename'    => $self->pdf_filename,
2476   );
2477 }
2478
2479 =item postal_mail_fsinc
2480
2481 Sends this invoice to the Freeside Internet Services, Inc. print and mail
2482 service.
2483
2484 =cut
2485
2486 use CAM::PDF;
2487 use IO::Socket::SSL;
2488 use LWP::UserAgent;
2489 use HTTP::Request::Common qw( POST );
2490 use Cpanel::JSON::XS;
2491 use MIME::Base64;
2492 sub postal_mail_fsinc {
2493   my ( $self, %opt ) = @_;
2494
2495   my $url = 'https://ws.freeside.biz/print';
2496
2497   my $cust_main = $self->cust_main;
2498   my $agentnum = $cust_main->agentnum;
2499   my $bill_location = $cust_main->bill_location;
2500
2501   die "Extra charges for international mailing; contact support\@freeside.biz to enable\n"
2502     if $bill_location->country ne 'US';
2503
2504   my $conf = new FS::Conf;
2505
2506   my @company_address = $conf->config('company_address', $agentnum);
2507   my ( $company_address1, $company_address2, $company_city, $company_state, $company_zip );
2508   if ( $company_address[2] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2509     $company_address1 = $company_address[0];
2510     $company_address2 = $company_address[1];
2511     $company_city  = $1;
2512     $company_state = $2;
2513     $company_zip   = $3;
2514   } elsif ( $company_address[1] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2515     $company_address1 = $company_address[0];
2516     $company_address2 = '';
2517     $company_city  = $1;
2518     $company_state = $2;
2519     $company_zip   = $3;
2520   } else {
2521     die "Unparsable company_address; contact support\@freeside.biz\n";
2522   }
2523   $company_city =~ s/,$//;
2524
2525   my $file = $self->print_pdf(%opt, 'no_addresses' => 1);
2526   my $pages = CAM::PDF->new($file)->numPages;
2527
2528   my $ua = LWP::UserAgent->new(
2529     'ssl_opts' => { 
2530       verify_hostname => 0,
2531       SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
2532       SSL_version     => 'SSLv3',
2533     }
2534   );
2535   my $response = $ua->request( POST $url, [
2536     'support-key'      => scalar($conf->config('support-key')),
2537     'file'             => encode_base64($file),
2538     'pages'            => $pages,
2539
2540     #from:
2541     'company_name'     => scalar( $conf->config('company_name', $agentnum) ),
2542     'company_address1' => $company_address1,
2543     'company_address2' => $company_address2,
2544     'company_city'     => $company_city,
2545     'company_state'    => $company_state,
2546     'company_zip'      => $company_zip,
2547     'company_country'  => 'US',
2548     'company_phonenum' => scalar($conf->config('company_phonenum', $agentnum)),
2549     'company_email'    => scalar($conf->config('invoice_from', $agentnum)),
2550
2551     #to:
2552     'name'             => ( $cust_main->payname
2553                               && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/
2554                                 ? $cust_main->payname
2555                                 : $cust_main->contact_firstlast
2556                           ),
2557     'company'          => $cust_main->company,
2558     'address1'         => $bill_location->address1,
2559     'address2'         => $bill_location->address2,
2560     'city'             => $bill_location->city,
2561     'state'            => $bill_location->state,
2562     'zip'              => $bill_location->zip,
2563     'country'          => $bill_location->country,
2564   ]);
2565
2566   die "Print connection error: ". $response->message.
2567       ' ('. $response->as_string. ")\n"
2568     unless $response->is_success;
2569
2570   local $@;
2571   my $content = eval { decode_json($response->content) };
2572   die "Print JSON error : $@\n" if $@;
2573
2574   die $content->{error}."\n"
2575     if $content->{error};
2576
2577   #TODO: store this so we can query for a status later
2578   warn "Invoice printed, ID ". $content->{id}. "\n";
2579
2580   $content->{id};
2581 }
2582
2583 =item _items_sections OPTIONS
2584
2585 Generate section information for all items appearing on this invoice.
2586 This will only be called for multi-section invoices.
2587
2588 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
2589 related display records (L<FS::cust_bill_pkg_display>) and organize 
2590 them into two groups ("early" and "late" according to whether they come 
2591 before or after the total), then into sections.  A subtotal is calculated 
2592 for each section.
2593
2594 Section descriptions are returned in sort weight order.  Each consists 
2595 of a hash containing:
2596
2597 description: the package category name, escaped
2598 subtotal: the total charges in that section
2599 tax_section: a flag indicating that the section contains only tax charges
2600 summarized: same as tax_section, for some reason
2601 sort_weight: the package category's sort weight
2602
2603 If 'condense' is set on the display record, it also contains everything 
2604 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
2605 coderefs to generate parts of the invoice.  This is not advised.
2606
2607 The method returns two arrayrefs, one of "early" sections and one of "late"
2608 sections.
2609
2610 OPTIONS may include:
2611
2612 by_location: a flag to divide the invoice into sections by location.  
2613 Each section hash will have a 'location' element containing a hashref of 
2614 the location fields (see L<FS::cust_location>).  The section description
2615 will be the location label, but the template can use any of the location 
2616 fields to create a suitable label.
2617
2618 by_category: a flag to divide the invoice into sections using display 
2619 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
2620 behavior.  Each section hash will have a 'category' element containing
2621 the section name from the display record (which probably equals the 
2622 category name of the package, but may not in some cases).
2623
2624 summary: a flag indicating that this is a summary-format invoice.
2625 Turning this on has the following effects:
2626 - Ignores display items with the 'summary' flag.
2627 - Places all sections in the "early" group even if they have post_total.
2628 - Creates sections for all non-disabled package categories, even if they 
2629 have no charges on this invoice, as well as a section with no name.
2630
2631 escape: an escape function to use for section titles.
2632
2633 extra_sections: an arrayref of additional sections to return after the 
2634 sorted list.  If there are any of these, section subtotals exclude 
2635 usage charges.
2636
2637 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
2638 passed through to C<_condense_section()>.
2639
2640 =cut
2641
2642 use vars qw(%pkg_category_cache);
2643 sub _items_sections {
2644   my $self = shift;
2645   my %opt = @_;
2646   
2647   my $escape = $opt{escape};
2648   my @extra_sections = @{ $opt{extra_sections} || [] };
2649
2650   # $subtotal{$locationnum}{$categoryname} = amount.
2651   # if we're not using by_location, $locationnum is undef.
2652   # if we're not using by_category, you guessed it, $categoryname is undef.
2653   # if we're not using either one, we shouldn't be here in the first place...
2654   my %subtotal = ();
2655   my %late_subtotal = ();
2656   my %not_tax = ();
2657
2658   # About tax items + multisection invoices:
2659   # If either invoice_*summary option is enabled, AND there is a 
2660   # package category with the name of the tax, then there will be 
2661   # a display record assigning the tax item to that category.
2662   #
2663   # However, the taxes are always placed in the "Taxes, Surcharges,
2664   # and Fees" section regardless of that.  The only effect of the 
2665   # display record is to create a subtotal for the summary page.
2666
2667   # cache these
2668   my $pkg_hash = $self->cust_pkg_hash;
2669
2670   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2671   {
2672
2673       my $usage = $cust_bill_pkg->usage;
2674
2675       my $locationnum;
2676       if ( $opt{by_location} ) {
2677         if ( $cust_bill_pkg->pkgnum ) {
2678           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2679         } else {
2680           $locationnum = '';
2681         }
2682       } else {
2683         $locationnum = undef;
2684       }
2685
2686       # as in _items_cust_pkg, if a line item has no display records,
2687       # cust_bill_pkg_display() returns a default record for it
2688
2689       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2690         next if ( $display->summary && $opt{summary} );
2691
2692         #my $section = $display->section;
2693         #false laziness with the method, but for efficiency inside this loop
2694         my $section = $display->get('section');
2695         if ( !$section && !$cust_bill_pkg->hidden ) {
2696           $section = $cust_bill_pkg->get('categoryname'); #cust_bill->cust_bill_pkg added it (XXX quotations / quotation_section)
2697         }
2698
2699         my $type    = $display->type;
2700         # Set $section = undef if we're sectioning by location and this
2701         # line item _has_ a location (i.e. isn't a fee).
2702         $section = undef if $locationnum;
2703
2704         # set this flag if the section is not tax-only
2705         $not_tax{$locationnum}{$section} = 1
2706           if $cust_bill_pkg->pkgnum  or $cust_bill_pkg->feepart;
2707
2708         # there's actually a very important piece of logic buried in here:
2709         # incrementing $late_subtotal{$section} CREATES 
2710         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
2711         # to define the list of late sections, and likewise keys(%subtotal).
2712         # When _items_cust_bill_pkg is called to generate line items for 
2713         # real, it will be called with 'section' => $section for each 
2714         # of these.
2715         if ( $display->post_total && !$opt{summary} ) {
2716           if (! $type || $type eq 'S') {
2717             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2718               if $cust_bill_pkg->setup != 0
2719               || $cust_bill_pkg->setup_show_zero;
2720           }
2721
2722           if (! $type) {
2723             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2724               if $cust_bill_pkg->recur != 0
2725               || $cust_bill_pkg->recur_show_zero;
2726           }
2727
2728           if ($type && $type eq 'R') {
2729             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2730               if $cust_bill_pkg->recur != 0
2731               || $cust_bill_pkg->recur_show_zero;
2732           }
2733           
2734           if ($type && $type eq 'U') {
2735             $late_subtotal{$locationnum}{$section} += $usage
2736               unless scalar(@extra_sections);
2737           }
2738
2739         } else { # it's a pre-total (normal) section
2740
2741           # skip tax items unless they're explicitly included in a section
2742           next if $cust_bill_pkg->pkgnum == 0 and
2743                   ! $cust_bill_pkg->feepart   and
2744                   ! $section;
2745
2746           if ( $type eq 'S' ) {
2747             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2748               if $cust_bill_pkg->setup != 0
2749               || $cust_bill_pkg->setup_show_zero;
2750           } elsif ( $type eq 'R' ) {
2751             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2752               if $cust_bill_pkg->recur != 0
2753               || $cust_bill_pkg->recur_show_zero;
2754           } elsif ( $type eq 'U' ) {
2755             $subtotal{$locationnum}{$section} += $usage
2756               unless scalar(@extra_sections);
2757           } elsif ( !$type ) {
2758             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2759                                                + $cust_bill_pkg->recur;
2760           }
2761
2762         }
2763
2764       }
2765
2766   }
2767
2768   %pkg_category_cache = ();
2769
2770   # summary invoices need subtotals for all non-disabled package categories,
2771   # even if they're zero
2772   # but currently assume that there are no location sections, or at least
2773   # that the summary page doesn't care about them
2774   if ( $opt{summary} ) {
2775     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2776       $subtotal{''}{$category->categoryname} ||= 0;
2777     }
2778     $subtotal{''}{''} ||= 0;
2779   }
2780
2781   my @sections;
2782   foreach my $post_total (0,1) {
2783     my @these;
2784     my $s = $post_total ? \%late_subtotal : \%subtotal;
2785     foreach my $locationnum (keys %$s) {
2786       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2787         my $section = {
2788                         'subtotal'    => $s->{$locationnum}{$sectionname},
2789                         'sort_weight' => 0,
2790                       };
2791         if ( $locationnum ) {
2792           $section->{'locationnum'} = $locationnum;
2793           my $location = FS::cust_location->by_key($locationnum);
2794           $section->{'description'} = &{ $escape }($location->location_label);
2795           # Better ideas? This will roughly group them by proximity, 
2796           # which alpha sorting on any of the address fields won't.
2797           # Sorting by locationnum is meaningless.
2798           # We have to sort on _something_ or the order may change 
2799           # randomly from one invoice to the next, which will confuse
2800           # people.
2801           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2802                                       $locationnum;
2803           $section->{'location'} = {
2804             label_prefix => &{ $escape }($location->label_prefix),
2805             map { $_ => &{ $escape }($location->get($_)) }
2806               $location->fields
2807           };
2808         } else {
2809           $section->{'category'} = $sectionname;
2810           $section->{'description'} = &{ $escape }($sectionname);
2811           if ( _pkg_category($sectionname) ) {
2812             $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2813             if ( _pkg_category($sectionname)->condense ) {
2814               $section = { %$section, $self->_condense_section($opt{format}) };
2815             }
2816           }
2817         }
2818         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2819           # then it's a tax-only section
2820           $section->{'summarized'} = 'Y';
2821           $section->{'tax_section'} = 'Y';
2822         }
2823         push @these, $section;
2824       } # foreach $sectionname
2825     } #foreach $locationnum
2826     push @these, @extra_sections if $post_total == 0;
2827     # need an alpha sort for location sections, because postal codes can 
2828     # be non-numeric
2829     $sections[ $post_total ] = [ sort {
2830       $opt{'by_location'} ? 
2831         ($a->{sort_weight} cmp $b->{sort_weight}) :
2832         ($a->{sort_weight} <=> $b->{sort_weight})
2833       } @these ];
2834   } #foreach $post_total
2835
2836   return @sections; # early, late
2837 }
2838
2839 #helper subs for above
2840
2841 sub cust_pkg_hash {
2842   my $self = shift;
2843   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2844 }
2845
2846 sub _pkg_category {
2847   my $categoryname = shift;
2848   $pkg_category_cache{$categoryname} ||=
2849     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2850 }
2851
2852 my %condensed_format = (
2853   'label' => [ qw( Description Qty Amount ) ],
2854   'fields' => [
2855                 sub { shift->{description} },
2856                 sub { shift->{quantity} },
2857                 sub { my($href, %opt) = @_;
2858                       ($opt{dollar} || ''). $href->{amount};
2859                     },
2860               ],
2861   'align'  => [ qw( l r r ) ],
2862   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2863   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2864 );
2865
2866 sub _condense_section {
2867   my ( $self, $format ) = ( shift, shift );
2868   ( 'condensed' => 1,
2869     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2870       qw( description_generator
2871           header_generator
2872           total_generator
2873           total_line_generator
2874         )
2875   );
2876 }
2877
2878 sub _condensed_generator_defaults {
2879   my ( $self, $format ) = ( shift, shift );
2880   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2881 }
2882
2883 my %html_align = (
2884   'c' => 'center',
2885   'l' => 'left',
2886   'r' => 'right',
2887 );
2888
2889 sub _condensed_header_generator {
2890   my ( $self, $format ) = ( shift, shift );
2891
2892   my ( $f, $prefix, $suffix, $separator, $column ) =
2893     _condensed_generator_defaults($format);
2894
2895   if ($format eq 'latex') {
2896     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2897     $suffix = "\\\\\n\\hline";
2898     $separator = "&\n";
2899     $column =
2900       sub { my ($d,$a,$s,$w) = @_;
2901             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2902           };
2903   } elsif ( $format eq 'html' ) {
2904     $prefix = '<th></th>';
2905     $suffix = '';
2906     $separator = '';
2907     $column =
2908       sub { my ($d,$a,$s,$w) = @_;
2909             return qq!<th align="$html_align{$a}">$d</th>!;
2910       };
2911   }
2912
2913   sub {
2914     my @args = @_;
2915     my @result = ();
2916
2917     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2918       push @result,
2919         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2920     }
2921
2922     $prefix. join($separator, @result). $suffix;
2923   };
2924
2925 }
2926
2927 sub _condensed_description_generator {
2928   my ( $self, $format ) = ( shift, shift );
2929
2930   my ( $f, $prefix, $suffix, $separator, $column ) =
2931     _condensed_generator_defaults($format);
2932
2933   my $money_char = '$';
2934   if ($format eq 'latex') {
2935     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2936     $suffix = '\\\\';
2937     $separator = " & \n";
2938     $column =
2939       sub { my ($d,$a,$s,$w) = @_;
2940             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2941           };
2942     $money_char = '\\dollar';
2943   }elsif ( $format eq 'html' ) {
2944     $prefix = '"><td align="center"></td>';
2945     $suffix = '';
2946     $separator = '';
2947     $column =
2948       sub { my ($d,$a,$s,$w) = @_;
2949             return qq!<td align="$html_align{$a}">$d</td>!;
2950       };
2951     #$money_char = $conf->config('money_char') || '$';
2952     $money_char = '';  # this is madness
2953   }
2954
2955   sub {
2956     #my @args = @_;
2957     my $href = shift;
2958     my @result = ();
2959
2960     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2961       my $dollar = '';
2962       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2963       push @result,
2964         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2965                     map { $f->{$_}->[$i] } qw(align span width)
2966                   );
2967     }
2968
2969     $prefix. join( $separator, @result ). $suffix;
2970   };
2971
2972 }
2973
2974 sub _condensed_total_generator {
2975   my ( $self, $format ) = ( shift, shift );
2976
2977   my ( $f, $prefix, $suffix, $separator, $column ) =
2978     _condensed_generator_defaults($format);
2979   my $style = '';
2980
2981   if ($format eq 'latex') {
2982     $prefix = "& ";
2983     $suffix = "\\\\\n";
2984     $separator = " & \n";
2985     $column =
2986       sub { my ($d,$a,$s,$w) = @_;
2987             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2988           };
2989   }elsif ( $format eq 'html' ) {
2990     $prefix = '';
2991     $suffix = '';
2992     $separator = '';
2993     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2994     $column =
2995       sub { my ($d,$a,$s,$w) = @_;
2996             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2997       };
2998   }
2999
3000
3001   sub {
3002     my @args = @_;
3003     my @result = ();
3004
3005     #  my $r = &{$f->{fields}->[$i]}(@args);
3006     #  $r .= ' Total' unless $i;
3007
3008     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3009       push @result,
3010         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3011                     map { $f->{$_}->[$i] } qw(align span width)
3012                   );
3013     }
3014
3015     $prefix. join( $separator, @result ). $suffix;
3016   };
3017
3018 }
3019
3020 =item total_line_generator FORMAT
3021
3022 Returns a coderef used for generation of invoice total line items for this
3023 usage_class.  FORMAT is either html or latex
3024
3025 =cut
3026
3027 # should not be used: will have issues with hash element names (description vs
3028 # total_item and amount vs total_amount -- another array of functions?
3029
3030 sub _condensed_total_line_generator {
3031   my ( $self, $format ) = ( shift, shift );
3032
3033   my ( $f, $prefix, $suffix, $separator, $column ) =
3034     _condensed_generator_defaults($format);
3035   my $style = '';
3036
3037   if ($format eq 'latex') {
3038     $prefix = "& ";
3039     $suffix = "\\\\\n";
3040     $separator = " & \n";
3041     $column =
3042       sub { my ($d,$a,$s,$w) = @_;
3043             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3044           };
3045   }elsif ( $format eq 'html' ) {
3046     $prefix = '';
3047     $suffix = '';
3048     $separator = '';
3049     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3050     $column =
3051       sub { my ($d,$a,$s,$w) = @_;
3052             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3053       };
3054   }
3055
3056
3057   sub {
3058     my @args = @_;
3059     my @result = ();
3060
3061     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3062       push @result,
3063         &{$column}( &{$f->{fields}->[$i]}(@args),
3064                     map { $f->{$_}->[$i] } qw(align span width)
3065                   );
3066     }
3067
3068     $prefix. join( $separator, @result ). $suffix;
3069   };
3070
3071 }
3072
3073 =item _items_pkg [ OPTIONS ]
3074
3075 Return line item hashes for each package item on this invoice. Nearly 
3076 equivalent to 
3077
3078 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
3079
3080 OPTIONS are passed through to _items_cust_bill_pkg, and should include
3081 'format' and 'escape_function' at minimum.
3082
3083 To produce items for a specific invoice section, OPTIONS should include
3084 'section', a hashref containing 'category' and/or 'locationnum' keys.
3085
3086 'section' may also contain a key named 'condensed'. If this is present
3087 and has a true value, _items_pkg will try to merge identical items into items
3088 with 'quantity' equal to the number of items (not the sum of their separate
3089 quantities, for some reason).
3090
3091 =cut
3092
3093 sub _items_nontax {
3094   my $self = shift;
3095   # The order of these is important.  Bundled line items will be merged into
3096   # the most recent non-hidden item, so it needs to be the one with:
3097   # - the same pkgnum
3098   # - the same start date
3099   # - no pkgpart_override
3100   #
3101   # So: sort by pkgnum,
3102   # then by sdate
3103   # then sort the base line item before any overrides
3104   # then sort hidden before non-hidden add-ons
3105   # then sort by override pkgpart (for consistency)
3106   sort { $a->pkgnum <=> $b->pkgnum        or
3107          $a->sdate  <=> $b->sdate         or
3108          ($a->pkgpart_override ? 0 : -1)  or
3109          ($b->pkgpart_override ? 0 : 1)   or
3110          $b->hidden cmp $a->hidden        or
3111          $a->pkgpart_override <=> $b->pkgpart_override
3112        }
3113   # and of course exclude taxes and fees
3114   grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
3115 }
3116
3117 sub _items_fee {
3118   my $self = shift;
3119   my %options = @_;
3120   my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
3121   my $escape_function = $options{escape_function};
3122
3123   my $locale = $self->cust_main
3124              ? $self->cust_main->locale
3125              : $self->prospect_main->locale;
3126
3127   my @items;
3128   foreach my $cust_bill_pkg (@cust_bill_pkg) {
3129     # cache this, so we don't look it up again in every section
3130     my $part_fee = $cust_bill_pkg->get('part_fee')
3131        || $cust_bill_pkg->part_fee;
3132     $cust_bill_pkg->set('part_fee', $part_fee);
3133     if (!$part_fee) {
3134       #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
3135       warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
3136       next;
3137     }
3138     if ( exists($options{section}) and exists($options{section}{category}) )
3139     {
3140       my $categoryname = $options{section}{category};
3141       # then filter for items that have that section
3142       if ( $part_fee->categoryname ne $categoryname ) {
3143         warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
3144         next;
3145       }
3146     } # otherwise include them all in the main section
3147     # XXX what to do when sectioning by location?
3148     
3149     my @ext_desc;
3150     my %base_invnums; # invnum => invoice date
3151     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
3152       if ($_->base_invnum) {
3153         # XXX what if base_bill has been voided?
3154         my $base_bill = FS::cust_bill->by_key($_->base_invnum);
3155         my $base_date = $self->time2str_local('short', $base_bill->_date)
3156           if $base_bill;
3157         $base_invnums{$_->base_invnum} = $base_date || '';
3158       }
3159     }
3160     foreach (sort keys(%base_invnums)) {
3161       next if $_ == $self->invnum;
3162       # per convention, we must escape ext_description lines
3163       push @ext_desc,
3164         &{$escape_function}(
3165           $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
3166         );
3167     }
3168     my $desc = $part_fee->itemdesc_locale($locale);
3169     # but not escape the base description line
3170
3171     my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
3172       if $options{section_with_taxes};
3173
3174     push @items,
3175       { feepart     => $cust_bill_pkg->feepart,
3176         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
3177         description => $desc,
3178         pkg_tax     => \@pkg_tax,
3179         ext_description => \@ext_desc,
3180         # sdate/edate?
3181       };
3182   }
3183   @items;
3184 }
3185
3186 sub _items_pkg {
3187   my $self = shift;
3188   my %options = @_;
3189
3190   warn "$me _items_pkg searching for all package line items\n"
3191     if $DEBUG > 1;
3192
3193   my @cust_bill_pkg = $self->_items_nontax;
3194
3195   warn "$me _items_pkg filtering line items\n"
3196     if $DEBUG > 1;
3197   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3198
3199   if ($options{section} && $options{section}->{condensed}) {
3200
3201     warn "$me _items_pkg condensing section\n"
3202       if $DEBUG > 1;
3203
3204     my %itemshash = ();
3205     local $Storable::canonical = 1;
3206     foreach ( @items ) {
3207       my $item = { %$_ };
3208       delete $item->{ref};
3209       delete $item->{ext_description};
3210       my $key = freeze($item);
3211       $itemshash{$key} ||= 0;
3212       $itemshash{$key} ++; # += $item->{quantity};
3213     }
3214     @items = sort { $a->{description} cmp $b->{description} }
3215              map { my $i = thaw($_);
3216                    $i->{quantity} = $itemshash{$_};
3217                    $i->{amount} =
3218                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3219                    $i;
3220                  }
3221              keys %itemshash;
3222   }
3223
3224   warn "$me _items_pkg returning ". scalar(@items). " items\n"
3225     if $DEBUG > 1;
3226
3227   @items;
3228 }
3229
3230 sub _taxsort {
3231   return 0 unless $a->itemdesc cmp $b->itemdesc;
3232   return -1 if $b->itemdesc eq 'Tax';
3233   return 1 if $a->itemdesc eq 'Tax';
3234   return -1 if $b->itemdesc eq 'Other surcharges';
3235   return 1 if $a->itemdesc eq 'Other surcharges';
3236   $a->itemdesc cmp $b->itemdesc;
3237 }
3238
3239 sub _items_tax {
3240   my $self = shift;
3241   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
3242     $self->cust_bill_pkg;
3243   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3244
3245   if ( $self->conf->exists('always_show_tax') ) {
3246     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
3247     if (0 == grep { $_->{description} eq $itemdesc } @items) {
3248       push @items,
3249         { 'description' => $itemdesc,
3250           'amount'      => 0.00 };
3251     }
3252   }
3253   @items;
3254 }
3255
3256 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
3257
3258 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
3259 list of hashrefs describing the line items they generate on the invoice.
3260
3261 OPTIONS may include:
3262
3263 format: the invoice format.
3264
3265 escape_function: the function used to escape strings.
3266
3267 DEPRECATED? (expensive, mostly unused?)
3268 format_function: the function used to format CDRs.
3269
3270 section: a hashref containing 'category' and/or 'locationnum'; if this 
3271 is present, only returns line items that belong to that category and/or
3272 location (whichever is defined).
3273
3274 multisection: a flag indicating that this is a multisection invoice,
3275 which does something complicated.
3276
3277 preref_callback: coderef run for each line item, code should return HTML to be
3278 displayed before that line item (quotations only)
3279
3280 section_with_taxes:  Look up and include applied taxes for each record
3281
3282 Returns a list of hashrefs, each of which may contain:
3283
3284 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
3285 ext_description, which is an arrayref of detail lines to show below 
3286 the package line.
3287
3288 =cut
3289
3290 sub _items_cust_bill_pkg {
3291   my $self = shift;
3292   my $conf = $self->conf;
3293   my $cust_bill_pkgs = shift;
3294   my %opt = @_;
3295
3296   my $format = $opt{format} || '';
3297   my $escape_function = $opt{escape_function} || sub { shift };
3298   my $format_function = $opt{format_function} || '';
3299   my $no_usage = $opt{no_usage} || '';
3300   my $unsquelched = $opt{unsquelched} || ''; #unused
3301   my ($section, $locationnum, $category);
3302   if ( $opt{section} ) {
3303     $category = $opt{section}->{category};
3304     $locationnum = $opt{section}->{locationnum};
3305   }
3306   my $summary_page = $opt{summary_page} || ''; #unused
3307   my $multisection = defined($category) || defined($locationnum);
3308   # this variable is the value of the config setting, not whether it applies
3309   # to this particular line item.
3310   my $discount_show_always = $conf->exists('discount-show-always');
3311
3312   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
3313
3314   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
3315
3316   my $agentnum = $self->agentnum;
3317
3318   # for location labels: use default location on the invoice date
3319   my $default_locationnum;
3320   if ( $conf->exists('invoice-all_pkg_addresses') ) {
3321     $default_locationnum = 0; # treat them all as non-default
3322   } elsif ( $self->custnum ) {
3323     my $h_cust_main;
3324     my @h_search = FS::h_cust_main->sql_h_search($self->_date);
3325     $h_cust_main = qsearchs({
3326         'table'     => 'h_cust_main',
3327         'hashref'   => { custnum => $self->custnum },
3328         'extra_sql' => $h_search[1],
3329         'addl_from' => $h_search[3],
3330     }) || $cust_main;
3331     $default_locationnum = $h_cust_main->ship_locationnum;
3332   } elsif ( $self->prospectnum ) {
3333     my $cust_location = qsearchs('cust_location',
3334       { prospectnum => $self->prospectnum,
3335         disabled => '' });
3336     $default_locationnum = $cust_location->locationnum if $cust_location;
3337   }
3338
3339   my @b = (); # accumulator for the line item hashes that we'll return
3340   my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
3341             # the 'current' line item hashes for setup, recur, usage, discount
3342   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
3343   {
3344     # if the current line item is waiting to go out, and the one we're about
3345     # to start is not bundled, then push out the current one and start a new
3346     # one.
3347     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3348       if ( $_ && !$cust_bill_pkg->hidden ) {
3349         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
3350         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3351         if (exists($_->{unit_amount})) {
3352           $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3353         }
3354         push @b, { %$_ };
3355         # we already decided to create this display line; don't reconsider it
3356         # now.
3357         #  if $_->{amount} != 0
3358         #  || $discount_show_always
3359         #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3360         #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3361         ;
3362         $_ = undef;
3363       }
3364     }
3365
3366     if ( $locationnum ) {
3367       # this is a location section; skip packages that aren't at this
3368       # service location.
3369       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
3370       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
3371               != $locationnum;
3372     }
3373
3374     # Consider display records for this item to determine if it belongs
3375     # in this section.  Note that if there are no display records, there
3376     # will be a default pseudo-record that includes all charge types 
3377     # and has no section name.
3378     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
3379                                   ? $cust_bill_pkg->cust_bill_pkg_display
3380                                   : ( $cust_bill_pkg );
3381
3382     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
3383          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
3384       if $DEBUG > 1;
3385
3386     if ( defined($category) ) {
3387       # then this is a package category section; process all display records
3388       # that belong to this section.
3389       @cust_bill_pkg_display = grep { $_->section eq $category }
3390                                 @cust_bill_pkg_display;
3391     } else {
3392       # otherwise, process all display records that aren't usage summaries
3393       # (I don't think there should be usage summaries if you aren't using 
3394       # category sections, but this is the historical behavior)
3395       @cust_bill_pkg_display = grep { !$_->summary }
3396                                 @cust_bill_pkg_display;
3397     }
3398
3399     my $classname = ''; # package class name, will fill in later
3400
3401     foreach my $display (@cust_bill_pkg_display) {
3402
3403       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
3404            $display->billpkgdisplaynum. "\n"
3405         if $DEBUG > 1;
3406
3407       my $type = $display->type;
3408
3409       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
3410       $desc = substr($desc, 0, $maxlength). '...'
3411         if $format eq 'latex' && length($desc) > $maxlength;
3412
3413       my %details_opt = ( 'format'          => $format,
3414                           'escape_function' => $escape_function,
3415                           'format_function' => $format_function,
3416                           'no_usage'        => $opt{'no_usage'},
3417                         );
3418
3419       my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
3420         if $opt{section_with_taxes};
3421
3422       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
3423         # XXX this should be pulled out into quotation_pkg
3424
3425         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
3426           if $DEBUG > 1;
3427         # quotation_pkgs are never fees, so don't worry about the case where
3428         # part_pkg is undefined
3429
3430         my @details = $cust_bill_pkg->details;
3431
3432         # and I guess they're never bundled either?
3433         if (( $cust_bill_pkg->setup != 0 ) || ( $cust_bill_pkg->setup_show_zero )) {
3434           my $description = $desc;
3435           $description .= ' Setup'
3436             if $cust_bill_pkg->recur != 0
3437             || $discount_show_always
3438             || $cust_bill_pkg->recur_show_zero;
3439           #push @b, {
3440           # keep it consistent, please
3441           $s = {
3442             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3443             'description' => $description,
3444             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3445             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
3446             'quantity'    => $cust_bill_pkg->quantity,
3447             'pkg_tax'     => \@pkg_tax,
3448             'ext_description' => \@details,
3449             'preref_html' => ( $opt{preref_callback}
3450                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3451                                  : ''
3452                              ),
3453           };
3454         }
3455         if (( $cust_bill_pkg->recur != 0 ) || ( $cust_bill_pkg->recur_show_zero )) {
3456           #push @b, {
3457           $r = {
3458             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3459             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
3460             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3461             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
3462             'quantity'    => $cust_bill_pkg->quantity,
3463             'pkg_tax'     => \@pkg_tax,
3464             'ext_description' => \@details,
3465            'preref_html'  => ( $opt{preref_callback}
3466                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3467                                  : ''
3468                              ),
3469           };
3470         }
3471
3472       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
3473         # a "normal" package line item (not a quotation, not a fee, not a tax)
3474
3475         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
3476           if $DEBUG > 1;
3477  
3478         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3479
3480         unless ( $cust_pkg ) {
3481           # There is no related row in cust_pkg for this cust_bill_pkg.pkgnum.
3482           # This invoice may have been broken by an unusual combination
3483           # of manually editing package dates, and aborted package changes
3484           # when the manually edited dates used are nonsensical.
3485
3486           my $error = sprintf
3487             'cust_bill_pkg(billpkgnum:%s) '.
3488             'is missing related row in cust_pkg(pkgnum:%s)! '.
3489             'cust_bill(invnum:%s) is corrupted by bad database data, '.
3490             'and should be investigated',
3491               $cust_bill_pkg->billpkgnum,
3492               $cust_bill_pkg->pkgnum,
3493               $cust_bill_pkg->invnum;
3494
3495           FS::Log->new('FS::cust_bill_pkg')->critical( $error );
3496           warn $error;
3497           next;
3498         }
3499
3500         my $part_pkg = $cust_pkg->part_pkg;
3501
3502         # which pkgpart to show for display purposes?
3503         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
3504
3505         # start/end dates for invoice formats that do nonstandard 
3506         # things with them
3507         my %item_dates = ();
3508         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
3509           unless $part_pkg->option('disable_line_item_date_ranges',1);
3510
3511         # not normally used, but pass this to the template anyway
3512         $classname = $part_pkg->classname;
3513
3514         if (    (!$type || $type eq 'S')
3515              && (    $cust_bill_pkg->setup != 0
3516                   || $cust_bill_pkg->setup_show_zero
3517                   || ($discount_show_always and $cust_bill_pkg->unitsetup > 0)
3518                 )
3519            )
3520          {
3521
3522           warn "$me _items_cust_bill_pkg adding setup\n"
3523             if $DEBUG > 1;
3524
3525           # append the word 'Setup' to the setup line if there's going to be
3526           # a recur line for the same package (i.e. not a one-time charge) 
3527           # XXX localization
3528           my $description = $desc;
3529           $description .= ' Setup'
3530             if $cust_bill_pkg->recur != 0
3531             || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3532             || $cust_bill_pkg->recur_show_zero;
3533
3534           my $disable_date_ranges =
3535                $opt{disable_line_item_date_ranges}
3536             || $part_pkg->option('disable_line_item_date_ranges', 1);
3537
3538           $description .= $cust_bill_pkg->time_period_pretty(
3539                             $part_pkg,
3540                             $agentnum,
3541                             disable_date_ranges => $disable_date_ranges,
3542                           )
3543             if $part_pkg->is_prepaid #for prepaid, "display the validity period
3544                                      # triggered by the recurring charge freq
3545                                      # (RT#26274)
3546             && $cust_bill_pkg->recur == 0
3547             && ! $cust_bill_pkg->recur_show_zero;
3548
3549           my @d = ();
3550           my $svc_label;
3551
3552           # always pass the svc_label through to the template, even if 
3553           # not displaying it as an ext_description
3554           my @svc_labels = map &{$escape_function}($_),
3555             $cust_pkg->h_labels_short($self->_date,
3556                                       undef,
3557                                       'I',
3558                                       $self->conf->{locale},
3559                                      );
3560           $svc_label = $svc_labels[0];
3561
3562           unless ( $cust_pkg->part_pkg->hide_svc_detail
3563                 || $cust_bill_pkg->hidden )
3564           {
3565
3566             push @d, @svc_labels
3567               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3568             # show the location label if it's not the customer's default
3569             # location, and we're not grouping items by location already
3570             if ( $cust_pkg->locationnum != $default_locationnum
3571                   and !defined($locationnum) ) {
3572               my $loc = $cust_pkg->location_label;
3573               $loc = substr($loc, 0, $maxlength). '...'
3574                 if $format eq 'latex' && length($loc) > $maxlength;
3575               push @d, &{$escape_function}($loc);
3576             }
3577
3578           } #unless hiding service details
3579
3580           push @d, $cust_bill_pkg->details(%details_opt)
3581             if $cust_bill_pkg->recur == 0;
3582
3583           if ( $cust_bill_pkg->hidden ) {
3584             $s->{amount}      += $cust_bill_pkg->setup;
3585             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3586             push @{ $s->{ext_description} }, @d;
3587           } else {
3588             $s = {
3589               _is_setup       => 1,
3590               description     => $description,
3591               pkgpart         => $pkgpart,
3592               pkgnum          => $cust_bill_pkg->pkgnum,
3593               amount          => $cust_bill_pkg->setup,
3594               setup_show_zero => $cust_bill_pkg->setup_show_zero,
3595               unit_amount     => $cust_bill_pkg->unitsetup,
3596               quantity        => $cust_bill_pkg->quantity,
3597               pkg_tax         => \@pkg_tax,
3598               ext_description => \@d,
3599               svc_label       => ($svc_label || ''),
3600               locationnum     => $cust_pkg->locationnum, # sure, why not?
3601             };
3602           };
3603
3604         }
3605
3606         # should we show a recur line?
3607         # if type eq 'S', then NO, because we've been told not to.
3608         # otherwise, show the recur line if:
3609         # - there's a recurring charge
3610         # - or recur_show_zero is on
3611         # - or there's a positive unitrecur (so it's been discounted to zero)
3612         #   and discount-show-always is on
3613         if (    ( !$type || $type eq 'R' || $type eq 'U' )
3614              && (
3615                      $cust_bill_pkg->recur != 0
3616                   || !defined($s)
3617                   || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3618                   || $cust_bill_pkg->recur_show_zero
3619                 )
3620            )
3621         {
3622
3623           warn "$me _items_cust_bill_pkg adding recur/usage\n"
3624             if $DEBUG > 1;
3625
3626           my $is_summary = $display->summary;
3627           my $description = $desc;
3628           if ( $type eq 'U' and defined($r) ) {
3629             # don't just show the same description as the recur line
3630             $description = $self->mt('Usage charges');
3631           }
3632
3633           my $disable_date_ranges =
3634                $opt{disable_line_item_date_ranges}
3635             || $part_pkg->option('disable_line_item_date_ranges', 1);
3636
3637           $description .= $cust_bill_pkg->time_period_pretty(
3638                                     $part_pkg,
3639                                     $agentnum,
3640                                     disable_date_ranges => $disable_date_ranges,
3641                           );
3642
3643           my @d = ();
3644           my @seconds = (); # for display of usage info
3645           my $svc_label = '';
3646
3647           #at least until cust_bill_pkg has "past" ranges in addition to
3648           #the "future" sdate/edate ones... see #3032
3649           my @dates = ( $self->_date );
3650           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3651           push @dates, $prev->sdate if $prev;
3652           push @dates, undef if !$prev;
3653
3654           my @svc_labels = map &{$escape_function}($_),
3655             $cust_pkg->h_labels_short(@dates,
3656                                       'I',
3657                                       $self->conf->{locale});
3658           $svc_label = $svc_labels[0];
3659
3660           # show service labels, unless...
3661                     # the package is set not to display them
3662           unless ( $part_pkg->hide_svc_detail
3663                     # or this is a tax-like line item
3664                 || $cust_bill_pkg->itemdesc
3665                     # or this is a hidden (bundled) line item
3666                 || $cust_bill_pkg->hidden
3667                     # or this is a usage summary line
3668                 || $is_summary && $type && $type eq 'U'
3669                     # or this is a usage line and there's a recurring line
3670                     # for the package in the same section (which will 
3671                     # have service labels already)
3672                 || ($type eq 'U' and defined($r))
3673               )
3674           {
3675
3676             warn "$me _items_cust_bill_pkg adding service details\n"
3677               if $DEBUG > 1;
3678
3679             push @d, @svc_labels
3680               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3681             warn "$me _items_cust_bill_pkg done adding service details\n"
3682               if $DEBUG > 1;
3683
3684             # show the location label if it's not the customer's default
3685             # location, and we're not grouping items by location already
3686             if ( $cust_pkg->locationnum != $default_locationnum
3687                   and !defined($locationnum) ) {
3688               my $loc = $cust_pkg->location_label;
3689               $loc = substr($loc, 0, $maxlength). '...'
3690                 if $format eq 'latex' && length($loc) > $maxlength;
3691               push @d, &{$escape_function}($loc);
3692             }
3693
3694             # Display of seconds_since_sqlradacct:
3695             # On the invoice, when processing @detail_items, look for a field
3696             # named 'seconds'.  This will contain total seconds for each 
3697             # service, in the same order as @ext_description.  For services 
3698             # that don't support this it will show undef.
3699             if ( $conf->exists('svc_acct-usage_seconds') 
3700                  and ! $cust_bill_pkg->pkgpart_override ) {
3701               foreach my $cust_svc ( 
3702                   $cust_pkg->h_cust_svc(@dates, 'I') 
3703                 ) {
3704
3705                 # eval because not having any part_export_usage exports 
3706                 # is a fatal error, last_bill/_date because that's how 
3707                 # sqlradius_hour billing does it
3708                 my $sec = eval {
3709                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
3710                 };
3711                 push @seconds, $sec;
3712               }
3713             } #if svc_acct-usage_seconds
3714
3715           } # if we are showing service labels
3716
3717           unless ( $is_summary ) {
3718             warn "$me _items_cust_bill_pkg adding details\n"
3719               if $DEBUG > 1;
3720
3721             #instead of omitting details entirely in this case (unwanted side
3722             # effects), just omit CDRs
3723             $details_opt{'no_usage'} = 1
3724               if $type && $type eq 'R';
3725
3726             push @d, $cust_bill_pkg->details(%details_opt);
3727           }
3728
3729           warn "$me _items_cust_bill_pkg calculating amount\n"
3730             if $DEBUG > 1;
3731   
3732           my $amount = 0;
3733           if (!$type) {
3734             $amount = $cust_bill_pkg->recur;
3735           } elsif ($type eq 'R') {
3736             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3737           } elsif ($type eq 'U') {
3738             $amount = $cust_bill_pkg->usage;
3739           }
3740   
3741           if ( !$type || $type eq 'R' ) {
3742
3743             warn "$me _items_cust_bill_pkg adding recur\n"
3744               if $DEBUG > 1;
3745
3746             my $unit_amount =
3747               ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
3748                                                 : $amount;
3749
3750             if ( $cust_bill_pkg->hidden ) {
3751               $r->{amount}      += $amount;
3752               $r->{unit_amount} += $unit_amount;
3753               push @{ $r->{ext_description} }, @d;
3754             } else {
3755               $r = {
3756                 description     => $description,
3757                 pkgpart         => $pkgpart,
3758                 pkgnum          => $cust_bill_pkg->pkgnum,
3759                 amount          => $amount,
3760                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3761                 unit_amount     => $unit_amount,
3762                 quantity        => $cust_bill_pkg->quantity,
3763                 pkg_tax         => \@pkg_tax,
3764                 %item_dates,
3765                 ext_description => \@d,
3766                 svc_label       => ($svc_label || ''),
3767                 locationnum     => $cust_pkg->locationnum,
3768               };
3769               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3770             }
3771
3772           } else {  # $type eq 'U'
3773
3774             warn "$me _items_cust_bill_pkg adding usage\n"
3775               if $DEBUG > 1;
3776
3777             if ( $cust_bill_pkg->hidden and defined($u) ) {
3778               # if this is a hidden package and there's already a usage
3779               # line for the bundle, add this package's total amount and
3780               # usage details to it
3781               $u->{amount}      += $amount;
3782               push @{ $u->{ext_description} }, @d;
3783             } elsif ( $amount ) {
3784               # create a new usage line
3785               $u = {
3786                 description     => $description,
3787                 pkgpart         => $pkgpart,
3788                 pkgnum          => $cust_bill_pkg->pkgnum,
3789                 amount          => $amount,
3790                 usage_item      => 1,
3791                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3792                 pkg_tax         => \@pkg_tax,
3793                 %item_dates,
3794                 ext_description => \@d,
3795                 locationnum     => $cust_pkg->locationnum,
3796               };
3797             } # else this has no usage, so don't create a usage section
3798           }
3799
3800         } # recurring or usage with recurring charge
3801
3802       } else { # taxes and fees
3803
3804         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3805           if $DEBUG > 1;
3806
3807         # items of this kind should normally not have sdate/edate.
3808         push @b, {
3809           'description' => $desc,
3810           'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
3811                                            + $cust_bill_pkg->recur)
3812         };
3813
3814       } # if quotation / package line item / other line item
3815
3816       # decide whether to show active discounts here
3817       if (
3818           # case 1: we are showing a single line for the package
3819           ( !$type )
3820           # case 2: we are showing a setup line for a package that has
3821           # no base recurring fee
3822           or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3823           # case 3: we are showing a recur line for a package that has 
3824           # a base recurring fee
3825           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3826       ) {
3827
3828         my $item_discount = $cust_bill_pkg->_item_discount;
3829         if ( $item_discount ) {
3830           # $item_discount->{amount} is negative
3831
3832           if ( $d and $cust_bill_pkg->hidden ) {
3833             $d->{amount}      += $item_discount->{amount};
3834           } else {
3835             $d = $item_discount;
3836             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
3837           }
3838
3839           # update the active line (before the discount) to show the 
3840           # original price (whether this is a hidden line or not)
3841           #
3842           # quotation discounts keep track of setup and recur; invoice 
3843           # discounts currently don't
3844           if ( exists $item_discount->{setup_amount} ) {
3845
3846             $s->{amount} -= $item_discount->{setup_amount} if $s;
3847             $r->{amount} -= $item_discount->{recur_amount} if $r;
3848
3849           } else {
3850
3851             # $active_line is the line item hashref for the line that will
3852             # show the original price
3853             # (use the recur or single line for the package, unless we're 
3854             # showing a setup line for a package with no recurring fee)
3855             my $active_line = $r;
3856             if ( $type eq 'S' ) {
3857               $active_line = $s;
3858             }
3859             $active_line->{amount} -= $item_discount->{amount};
3860
3861           }
3862
3863         } # if there are any discounts
3864       } # if this is an appropriate place to show discounts
3865
3866     } # foreach $display
3867
3868   }
3869
3870   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3871     if ( $_  ) {
3872       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3873         if exists($_->{amount});
3874       $_->{amount}      =~ s/^\-0\.00$/0.00/;
3875       if (exists($_->{unit_amount})) {
3876         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3877       }
3878
3879       push @b, { %$_ };
3880       #if $_->{amount} != 0
3881       #  || $discount_show_always
3882       #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3883       #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3884     }
3885   }
3886
3887   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3888     if $DEBUG > 1;
3889
3890   @b;
3891
3892 }
3893
3894 =item _items_discounts_avail
3895
3896 Returns an array of line item hashrefs representing available term discounts
3897 for this invoice.  This makes the same assumptions that apply to term 
3898 discounts in general: that the package is billed monthly, at a flat rate, 
3899 with no usage charges.  A prorated first month will be handled, as will 
3900 a setup fee if the discount is allowed to apply to setup fees.
3901
3902 =cut
3903
3904 sub _items_discounts_avail {
3905   my $self = shift;
3906
3907   #maybe move this method from cust_bill when quotations support discount_plans 
3908   return () unless $self->can('discount_plans');
3909   my %plans = $self->discount_plans;
3910
3911   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3912   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3913
3914   map {
3915     my $months = $_;
3916     my $plan = $plans{$months};
3917
3918     my $term_total = sprintf('%.2f', $plan->discounted_total);
3919     my $percent = sprintf('%.0f', 
3920                           100 * (1 - $term_total / $plan->base_total) );
3921     my $permonth = sprintf('%.2f', $term_total / $months);
3922     my $detail = $self->mt('discount on item'). ' '.
3923                  join(', ', map { "#$_" } $plan->pkgnums)
3924       if $list_pkgnums;
3925
3926     # discounts for non-integer months don't work anyway
3927     $months = sprintf("%d", $months);
3928
3929     +{
3930       description => $self->mt('Save [_1]% by paying for [_2] months',
3931                                 $percent, $months),
3932       amount      => $self->mt('[_1] ([_2] per month)', 
3933                                 $term_total, $money_char.$permonth),
3934       ext_description => ($detail || ''),
3935     }
3936   } #map
3937   sort { $b <=> $a } keys %plans;
3938
3939 }
3940
3941 =item has_sections AGENTNUM
3942
3943 Return true if invoice_sections should be enabled for this bill.
3944  (Inherited by both cust_bill and cust_bill_void)
3945
3946 Determination:
3947 * False if not an invoice
3948 * True always if conf invoice_sections is enabled
3949 * True always if sections_by_location is enabled
3950 * True if conf invoice_sections_multilocation > 1,
3951   and location_count >= invoice_sections_multilocation
3952 * Else, False
3953
3954 =cut
3955
3956 sub has_sections {
3957   my ($self, $agentnum) = @_;
3958
3959   return 0 unless $self->invnum > 0;
3960
3961   $agentnum ||= $self->agentnum;
3962   return 1 if $self->conf->config_bool('invoice_sections', $agentnum);
3963   return 1 if $self->conf->exists('sections_by_location', $agentnum);
3964
3965   my $location_min = $self->conf->config(
3966     'invoice_sections_multilocation', $agentnum,
3967   );
3968
3969   return 1
3970     if $location_min
3971     && $self->location_count >= $location_min;
3972
3973   0;
3974 }
3975
3976
3977 =item location_count
3978
3979 Return the number of locations billed on this invoice
3980
3981 =cut
3982
3983 sub location_count {
3984   my ($self) = @_;
3985   return 0 unless $self->invnum;
3986
3987   # SELECT COUNT( DISTINCT cust_pkg.locationnum )
3988   # FROM cust_bill_pkg
3989   # LEFT JOIN cust_pkg USING (pkgnum)
3990   # WHERE invnum = 278
3991   #   AND cust_bill_pkg.pkgnum > 0
3992
3993   my $result = qsearchs({
3994     select    => 'COUNT(DISTINCT cust_pkg.locationnum) as location_count',
3995     table     => 'cust_bill_pkg',
3996     addl_from => 'LEFT JOIN cust_pkg USING (pkgnum)',
3997     extra_sql => 'WHERE invnum = '.dbh->quote( $self->invnum )
3998                . '  AND cust_bill_pkg.pkgnum > 0'
3999   });
4000   ref $result ? $result->location_count : 0;
4001 }
4002
4003 1;