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