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