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