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