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