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