Merge branch 'master' of git.freeside.biz:/home/git/freeside
[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);
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
351     die "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'     => $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 + $pr_total;
689
690   # the sum of amount owed on all invoices
691   # (this is used in the summary & on the payment coupon)
692   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
693
694   # flag telling this invoice to have a first-page summary
695   my $summarypage = '';
696
697   if ( $self->custnum && $self->invnum ) {
698     # XXX should be an FS::cust_bill method to set the defaults, instead
699     # of checking the type here
700
701     my $last_bill = $self->previous_bill;
702     if ( $last_bill ) {
703
704       # "balance_date_range" unfortunately is unsuitable for this, since it
705       # cares about application dates.  We want to know the sum of all 
706       # _top-level transactions_ dated before the last invoice.
707       my @sql =
708         map "$_ WHERE _date <= ? AND custnum = ?", (
709           "SELECT      COALESCE( SUM(charged), 0 ) FROM cust_bill",
710           "SELECT -1 * COALESCE( SUM(amount),  0 ) FROM cust_credit",
711           "SELECT -1 * COALESCE( SUM(paid),    0 ) FROM cust_pay",
712           "SELECT      COALESCE( SUM(refund),  0 ) FROM cust_refund",
713         );
714
715       # the customer's current balance immediately after generating the last 
716       # bill
717
718       my $last_bill_balance = $last_bill->charged;
719       foreach (@sql) {
720         my $delta = FS::Record->scalar_sql(
721           $_,
722           $last_bill->_date - 1,
723           $self->custnum,
724         );
725         $last_bill_balance += $delta;
726       }
727
728       $last_bill_balance = sprintf("%.2f", $last_bill_balance);
729
730       warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
731         $last_bill->invnum,
732         $self->time2str_local('%D', $last_bill->_date),
733         $last_bill_balance
734       ) if $DEBUG > 0;
735       # ("true_previous_balance" is a terrible name, but at least it's no
736       # longer stored in the database)
737       $invoice_data{'true_previous_balance'} = $last_bill_balance;
738
739       # the change in balance from immediately after that invoice
740       # to immediately before this one
741       my $before_this_bill_balance = 0;
742       foreach (@sql) {
743         my $delta = FS::Record->scalar_sql(
744           $_,
745           $self->_date - 1,
746           $self->custnum,
747         );
748         $before_this_bill_balance += $delta;
749       }
750       $invoice_data{'balance_adjustments'} =
751         sprintf("%.2f", $last_bill_balance - $before_this_bill_balance);
752
753       warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
754                    $invoice_data{'balance_adjustments'}
755       ) if $DEBUG > 0;
756
757       # the sum of amount owed on all previous invoices
758       # ($pr_total is used elsewhere but not as $previous_balance)
759       $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
760
761       $invoice_data{'last_bill'} = {
762         '_date'     => $last_bill->_date, #unformatted
763       };
764       my (@payments, @credits);
765       # for formats that itemize previous payments
766       foreach my $cust_pay ( qsearch('cust_pay', {
767                               'custnum' => $self->custnum,
768                               '_date'   => { op => '>=',
769                                              value => $last_bill->_date }
770                              } ) )
771       {
772         next if $cust_pay->_date > $self->_date;
773         push @payments, {
774             '_date'       => $cust_pay->_date,
775             'date'        => $self->time2str_local('long', $cust_pay->_date, $format),
776             'payinfo'     => $cust_pay->payby_payinfo_pretty,
777             'amount'      => sprintf('%.2f', $cust_pay->paid),
778         };
779         # not concerned about applications
780       }
781       foreach my $cust_credit ( qsearch('cust_credit', {
782                               'custnum' => $self->custnum,
783                               '_date'   => { op => '>=',
784                                              value => $last_bill->_date }
785                              } ) )
786       {
787         next if $cust_credit->_date > $self->_date;
788         push @credits, {
789             '_date'       => $cust_credit->_date,
790             'date'        => $self->time2str_local('long', $cust_credit->_date, $format),
791             'creditreason'=> $cust_credit->reason,
792             'amount'      => sprintf('%.2f', $cust_credit->amount),
793         };
794       }
795       $invoice_data{'previous_payments'} = \@payments;
796       $invoice_data{'previous_credits'}  = \@credits;
797     } else {
798       # there is no $last_bill
799       $invoice_data{'true_previous_balance'} =
800       $invoice_data{'balance_adjustments'}   =
801       $invoice_data{'previous_balance'}      = '0.00';
802       $invoice_data{'previous_payments'} = [];
803       $invoice_data{'previous_credits'} = [];
804     }
805
806     # info from customer's last invoice before this one, for some 
807     # summary formats
808     $invoice_data{'last_bill'} = {};
809   
810     if ( $conf->exists('invoice_usesummary', $agentnum) ) {
811       $invoice_data{'summarypage'} = $summarypage = 1;
812     }
813
814   } # if this is an invoice
815
816   warn "$me substituting variables in notes, footer, smallfooter\n"
817     if $DEBUG > 1;
818
819   my $tc = $self->template_conf;
820   my @include = ( [ $tc,        'notes' ],
821                   [ 'invoice_', 'footer' ],
822                   [ 'invoice_', 'smallfooter', ],
823                 );
824   push @include, [ $tc,        'coupon', ]
825     unless $params{'no_coupon'};
826
827   foreach my $i (@include) {
828
829     my($base, $include) = @$i;
830
831     my $inc_file = $conf->key_orbase("$base$format$include", $template);
832     my @inc_src;
833
834     if ( $conf->exists($inc_file, $agentnum)
835          && length( $conf->config($inc_file, $agentnum) ) ) {
836
837       @inc_src = $conf->config($inc_file, $agentnum);
838
839     } else {
840
841       $inc_file = $conf->key_orbase("${base}latex$include", $template);
842
843       my $convert_map = $convert_maps{$format}{$include};
844
845       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
846                        s/--\@\]/$delimiters{$format}[1]/g;
847                        $_;
848                      } 
849                  &$convert_map( $conf->config($inc_file, $agentnum) );
850
851     }
852
853     my $inc_tt = new Text::Template (
854       TYPE       => 'ARRAY',
855       SOURCE     => [ map "$_\n", @inc_src ],
856       DELIMITERS => $delimiters{$format},
857     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
858
859     unless ( $inc_tt->compile() ) {
860       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
861       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
862       die $error;
863     }
864
865     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
866
867     $invoice_data{$include} =~ s/\n+$//
868       if ($format eq 'latex');
869   }
870
871   # let invoices use either of these as needed
872   $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
873     ? $cust_main->payinfo : '';
874   $invoice_data{'po_line'} = 
875     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
876       ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
877       : $nbsp;
878
879   my %money_chars = ( 'latex'    => '',
880                       'html'     => $conf->config('money_char') || '$',
881                       'template' => '',
882                     );
883   my $money_char = $money_chars{$format};
884
885   # extremely dubious
886   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
887                             'html'     => $conf->config('money_char') || '$',
888                             'template' => '',
889                           );
890   my $other_money_char = $other_money_chars{$format};
891   $invoice_data{'dollar'} = $other_money_char;
892
893   my %minus_signs = ( 'latex'    => '$-$',
894                       'html'     => '&minus;',
895                       'template' => '- ' );
896   my $minus = $minus_signs{$format};
897
898   my @detail_items = ();
899   my @total_items = ();
900   my @buf = ();
901   my @sections = ();
902
903   $invoice_data{'detail_items'} = \@detail_items;
904   $invoice_data{'total_items'} = \@total_items;
905   $invoice_data{'buf'} = \@buf;
906   $invoice_data{'sections'} = \@sections;
907
908   warn "$me generating sections\n"
909     if $DEBUG > 1;
910
911   my $taxtotal = 0;
912   my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
913                       'subtotal'    => $taxtotal,   # adjusted below
914                       'tax_section' => 1,
915                     };
916   my $tax_weight = _pkg_category($tax_section->{description})
917                         ? _pkg_category($tax_section->{description})->weight
918                         : 0;
919   $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
920   $tax_section->{'sort_weight'} = $tax_weight;
921
922   my $adjusttotal = 0;
923   my $adjust_section = {
924     'description'    => $self->mt('Credits, Payments, and Adjustments'),
925     'adjust_section' => 1,
926     'subtotal'       => 0,   # adjusted below
927   };
928   my $adjust_weight = _pkg_category($adjust_section->{description})
929                         ? _pkg_category($adjust_section->{description})->weight
930                         : 0;
931   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
932   $adjust_section->{'sort_weight'} = $adjust_weight;
933
934   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
935   my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
936                      $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
937   $invoice_data{'multisection'} = $multisection;
938   my $late_sections;
939   my $extra_sections = [];
940   my $extra_lines = ();
941
942   # default section ('Charges')
943   my $default_section = { 'description' => '',
944                           'subtotal'    => '', 
945                           'no_subtotal' => 1,
946                         };
947
948   # Previous Charges section
949   # subtotal is the first return value from $self->previous
950   my $previous_section;
951   # if the invoice has major sections, or if we're summarizing previous 
952   # charges with a single line, or if we've been specifically told to put them
953   # in a section, create a section for previous charges:
954   if ( $multisection or
955        $conf->exists('previous_balance-summary_only') or
956        $conf->exists('previous_balance-section') ) {
957     
958     $previous_section =  { 'description' => $self->mt('Previous Charges'),
959                            'subtotal'    => $other_money_char.
960                                             sprintf('%.2f', $pr_total),
961                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
962                          };
963     $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
964       join(' / ', map { $cust_main->balance_date_range(@$_) }
965                   $self->_prior_month30s
966           )
967       if $conf->exists('invoice_include_aging');
968
969   } else {
970     # otherwise put them in the main section
971     $previous_section = $default_section;
972   }
973
974   if ( $multisection ) {
975     ($extra_sections, $extra_lines) =
976       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
977       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
978       && $self->can('_items_extra_usage_sections');
979
980     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
981
982     push @detail_items, @$extra_lines if $extra_lines;
983
984     # the code is written so that both methods can be used together, but
985     # we haven't yet changed the template to take advantage of that, so for 
986     # now, treat them as mutually exclusive.
987     my %section_method = ( by_category => 1 );
988     if ( $conf->config($tc.'sections_method') eq 'location' ) {
989       %section_method = ( by_location => 1 );
990     }
991     my ($early, $late) =
992       $self->_items_sections( 'summary' => $summarypage,
993                               'escape'  => $escape_function_nonbsp,
994                               'extra_sections' => $extra_sections,
995                               'format'  => $format,
996                               %section_method
997                             );
998     push @sections, @$early;
999     $late_sections = $late;
1000
1001     if (    $conf->exists('svc_phone_sections')
1002          && $self->can('_items_svc_phone_sections')
1003        )
1004     {
1005       my ($phone_sections, $phone_lines) =
1006         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
1007       push @{$late_sections}, @$phone_sections;
1008       push @detail_items, @$phone_lines;
1009     }
1010     if ( $conf->exists('voip-cust_accountcode_cdr')
1011          && $cust_main->accountcode_cdr
1012          && $self->can('_items_accountcode_cdr')
1013        )
1014     {
1015       my ($accountcode_section, $accountcode_lines) =
1016         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
1017       if ( scalar(@$accountcode_lines) ) {
1018           push @{$late_sections}, $accountcode_section;
1019           push @detail_items, @$accountcode_lines;
1020       }
1021     }
1022   } else {# not multisection
1023     # make a default section
1024     push @sections, $default_section;
1025     # and calculate the finance charge total, since it won't get done otherwise.
1026     # and the default section total
1027     # XXX possibly finance_pkgclass should not be used in this manner?
1028     my @finance_charges;
1029     my @charges;
1030     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1031       if ( $invoice_data{finance_section} and 
1032         grep { $_->section eq $invoice_data{finance_section} }
1033            $cust_bill_pkg->cust_bill_pkg_display ) {
1034         # I think these are always setup fees, but just to be sure...
1035         push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1036       } else {
1037         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1038       }
1039     }
1040     $invoice_data{finance_amount} = 
1041       sprintf('%.2f', sum( @finance_charges ) || 0);
1042     $default_section->{subtotal} = $other_money_char.
1043                                     sprintf('%.2f', sum( @charges ) || 0);
1044   }
1045
1046   # start setting up summary subtotals
1047   my @summary_subtotals;
1048   my $method = $conf->config('summary_subtotals_method');
1049   if ( $method and $method ne $conf->config($tc.'sections_method') ) {
1050     # then re-section them by the correct method
1051     my %section_method = ( by_category => 1 );
1052     if ( $conf->config('summary_subtotals_method') eq 'location' ) {
1053       %section_method = ( by_location => 1 );
1054     }
1055     my ($early, $late) =
1056       $self->_items_sections( 'summary' => $summarypage,
1057                               'escape'  => $escape_function_nonbsp,
1058                               'extra_sections' => $extra_sections,
1059                               'format'  => $format,
1060                               %section_method
1061                             );
1062     foreach ( @$early ) {
1063       next if $_->{subtotal} == 0;
1064       $_->{subtotal} = $other_money_char.sprintf('%.2f', $_->{subtotal});
1065       push @summary_subtotals, $_;
1066     }
1067   } else {
1068     # subtotal sectioning is the same as for the actual invoice sections
1069     @summary_subtotals = @sections;
1070   }
1071
1072   # Hereafter, push sections to both @sections and @summary_subtotals
1073   # if they belong in both places (e.g. tax section).  Late sections are
1074   # never in @summary_subtotals.
1075
1076   # previous invoice balances in the Previous Charges section if there
1077   # is one, otherwise in the main detail section
1078   # (except if summary_only is enabled, don't show them at all)
1079   if ( $self->can('_items_previous') &&
1080        $self->enable_previous &&
1081        ! $conf->exists('previous_balance-summary_only') ) {
1082
1083     warn "$me adding previous balances\n"
1084       if $DEBUG > 1;
1085
1086     foreach my $line_item ( $self->_items_previous ) {
1087
1088       my $detail = {
1089         ref             => $line_item->{'pkgnum'},
1090         pkgpart         => $line_item->{'pkgpart'},
1091         #quantity        => 1, # not really correct
1092         section         => $previous_section, # which might be $default_section
1093         description     => &$escape_function($line_item->{'description'}),
1094         ext_description => [ map { &$escape_function($_) } 
1095                              @{ $line_item->{'ext_description'} || [] }
1096                            ],
1097         amount          => $money_char . $line_item->{'amount'},
1098         product_code    => $line_item->{'pkgpart'} || 'N/A',
1099       };
1100
1101       push @detail_items, $detail;
1102       push @buf, [ $detail->{'description'},
1103                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1104                  ];
1105     }
1106
1107   }
1108
1109   if ( @pr_cust_bill && $self->enable_previous ) {
1110     push @buf, ['','-----------'];
1111     push @buf, [ $self->mt('Total Previous Balance'),
1112                  $money_char. sprintf("%10.2f", $pr_total) ];
1113     push @buf, ['',''];
1114   }
1115  
1116   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
1117       warn "$me adding DID summary\n"
1118         if $DEBUG > 1;
1119
1120       my ($didsummary,$minutes) = $self->_did_summary;
1121       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
1122       push @detail_items, 
1123        { 'description' => $didsummary_desc,
1124            'ext_description' => [ $didsummary, $minutes ],
1125        };
1126   }
1127
1128   foreach my $section (@sections, @$late_sections) {
1129
1130     # begin some normalization
1131     $section->{'subtotal'} = $section->{'amount'}
1132       if $multisection
1133          && !exists($section->{subtotal})
1134          && exists($section->{amount});
1135
1136     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1137       if ( $invoice_data{finance_section} &&
1138            $section->{'description'} eq $invoice_data{finance_section} );
1139
1140     $section->{'subtotal'} = $other_money_char.
1141                              sprintf('%.2f', $section->{'subtotal'})
1142       if $multisection;
1143
1144     # continue some normalization
1145     $section->{'amount'}   = $section->{'subtotal'}
1146       if $multisection;
1147
1148
1149     if ( $section->{'description'} ) {
1150       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1151                    [ '', '' ],
1152                  );
1153     }
1154
1155     warn "$me   setting options\n"
1156       if $DEBUG > 1;
1157
1158     my %options = ();
1159     $options{'section'} = $section if $multisection;
1160     $options{'format'} = $format;
1161     $options{'escape_function'} = $escape_function;
1162     $options{'no_usage'} = 1 unless $unsquelched;
1163     $options{'unsquelched'} = $unsquelched;
1164     $options{'summary_page'} = $summarypage;
1165     $options{'skip_usage'} =
1166       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1167     $options{'preref_callback'} = $params{'preref_callback'};
1168
1169     warn "$me   searching for line items\n"
1170       if $DEBUG > 1;
1171
1172     foreach my $line_item ( $self->_items_pkg(%options),
1173                             $self->_items_fee(%options) ) {
1174
1175       warn "$me     adding line item ".
1176            join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
1177         if $DEBUG > 1;
1178
1179       push @buf, ( [ $line_item->{'description'},
1180                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1181                    ],
1182                    map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1183                  );
1184
1185       $line_item->{'ref'} = $line_item->{'pkgnum'};
1186       $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1187       $line_item->{'section'} = $section;
1188       $line_item->{'description'} = &$escape_function($line_item->{'description'});
1189       $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1190
1191       if ( length($line_item->{'unit_amount'}) ) {
1192         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1193       }
1194       $line_item->{'ext_description'} ||= [];
1195  
1196       push @detail_items, $line_item;
1197     }
1198
1199     if ( $section->{'description'} ) {
1200       push @buf, ( ['','-----------'],
1201                    [ $section->{'description'}. ' sub-total',
1202                       $section->{'subtotal'} # already formatted this 
1203                    ],
1204                    [ '', '' ],
1205                    [ '', '' ],
1206                  );
1207     }
1208   
1209   }
1210
1211   $invoice_data{current_less_finance} =
1212     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1213
1214   # if there's anything in the Previous Charges section, prepend it to the list
1215   if ( $pr_total and $previous_section ne $default_section ) {
1216     unshift @sections, $previous_section;
1217     # but not @summary_subtotals
1218   }
1219
1220   warn "$me adding taxes\n"
1221     if $DEBUG > 1;
1222
1223   my @items_tax = $self->_items_tax;
1224   foreach my $tax ( @items_tax ) {
1225
1226     $taxtotal += $tax->{'amount'};
1227
1228     my $description = &$escape_function( $tax->{'description'} );
1229     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
1230
1231     if ( $multisection ) {
1232
1233       push @detail_items, {
1234         ext_description => [],
1235         ref          => '',
1236         quantity     => '',
1237         description  => $description,
1238         amount       => $money_char. $amount,
1239         product_code => '',
1240         section      => $tax_section,
1241       };
1242
1243     } else {
1244
1245       push @total_items, {
1246         'total_item'   => $description,
1247         'total_amount' => $other_money_char. $amount,
1248       };
1249
1250     }
1251
1252     push @buf,[ $description,
1253                 $money_char. $amount,
1254               ];
1255
1256   }
1257   
1258   if ( @items_tax ) {
1259     my $total = {};
1260     $total->{'total_item'} = $self->mt('Sub-total');
1261     $total->{'total_amount'} =
1262       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1263
1264     if ( $multisection ) {
1265       $tax_section->{'subtotal'} = $other_money_char.
1266                                    sprintf('%.2f', $taxtotal);
1267       $tax_section->{'pretotal'} = 'New charges sub-total '.
1268                                    $total->{'total_amount'};
1269       if ( $taxtotal ) {
1270         push @sections, $tax_section;
1271         push @summary_subtotals, $tax_section;
1272       }
1273     } else {
1274       unshift @total_items, $total;
1275     }
1276   }
1277   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1278
1279   push @buf,['','-----------'];
1280   push @buf,[$self->mt( 
1281               (!$self->enable_previous)
1282                ? 'Total Charges'
1283                : 'Total New Charges'
1284              ),
1285              $money_char. sprintf("%10.2f",$self->charged) ];
1286   push @buf,['',''];
1287
1288
1289   ###
1290   # Totals
1291   ###
1292
1293   my %embolden_functions = (
1294     'latex'    => sub { return '\textbf{'. shift(). '}' },
1295     'html'     => sub { return '<b>'. shift(). '</b>' },
1296     'template' => sub { shift },
1297   );
1298   my $embolden_function = $embolden_functions{$format};
1299
1300   if ( $self->can('_items_total') ) { # quotations
1301
1302     $self->_items_total(\@total_items);
1303
1304     foreach ( @total_items ) {
1305       $_->{'total_item'}   = &$embolden_function( $_->{'total_item'} );
1306       $_->{'total_amount'} = &$embolden_function( $other_money_char.
1307                                                    $_->{'total_amount'}
1308                                                 );
1309     }
1310
1311   } else { #normal invoice case
1312
1313     # calculate total, possibly including total owed on previous
1314     # invoices
1315     my $total = {};
1316     my $item = 'Total';
1317     $item = $conf->config('previous_balance-exclude_from_total')
1318          || 'Total New Charges'
1319       if $conf->exists('previous_balance-exclude_from_total');
1320     my $amount = $self->charged;
1321     if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1322       $amount += $pr_total;
1323     }
1324
1325     $total->{'total_item'} = &$embolden_function($self->mt($item));
1326     $total->{'total_amount'} =
1327       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
1328     if ( $multisection ) {
1329       if ( $adjust_section->{'sort_weight'} ) {
1330         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1331           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
1332       } else {
1333         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1334           $other_money_char.  sprintf('%.2f', $self->charged );
1335       } 
1336     } else {
1337       push @total_items, $total;
1338     }
1339     push @buf,['','-----------'];
1340     push @buf,[$item,
1341                $money_char.
1342                sprintf( '%10.2f', $amount )
1343               ];
1344     push @buf,['',''];
1345
1346     # if we're showing previous invoices, also show previous
1347     # credits and payments 
1348     if ( $self->enable_previous 
1349           and $self->can('_items_credits')
1350           and $self->can('_items_payments') )
1351       {
1352       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1353     
1354       # credits
1355       my $credittotal = 0;
1356       foreach my $credit (
1357         $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1358       ) {
1359
1360         my $total;
1361         $total->{'total_item'} = &$escape_function($credit->{'description'});
1362         $credittotal += $credit->{'amount'};
1363         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1364         $adjusttotal += $credit->{'amount'};
1365         if ( $multisection ) {
1366           push @detail_items, {
1367             ext_description => [],
1368             ref          => '',
1369             quantity     => '',
1370             description  => &$escape_function($credit->{'description'}),
1371             amount       => $money_char . $credit->{'amount'},
1372             product_code => '',
1373             section      => $adjust_section,
1374           };
1375         } else {
1376           push @total_items, $total;
1377         }
1378
1379       }
1380       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1381
1382       #credits (again)
1383       foreach my $credit (
1384         $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1385       ) {
1386         push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1387       }
1388
1389       # payments
1390       my $paymenttotal = 0;
1391       foreach my $payment (
1392         $self->_items_payments( 'template' => $template )
1393       ) {
1394         my $total = {};
1395         $total->{'total_item'} = &$escape_function($payment->{'description'});
1396         $paymenttotal += $payment->{'amount'};
1397         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1398         $adjusttotal += $payment->{'amount'};
1399         if ( $multisection ) {
1400           push @detail_items, {
1401             ext_description => [],
1402             ref          => '',
1403             quantity     => '',
1404             description  => &$escape_function($payment->{'description'}),
1405             amount       => $money_char . $payment->{'amount'},
1406             product_code => '',
1407             section      => $adjust_section,
1408           };
1409         }else{
1410           push @total_items, $total;
1411         }
1412         push @buf, [ $payment->{'description'},
1413                      $money_char. sprintf("%10.2f", $payment->{'amount'}),
1414                    ];
1415       }
1416       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1417     
1418       if ( $multisection ) {
1419         $adjust_section->{'subtotal'} = $other_money_char.
1420                                         sprintf('%.2f', $adjusttotal);
1421         push @sections, $adjust_section
1422           unless $adjust_section->{sort_weight};
1423         # do not summarize; adjustments there are shown according to 
1424         # different rules
1425       }
1426
1427       # create Balance Due message
1428       { 
1429         my $total;
1430         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1431         $total->{'total_amount'} =
1432           &$embolden_function(
1433             $other_money_char. sprintf('%.2f', #why? $summarypage 
1434                                                #  ? $self->charged +
1435                                                #    $self->billing_balance
1436                                                #  :
1437                                                    $self->owed + $pr_total
1438                                       )
1439           );
1440         if ( $multisection && !$adjust_section->{sort_weight} ) {
1441           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1442                                            $total->{'total_amount'};
1443         }else{
1444           push @total_items, $total;
1445         }
1446         push @buf,['','-----------'];
1447         push @buf,[$self->balance_due_msg, $money_char. 
1448           sprintf("%10.2f", $balance_due ) ];
1449       }
1450
1451       if ( $conf->exists('previous_balance-show_credit')
1452           and $cust_main->balance < 0 ) {
1453         my $credit_total = {
1454           'total_item'    => &$embolden_function($self->credit_balance_msg),
1455           'total_amount'  => &$embolden_function(
1456             $other_money_char. sprintf('%.2f', -$cust_main->balance)
1457           ),
1458         };
1459         if ( $multisection ) {
1460           $adjust_section->{'posttotal'} .= $newline_token .
1461             $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1462         }
1463         else {
1464           push @total_items, $credit_total;
1465         }
1466         push @buf,['','-----------'];
1467         push @buf,[$self->credit_balance_msg, $money_char. 
1468           sprintf("%10.2f", -$cust_main->balance ) ];
1469       }
1470     }
1471
1472   } #end of default total adding ! can('_items_total')
1473
1474   if ( $multisection ) {
1475     if (    $conf->exists('svc_phone_sections')
1476          && $self->can('_items_svc_phone_sections')
1477        )
1478     {
1479       my $total;
1480       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1481       $total->{'total_amount'} =
1482         &$embolden_function(
1483           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1484         );
1485       my $last_section = pop @sections;
1486       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1487                                      $total->{'total_amount'};
1488       push @sections, $last_section;
1489     }
1490     push @sections, @$late_sections
1491       if $unsquelched;
1492   }
1493
1494   # make a discounts-available section, even without multisection
1495   if ( $conf->exists('discount-show_available') 
1496        and my @discounts_avail = $self->_items_discounts_avail ) {
1497     my $discount_section = {
1498       'description' => $self->mt('Discounts Available'),
1499       'subtotal'    => '',
1500       'no_subtotal' => 1,
1501     };
1502
1503     push @sections, $discount_section; # do not summarize
1504     push @detail_items, map { +{
1505         'ref'         => '', #should this be something else?
1506         'section'     => $discount_section,
1507         'description' => &$escape_function( $_->{description} ),
1508         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1509         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1510     } } @discounts_avail;
1511   }
1512
1513   # not adding any more sections after this
1514   $invoice_data{summary_subtotals} = \@summary_subtotals;
1515
1516   # usage subtotals
1517   if ( $conf->exists('usage_class_summary')
1518        and $self->can('_items_usage_class_summary') ) {
1519     my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function);
1520     if ( @usage_subtotals ) {
1521       unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1522       unshift @detail_items, @usage_subtotals;
1523     }
1524   }
1525
1526   # invoice history "section" (not really a section)
1527   # not to be included in any subtotals, completely independent of 
1528   # everything...
1529   if ( $conf->exists('previous_invoice_history') ) {
1530     my %history;
1531     my %monthorder;
1532     foreach my $cust_bill ( $cust_main->cust_bill ) {
1533       # XXX hardcoded format, and currently only 'charged'; add other fields
1534       # if they become necessary
1535       my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1536       $history{$date} ||= 0;
1537       $history{$date} += $cust_bill->charged;
1538       # just so we have a numeric sort key
1539       $monthorder{$date} ||= $cust_bill->_date;
1540     }
1541     my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1542                         keys %history;
1543     my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1544     $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1545   }
1546
1547   # service locations: another option for template customization
1548   my %location_info;
1549   foreach my $item (@detail_items) {
1550     if ( $item->{locationnum} ) {
1551       $location_info{ $item->{locationnum} } ||= {
1552         FS::cust_location->by_key( $item->{locationnum} )->location_hash
1553       };
1554     }
1555   }
1556   $invoice_data{location_info} = \%location_info;
1557
1558   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1559   # the invoice variables
1560   return \%invoice_data if ( $params{'diag'} );
1561
1562   # All sections and items are built; now fill in templates.
1563   my @includelist = ();
1564   push @includelist, 'summary' if $summarypage;
1565   foreach my $include ( @includelist ) {
1566
1567     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1568     my @inc_src;
1569
1570     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1571
1572       @inc_src = $conf->config($inc_file, $agentnum);
1573
1574     } else {
1575
1576       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1577
1578       my $convert_map = $convert_maps{$format}{$include};
1579
1580       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1581                        s/--\@\]/$delimiters{$format}[1]/g;
1582                        $_;
1583                      } 
1584                  &$convert_map( $conf->config($inc_file, $agentnum) );
1585
1586     }
1587
1588     my $inc_tt = new Text::Template (
1589       TYPE       => 'ARRAY',
1590       SOURCE     => [ map "$_\n", @inc_src ],
1591       DELIMITERS => $delimiters{$format},
1592     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1593
1594     unless ( $inc_tt->compile() ) {
1595       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1596       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1597       die $error;
1598     }
1599
1600     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1601
1602     $invoice_data{$include} =~ s/\n+$//
1603       if ($format eq 'latex');
1604   }
1605
1606   $invoice_lines = 0;
1607   my $wasfunc = 0;
1608   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1609     /invoice_lines\((\d*)\)/;
1610     $invoice_lines += $1 || scalar(@buf);
1611     $wasfunc=1;
1612   }
1613   die "no invoice_lines() functions in template?"
1614     if ( $format eq 'template' && !$wasfunc );
1615
1616   if ($format eq 'template') {
1617
1618     if ( $invoice_lines ) {
1619       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1620       $invoice_data{'total_pages'}++
1621         if scalar(@buf) % $invoice_lines;
1622     }
1623
1624     #setup subroutine for the template
1625     $invoice_data{invoice_lines} = sub {
1626       my $lines = shift || scalar(@buf);
1627       map { 
1628         scalar(@buf)
1629           ? shift @buf
1630           : [ '', '' ];
1631       }
1632       ( 1 .. $lines );
1633     };
1634
1635     my $lines;
1636     my @collect;
1637     while (@buf) {
1638       push @collect, split("\n",
1639         $text_template->fill_in( HASH => \%invoice_data )
1640       );
1641       $invoice_data{'page'}++;
1642     }
1643     map "$_\n", @collect;
1644
1645   } else { # this is where we actually create the invoice
1646
1647     warn "filling in template for invoice ". $self->invnum. "\n"
1648       if $DEBUG;
1649     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1650       if $DEBUG > 1;
1651
1652     $text_template->fill_in(HASH => \%invoice_data);
1653   }
1654 }
1655
1656 sub notice_name { '('.shift->table.')'; }
1657
1658 sub template_conf { 'invoice_'; }
1659
1660 # helper routine for generating date ranges
1661 sub _prior_month30s {
1662   my $self = shift;
1663   my @ranges = (
1664    [ 1,       2592000 ], # 0-30 days ago
1665    [ 2592000, 5184000 ], # 30-60 days ago
1666    [ 5184000, 7776000 ], # 60-90 days ago
1667    [ 7776000, 0       ], # 90+   days ago
1668   );
1669
1670   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1671           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1672       ] }
1673   @ranges;
1674 }
1675
1676 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1677
1678 Returns an postscript invoice, as a scalar.
1679
1680 Options can be passed as a hashref (recommended) or as a list of time, template
1681 and then any key/value pairs for any other options.
1682
1683 I<time> an optional value used to control the printing of overdue messages.  The
1684 default is now.  It isn't the date of the invoice; that's the `_date' field.
1685 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1686 L<Time::Local> and L<Date::Parse> for conversion functions.
1687
1688 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1689
1690 =cut
1691
1692 sub print_ps {
1693   my $self = shift;
1694
1695   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1696   my $ps = generate_ps($file);
1697   unlink($logofile);
1698   unlink($barcodefile) if $barcodefile;
1699
1700   $ps;
1701 }
1702
1703 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1704
1705 Returns an PDF invoice, as a scalar.
1706
1707 Options can be passed as a hashref (recommended) or as a list of time, template
1708 and then any key/value pairs for any other options.
1709
1710 I<time> an optional value used to control the printing of overdue messages.  The
1711 default is now.  It isn't the date of the invoice; that's the `_date' field.
1712 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1713 L<Time::Local> and L<Date::Parse> for conversion functions.
1714
1715 I<template>, if specified, is the name of a suffix for alternate invoices.
1716
1717 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1718
1719 =cut
1720
1721 sub print_pdf {
1722   my $self = shift;
1723
1724   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1725   my $pdf = generate_pdf($file);
1726   unlink($logofile);
1727   unlink($barcodefile) if $barcodefile;
1728
1729   $pdf;
1730 }
1731
1732 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1733
1734 Returns an HTML invoice, as a scalar.
1735
1736 I<time> an optional value used to control the printing of overdue messages.  The
1737 default is now.  It isn't the date of the invoice; that's the `_date' field.
1738 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1739 L<Time::Local> and L<Date::Parse> for conversion functions.
1740
1741 I<template>, if specified, is the name of a suffix for alternate invoices.
1742
1743 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1744
1745 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1746 when emailing the invoice as part of a multipart/related MIME email.
1747
1748 =cut
1749
1750 sub print_html {
1751   my $self = shift;
1752   my %params;
1753   if ( ref($_[0]) ) {
1754     %params = %{ shift() }; 
1755   } else {
1756     %params = @_;
1757   }
1758   $params{'format'} = 'html';
1759   
1760   $self->print_generic( %params );
1761 }
1762
1763 # quick subroutine for print_latex
1764 #
1765 # There are ten characters that LaTeX treats as special characters, which
1766 # means that they do not simply typeset themselves: 
1767 #      # $ % & ~ _ ^ \ { }
1768 #
1769 # TeX ignores blanks following an escaped character; if you want a blank (as
1770 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1771
1772 sub _latex_escape {
1773   my $value = shift;
1774   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1775   $value =~ s/([<>])/\$$1\$/g;
1776   $value;
1777 }
1778
1779 sub _html_escape {
1780   my $value = shift;
1781   encode_entities($value);
1782   $value;
1783 }
1784
1785 sub _html_escape_nbsp {
1786   my $value = _html_escape(shift);
1787   $value =~ s/ +/&nbsp;/g;
1788   $value;
1789 }
1790
1791 #utility methods for print_*
1792
1793 sub _translate_old_latex_format {
1794   warn "_translate_old_latex_format called\n"
1795     if $DEBUG; 
1796
1797   my @template = ();
1798   while ( @_ ) {
1799     my $line = shift;
1800   
1801     if ( $line =~ /^%%Detail\s*$/ ) {
1802   
1803       push @template, q![@--!,
1804                       q!  foreach my $_tr_line (@detail_items) {!,
1805                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1806                       q!      $_tr_line->{'description'} .= !, 
1807                       q!        "\\tabularnewline\n~~".!,
1808                       q!        join( "\\tabularnewline\n~~",!,
1809                       q!          @{$_tr_line->{'ext_description'}}!,
1810                       q!        );!,
1811                       q!    }!;
1812
1813       while ( ( my $line_item_line = shift )
1814               !~ /^%%EndDetail\s*$/                            ) {
1815         $line_item_line =~ s/'/\\'/g;    # nice LTS
1816         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1817         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1818         push @template, "    \$OUT .= '$line_item_line';";
1819       }
1820
1821       push @template, '}',
1822                       '--@]';
1823       #' doh, gvim
1824     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1825
1826       push @template, '[@--',
1827                       '  foreach my $_tr_line (@total_items) {';
1828
1829       while ( ( my $total_item_line = shift )
1830               !~ /^%%EndTotalDetails\s*$/                      ) {
1831         $total_item_line =~ s/'/\\'/g;    # nice LTS
1832         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1833         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1834         push @template, "    \$OUT .= '$total_item_line';";
1835       }
1836
1837       push @template, '}',
1838                       '--@]';
1839
1840     } else {
1841       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1842       push @template, $line;  
1843     }
1844   
1845   }
1846
1847   if ($DEBUG) {
1848     warn "$_\n" foreach @template;
1849   }
1850
1851   (@template);
1852 }
1853
1854 =item terms
1855
1856 =cut
1857
1858 sub terms {
1859   my $self = shift;
1860   my $conf = $self->conf;
1861
1862   #check for an invoice-specific override
1863   return $self->invoice_terms if $self->invoice_terms;
1864   
1865   #check for a customer- specific override
1866   my $cust_main = $self->cust_main;
1867   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1868
1869   my $agentnum = '';
1870   if ( $cust_main ) {
1871     $agentnum = $cust_main->agentnum;
1872   } elsif ( my $prospect_main = $self->prospect_main ) {
1873     $agentnum = $prospect_main->agentnum;
1874   }
1875
1876   #use configured default
1877   $conf->config('invoice_default_terms', $agentnum) || '';
1878 }
1879
1880 =item due_date
1881
1882 =cut
1883
1884 sub due_date {
1885   my $self = shift;
1886   my $duedate = '';
1887   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1888     $duedate = $self->_date() + ( $1 * 86400 );
1889   }
1890   $duedate;
1891 }
1892
1893 =item due_date2str
1894
1895 =cut
1896
1897 sub due_date2str {
1898   my $self = shift;
1899   $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1900 }
1901
1902 =item balance_due_msg
1903
1904 =cut
1905
1906 sub balance_due_msg {
1907   my $self = shift;
1908   my $msg = $self->mt('Balance Due');
1909   return $msg unless $self->terms;
1910   if ( $self->due_date ) {
1911     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1912       $self->due_date2str('short');
1913   } elsif ( $self->terms ) {
1914     $msg .= ' - '. $self->terms;
1915   }
1916   $msg;
1917 }
1918
1919 =item balance_due_date
1920
1921 =cut
1922
1923 sub balance_due_date {
1924   my $self = shift;
1925   my $conf = $self->conf;
1926   my $duedate = '';
1927   my $terms = $self->terms;
1928   if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1929     $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1930   }
1931   $duedate;
1932 }
1933
1934 sub credit_balance_msg { 
1935   my $self = shift;
1936   $self->mt('Credit Balance Remaining')
1937 }
1938
1939 =item _date_pretty
1940
1941 Returns a string with the date, for example: "3/20/2008", localized for the
1942 customer.  Use _date_pretty_unlocalized for non-end-customer display use.
1943
1944 =cut
1945
1946 sub _date_pretty {
1947   my $self = shift;
1948   $self->time2str_local('short', $self->_date);
1949 }
1950
1951 =item _date_pretty_unlocalized
1952
1953 Returns a string with the date, for example: "3/20/2008", in the format
1954 configured for the back-office.  Use _date_pretty for end-customer display use.
1955
1956 =cut
1957
1958 sub _date_pretty_unlocalized {
1959   my $self = shift;
1960   time2str($date_format, $self->_date);
1961 }
1962
1963 =item email HASHREF
1964
1965 Emails this template.
1966
1967 Options are passed as a hashref.  Available options:
1968
1969 =over 4
1970
1971 =item from
1972
1973 If specified, overrides the default From: address.
1974
1975 =item notice_name
1976
1977 If specified, overrides the name of the sent document ("Invoice" or "Quotation")
1978
1979 =item template
1980
1981 (Deprecated) If specified, is the name of a suffix for alternate template files.
1982
1983 =back
1984
1985 Options accepted by generate_email can also be used.
1986
1987 =cut
1988
1989 sub email {
1990   my $self = shift;
1991   my $opt = shift || {};
1992   if ($opt and !ref($opt)) {
1993     die ref($self). '->email called with positional parameters';
1994   }
1995
1996   return if $self->hide;
1997
1998   my $error = send_email(
1999     $self->generate_email(
2000       'subject'     => $self->email_subject($opt->{template}),
2001       %$opt, # template, etc.
2002     )
2003   );
2004
2005   die "can't email: $error\n" if $error;
2006 }
2007
2008 =item generate_email OPTION => VALUE ...
2009
2010 Options:
2011
2012 =over 4
2013
2014 =item from
2015
2016 sender address, required
2017
2018 =item template
2019
2020 alternate template name, optional
2021
2022 =item print_text
2023
2024 text attachment arrayref, optional
2025
2026 =item subject
2027
2028 email subject, optional
2029
2030 =item notice_name
2031
2032 notice name instead of "Invoice", optional
2033
2034 =back
2035
2036 Returns an argument list to be passed to L<FS::Misc::send_email>.
2037
2038 =cut
2039
2040 use MIME::Entity;
2041
2042 sub generate_email {
2043
2044   my $self = shift;
2045   my %args = @_;
2046   my $conf = $self->conf;
2047
2048   my $me = '[FS::Template_Mixin::generate_email]';
2049
2050   my %return = (
2051     'from'      => $args{'from'},
2052     'subject'   => ($args{'subject'} || $self->email_subject),
2053     'custnum'   => $self->custnum,
2054     'msgtype'   => 'invoice',
2055   );
2056
2057   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
2058
2059   my $cust_main = $self->cust_main;
2060
2061   if (ref($args{'to'}) eq 'ARRAY') {
2062     $return{'to'} = $args{'to'};
2063   } elsif ( $cust_main ) {
2064     $return{'to'} = [ $cust_main->invoicing_list_emailonly ];
2065   }
2066
2067   my $tc = $self->template_conf;
2068
2069   if ( $conf->exists($tc.'html') ) {
2070
2071     warn "$me creating HTML/text multipart message"
2072       if $DEBUG;
2073
2074     $return{'nobody'} = 1;
2075
2076     my $alternative = build MIME::Entity
2077       'Type'        => 'multipart/alternative',
2078       #'Encoding'    => '7bit',
2079       'Disposition' => 'inline'
2080     ;
2081
2082     my $data = '';
2083     if ( $conf->exists($tc. 'email_pdf')
2084          and scalar($conf->config($tc. 'email_pdf_note')) ) {
2085
2086       warn "$me using '${tc}email_pdf_note' in multipart message"
2087         if $DEBUG;
2088       $data = [ map { $_ . "\n" }
2089                     $conf->config($tc.'email_pdf_note')
2090               ];
2091
2092     } else {
2093
2094       warn "$me not using '${tc}email_pdf_note' in multipart message"
2095         if $DEBUG;
2096       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
2097         $data = $args{'print_text'};
2098       } elsif ( $conf->exists($tc.'template') ) { #plaintext invoice_template
2099         $data = [ $self->print_text(\%args) ];
2100       }
2101
2102     }
2103
2104     if ( $data ) {
2105       $alternative->attach(
2106         'Type'        => 'text/plain',
2107         'Encoding'    => 'quoted-printable',
2108         'Charset'     => 'UTF-8',
2109         #'Encoding'    => '7bit',
2110         'Data'        => $data,
2111         'Disposition' => 'inline',
2112       );
2113     }
2114
2115     my $htmldata;
2116     my $image = '';
2117     my $barcode = '';
2118     if ( $conf->exists($tc.'email_pdf')
2119          and scalar($conf->config($tc.'email_pdf_note')) ) {
2120
2121       $htmldata = join('<BR>', $conf->config($tc.'email_pdf_note') );
2122
2123     } else {
2124
2125       $args{'from'} =~ /\@([\w\.\-]+)/;
2126       my $from = $1 || 'example.com';
2127       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2128
2129       my $logo;
2130       my $agentnum = $cust_main ? $cust_main->agentnum
2131                                 : $self->prospect_main->agentnum;
2132       if ( defined($args{'template'}) && length($args{'template'})
2133            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
2134          )
2135       {
2136         $logo = 'logo_'. $args{'template'}. '.png';
2137       } else {
2138         $logo = "logo.png";
2139       }
2140       my $image_data = $conf->config_binary( $logo, $agentnum);
2141
2142       $image = build MIME::Entity
2143         'Type'       => 'image/png',
2144         'Encoding'   => 'base64',
2145         'Data'       => $image_data,
2146         'Filename'   => 'logo.png',
2147         'Content-ID' => "<$content_id>",
2148       ;
2149    
2150       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
2151         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2152         $barcode = build MIME::Entity
2153           'Type'       => 'image/png',
2154           'Encoding'   => 'base64',
2155           'Data'       => $self->invoice_barcode(0),
2156           'Filename'   => 'barcode.png',
2157           'Content-ID' => "<$barcode_content_id>",
2158         ;
2159         $args{'barcode_cid'} = $barcode_content_id;
2160       }
2161
2162       $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
2163     }
2164
2165     $alternative->attach(
2166       'Type'        => 'text/html',
2167       'Encoding'    => 'quoted-printable',
2168       'Data'        => [ '<html>',
2169                          '  <head>',
2170                          '    <title>',
2171                          '      '. encode_entities($return{'subject'}), 
2172                          '    </title>',
2173                          '  </head>',
2174                          '  <body bgcolor="#e8e8e8">',
2175                          $htmldata,
2176                          '  </body>',
2177                          '</html>',
2178                        ],
2179       'Disposition' => 'inline',
2180       #'Filename'    => 'invoice.pdf',
2181     );
2182
2183
2184     my @otherparts = ();
2185     if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
2186
2187       push @otherparts, build MIME::Entity
2188         'Type'        => 'text/csv',
2189         'Encoding'    => '7bit',
2190         'Data'        => [ map { "$_\n" }
2191                              $self->call_details('prepend_billed_number' => 1)
2192                          ],
2193         'Disposition' => 'attachment',
2194         'Filename'    => 'usage-'. $self->invnum. '.csv',
2195       ;
2196
2197     }
2198
2199     if ( $conf->exists($tc.'email_pdf') ) {
2200
2201       #attaching pdf too:
2202       # multipart/mixed
2203       #   multipart/related
2204       #     multipart/alternative
2205       #       text/plain
2206       #       text/html
2207       #     image/png
2208       #   application/pdf
2209
2210       my $related = build MIME::Entity 'Type'     => 'multipart/related',
2211                                        'Encoding' => '7bit';
2212
2213       #false laziness w/Misc::send_email
2214       $related->head->replace('Content-type',
2215         $related->mime_type.
2216         '; boundary="'. $related->head->multipart_boundary. '"'.
2217         '; type=multipart/alternative'
2218       );
2219
2220       $related->add_part($alternative);
2221
2222       $related->add_part($image) if $image;
2223
2224       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
2225
2226       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
2227
2228     } else {
2229
2230       #no other attachment:
2231       # multipart/related
2232       #   multipart/alternative
2233       #     text/plain
2234       #     text/html
2235       #   image/png
2236
2237       $return{'content-type'} = 'multipart/related';
2238       if ($conf->exists('invoice-barcode') && $barcode) {
2239         $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
2240       } else {
2241         $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
2242       }
2243       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
2244       #$return{'disposition'} = 'inline';
2245
2246     }
2247   
2248   } else {
2249
2250     if ( $conf->exists($tc.'email_pdf') ) {
2251       warn "$me creating PDF attachment"
2252         if $DEBUG;
2253
2254       #mime parts arguments a la MIME::Entity->build().
2255       $return{'mimeparts'} = [
2256         { $self->mimebuild_pdf(\%args) }
2257       ];
2258     }
2259   
2260     if ( $conf->exists($tc.'email_pdf')
2261          and scalar($conf->config($tc.'email_pdf_note')) ) {
2262
2263       warn "$me using '${tc}email_pdf_note'"
2264         if $DEBUG;
2265       $return{'body'} = [ map { $_ . "\n" }
2266                               $conf->config($tc.'email_pdf_note')
2267                         ];
2268
2269     } else {
2270
2271       warn "$me not using '${tc}email_pdf_note'"
2272         if $DEBUG;
2273       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
2274         $return{'body'} = $args{'print_text'};
2275       } else {
2276         $return{'body'} = [ $self->print_text(\%args) ];
2277       }
2278
2279     }
2280
2281   }
2282
2283   %return;
2284
2285 }
2286
2287 =item mimebuild_pdf
2288
2289 Returns a list suitable for passing to MIME::Entity->build(), representing
2290 this invoice as PDF attachment.
2291
2292 =cut
2293
2294 sub mimebuild_pdf {
2295   my $self = shift;
2296   (
2297     'Type'        => 'application/pdf',
2298     'Encoding'    => 'base64',
2299     'Data'        => [ $self->print_pdf(@_) ],
2300     'Disposition' => 'attachment',
2301     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
2302   );
2303 }
2304
2305 =item _items_sections OPTIONS
2306
2307 Generate section information for all items appearing on this invoice.
2308 This will only be called for multi-section invoices.
2309
2310 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
2311 related display records (L<FS::cust_bill_pkg_display>) and organize 
2312 them into two groups ("early" and "late" according to whether they come 
2313 before or after the total), then into sections.  A subtotal is calculated 
2314 for each section.
2315
2316 Section descriptions are returned in sort weight order.  Each consists 
2317 of a hash containing:
2318
2319 description: the package category name, escaped
2320 subtotal: the total charges in that section
2321 tax_section: a flag indicating that the section contains only tax charges
2322 summarized: same as tax_section, for some reason
2323 sort_weight: the package category's sort weight
2324
2325 If 'condense' is set on the display record, it also contains everything 
2326 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
2327 coderefs to generate parts of the invoice.  This is not advised.
2328
2329 The method returns two arrayrefs, one of "early" sections and one of "late"
2330 sections.
2331
2332 OPTIONS may include:
2333
2334 by_location: a flag to divide the invoice into sections by location.  
2335 Each section hash will have a 'location' element containing a hashref of 
2336 the location fields (see L<FS::cust_location>).  The section description
2337 will be the location label, but the template can use any of the location 
2338 fields to create a suitable label.
2339
2340 by_category: a flag to divide the invoice into sections using display 
2341 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
2342 behavior.  Each section hash will have a 'category' element containing
2343 the section name from the display record (which probably equals the 
2344 category name of the package, but may not in some cases).
2345
2346 summary: a flag indicating that this is a summary-format invoice.
2347 Turning this on has the following effects:
2348 - Ignores display items with the 'summary' flag.
2349 - Places all sections in the "early" group even if they have post_total.
2350 - Creates sections for all non-disabled package categories, even if they 
2351 have no charges on this invoice, as well as a section with no name.
2352
2353 escape: an escape function to use for section titles.
2354
2355 extra_sections: an arrayref of additional sections to return after the 
2356 sorted list.  If there are any of these, section subtotals exclude 
2357 usage charges.
2358
2359 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
2360 passed through to C<_condense_section()>.
2361
2362 =cut
2363
2364 use vars qw(%pkg_category_cache);
2365 sub _items_sections {
2366   my $self = shift;
2367   my %opt = @_;
2368   
2369   my $escape = $opt{escape};
2370   my @extra_sections = @{ $opt{extra_sections} || [] };
2371
2372   # $subtotal{$locationnum}{$categoryname} = amount.
2373   # if we're not using by_location, $locationnum is undef.
2374   # if we're not using by_category, you guessed it, $categoryname is undef.
2375   # if we're not using either one, we shouldn't be here in the first place...
2376   my %subtotal = ();
2377   my %late_subtotal = ();
2378   my %not_tax = ();
2379
2380   # About tax items + multisection invoices:
2381   # If either invoice_*summary option is enabled, AND there is a 
2382   # package category with the name of the tax, then there will be 
2383   # a display record assigning the tax item to that category.
2384   #
2385   # However, the taxes are always placed in the "Taxes, Surcharges,
2386   # and Fees" section regardless of that.  The only effect of the 
2387   # display record is to create a subtotal for the summary page.
2388
2389   # cache these
2390   my $pkg_hash = $self->cust_pkg_hash;
2391
2392   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2393   {
2394
2395       my $usage = $cust_bill_pkg->usage;
2396
2397       my $locationnum;
2398       if ( $opt{by_location} ) {
2399         if ( $cust_bill_pkg->pkgnum ) {
2400           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2401         } else {
2402           $locationnum = '';
2403         }
2404       } else {
2405         $locationnum = undef;
2406       }
2407
2408       # as in _items_cust_pkg, if a line item has no display records,
2409       # cust_bill_pkg_display() returns a default record for it
2410
2411       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2412         next if ( $display->summary && $opt{summary} );
2413
2414         my $section = $display->section;
2415         my $type    = $display->type;
2416         # Set $section = undef if we're sectioning by location and this
2417         # line item _has_ a location (i.e. isn't a fee).
2418         $section = undef if $locationnum;
2419
2420         # set this flag if the section is not tax-only
2421         $not_tax{$locationnum}{$section} = 1
2422           if $cust_bill_pkg->pkgnum  or $cust_bill_pkg->feepart;
2423
2424         # there's actually a very important piece of logic buried in here:
2425         # incrementing $late_subtotal{$section} CREATES 
2426         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
2427         # to define the list of late sections, and likewise keys(%subtotal).
2428         # When _items_cust_bill_pkg is called to generate line items for 
2429         # real, it will be called with 'section' => $section for each 
2430         # of these.
2431         if ( $display->post_total && !$opt{summary} ) {
2432           if (! $type || $type eq 'S') {
2433             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2434               if $cust_bill_pkg->setup != 0
2435               || $cust_bill_pkg->setup_show_zero;
2436           }
2437
2438           if (! $type) {
2439             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2440               if $cust_bill_pkg->recur != 0
2441               || $cust_bill_pkg->recur_show_zero;
2442           }
2443
2444           if ($type && $type eq 'R') {
2445             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2446               if $cust_bill_pkg->recur != 0
2447               || $cust_bill_pkg->recur_show_zero;
2448           }
2449           
2450           if ($type && $type eq 'U') {
2451             $late_subtotal{$locationnum}{$section} += $usage
2452               unless scalar(@extra_sections);
2453           }
2454
2455         } else { # it's a pre-total (normal) section
2456
2457           # skip tax items unless they're explicitly included in a section
2458           next if $cust_bill_pkg->pkgnum == 0 and
2459                   ! $cust_bill_pkg->feepart   and
2460                   ! $section;
2461
2462           if ( $type eq 'S' ) {
2463             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2464               if $cust_bill_pkg->setup != 0
2465               || $cust_bill_pkg->setup_show_zero;
2466           } elsif ( $type eq 'R' ) {
2467             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2468               if $cust_bill_pkg->recur != 0
2469               || $cust_bill_pkg->recur_show_zero;
2470           } elsif ( $type eq 'U' ) {
2471             $subtotal{$locationnum}{$section} += $usage
2472               unless scalar(@extra_sections);
2473           } elsif ( !$type ) {
2474             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2475                                                + $cust_bill_pkg->recur;
2476           }
2477
2478         }
2479
2480       }
2481
2482   }
2483
2484   %pkg_category_cache = ();
2485
2486   # summary invoices need subtotals for all non-disabled package categories,
2487   # even if they're zero
2488   # but currently assume that there are no location sections, or at least
2489   # that the summary page doesn't care about them
2490   if ( $opt{summary} ) {
2491     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2492       $subtotal{''}{$category->categoryname} ||= 0;
2493     }
2494     $subtotal{''}{''} ||= 0;
2495   }
2496
2497   my @sections;
2498   foreach my $post_total (0,1) {
2499     my @these;
2500     my $s = $post_total ? \%late_subtotal : \%subtotal;
2501     foreach my $locationnum (keys %$s) {
2502       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2503         my $section = {
2504                         'subtotal'    => $s->{$locationnum}{$sectionname},
2505                         'post_total'  => $post_total,
2506                         'sort_weight' => 0,
2507                       };
2508         if ( $locationnum ) {
2509           $section->{'locationnum'} = $locationnum;
2510           my $location = FS::cust_location->by_key($locationnum);
2511           $section->{'description'} = &{ $escape }($location->location_label);
2512           # Better ideas? This will roughly group them by proximity, 
2513           # which alpha sorting on any of the address fields won't.
2514           # Sorting by locationnum is meaningless.
2515           # We have to sort on _something_ or the order may change 
2516           # randomly from one invoice to the next, which will confuse
2517           # people.
2518           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2519                                       $locationnum;
2520           $section->{'location'} = {
2521             label_prefix => &{ $escape }($location->label_prefix),
2522             map { $_ => &{ $escape }($location->get($_)) }
2523               $location->fields
2524           };
2525         } else {
2526           $section->{'category'} = $sectionname;
2527           $section->{'description'} = &{ $escape }($sectionname);
2528           if ( _pkg_category($sectionname) ) {
2529             $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2530             if ( _pkg_category($sectionname)->condense ) {
2531               $section = { %$section, $self->_condense_section($opt{format}) };
2532             }
2533           }
2534         }
2535         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2536           # then it's a tax-only section
2537           $section->{'summarized'} = 'Y';
2538           $section->{'tax_section'} = 'Y';
2539         }
2540         push @these, $section;
2541       } # foreach $sectionname
2542     } #foreach $locationnum
2543     push @these, @extra_sections if $post_total == 0;
2544     # need an alpha sort for location sections, because postal codes can 
2545     # be non-numeric
2546     $sections[ $post_total ] = [ sort {
2547       $opt{'by_location'} ? 
2548         ($a->{sort_weight} cmp $b->{sort_weight}) :
2549         ($a->{sort_weight} <=> $b->{sort_weight})
2550       } @these ];
2551   } #foreach $post_total
2552
2553   return @sections; # early, late
2554 }
2555
2556 #helper subs for above
2557
2558 sub cust_pkg_hash {
2559   my $self = shift;
2560   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2561 }
2562
2563 sub _pkg_category {
2564   my $categoryname = shift;
2565   $pkg_category_cache{$categoryname} ||=
2566     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2567 }
2568
2569 my %condensed_format = (
2570   'label' => [ qw( Description Qty Amount ) ],
2571   'fields' => [
2572                 sub { shift->{description} },
2573                 sub { shift->{quantity} },
2574                 sub { my($href, %opt) = @_;
2575                       ($opt{dollar} || ''). $href->{amount};
2576                     },
2577               ],
2578   'align'  => [ qw( l r r ) ],
2579   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2580   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2581 );
2582
2583 sub _condense_section {
2584   my ( $self, $format ) = ( shift, shift );
2585   ( 'condensed' => 1,
2586     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2587       qw( description_generator
2588           header_generator
2589           total_generator
2590           total_line_generator
2591         )
2592   );
2593 }
2594
2595 sub _condensed_generator_defaults {
2596   my ( $self, $format ) = ( shift, shift );
2597   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2598 }
2599
2600 my %html_align = (
2601   'c' => 'center',
2602   'l' => 'left',
2603   'r' => 'right',
2604 );
2605
2606 sub _condensed_header_generator {
2607   my ( $self, $format ) = ( shift, shift );
2608
2609   my ( $f, $prefix, $suffix, $separator, $column ) =
2610     _condensed_generator_defaults($format);
2611
2612   if ($format eq 'latex') {
2613     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2614     $suffix = "\\\\\n\\hline";
2615     $separator = "&\n";
2616     $column =
2617       sub { my ($d,$a,$s,$w) = @_;
2618             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2619           };
2620   } elsif ( $format eq 'html' ) {
2621     $prefix = '<th></th>';
2622     $suffix = '';
2623     $separator = '';
2624     $column =
2625       sub { my ($d,$a,$s,$w) = @_;
2626             return qq!<th align="$html_align{$a}">$d</th>!;
2627       };
2628   }
2629
2630   sub {
2631     my @args = @_;
2632     my @result = ();
2633
2634     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2635       push @result,
2636         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2637     }
2638
2639     $prefix. join($separator, @result). $suffix;
2640   };
2641
2642 }
2643
2644 sub _condensed_description_generator {
2645   my ( $self, $format ) = ( shift, shift );
2646
2647   my ( $f, $prefix, $suffix, $separator, $column ) =
2648     _condensed_generator_defaults($format);
2649
2650   my $money_char = '$';
2651   if ($format eq 'latex') {
2652     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2653     $suffix = '\\\\';
2654     $separator = " & \n";
2655     $column =
2656       sub { my ($d,$a,$s,$w) = @_;
2657             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2658           };
2659     $money_char = '\\dollar';
2660   }elsif ( $format eq 'html' ) {
2661     $prefix = '"><td align="center"></td>';
2662     $suffix = '';
2663     $separator = '';
2664     $column =
2665       sub { my ($d,$a,$s,$w) = @_;
2666             return qq!<td align="$html_align{$a}">$d</td>!;
2667       };
2668     #$money_char = $conf->config('money_char') || '$';
2669     $money_char = '';  # this is madness
2670   }
2671
2672   sub {
2673     #my @args = @_;
2674     my $href = shift;
2675     my @result = ();
2676
2677     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2678       my $dollar = '';
2679       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2680       push @result,
2681         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2682                     map { $f->{$_}->[$i] } qw(align span width)
2683                   );
2684     }
2685
2686     $prefix. join( $separator, @result ). $suffix;
2687   };
2688
2689 }
2690
2691 sub _condensed_total_generator {
2692   my ( $self, $format ) = ( shift, shift );
2693
2694   my ( $f, $prefix, $suffix, $separator, $column ) =
2695     _condensed_generator_defaults($format);
2696   my $style = '';
2697
2698   if ($format eq 'latex') {
2699     $prefix = "& ";
2700     $suffix = "\\\\\n";
2701     $separator = " & \n";
2702     $column =
2703       sub { my ($d,$a,$s,$w) = @_;
2704             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2705           };
2706   }elsif ( $format eq 'html' ) {
2707     $prefix = '';
2708     $suffix = '';
2709     $separator = '';
2710     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2711     $column =
2712       sub { my ($d,$a,$s,$w) = @_;
2713             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2714       };
2715   }
2716
2717
2718   sub {
2719     my @args = @_;
2720     my @result = ();
2721
2722     #  my $r = &{$f->{fields}->[$i]}(@args);
2723     #  $r .= ' Total' unless $i;
2724
2725     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2726       push @result,
2727         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2728                     map { $f->{$_}->[$i] } qw(align span width)
2729                   );
2730     }
2731
2732     $prefix. join( $separator, @result ). $suffix;
2733   };
2734
2735 }
2736
2737 =item total_line_generator FORMAT
2738
2739 Returns a coderef used for generation of invoice total line items for this
2740 usage_class.  FORMAT is either html or latex
2741
2742 =cut
2743
2744 # should not be used: will have issues with hash element names (description vs
2745 # total_item and amount vs total_amount -- another array of functions?
2746
2747 sub _condensed_total_line_generator {
2748   my ( $self, $format ) = ( shift, shift );
2749
2750   my ( $f, $prefix, $suffix, $separator, $column ) =
2751     _condensed_generator_defaults($format);
2752   my $style = '';
2753
2754   if ($format eq 'latex') {
2755     $prefix = "& ";
2756     $suffix = "\\\\\n";
2757     $separator = " & \n";
2758     $column =
2759       sub { my ($d,$a,$s,$w) = @_;
2760             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2761           };
2762   }elsif ( $format eq 'html' ) {
2763     $prefix = '';
2764     $suffix = '';
2765     $separator = '';
2766     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2767     $column =
2768       sub { my ($d,$a,$s,$w) = @_;
2769             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2770       };
2771   }
2772
2773
2774   sub {
2775     my @args = @_;
2776     my @result = ();
2777
2778     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2779       push @result,
2780         &{$column}( &{$f->{fields}->[$i]}(@args),
2781                     map { $f->{$_}->[$i] } qw(align span width)
2782                   );
2783     }
2784
2785     $prefix. join( $separator, @result ). $suffix;
2786   };
2787
2788 }
2789
2790 =item _items_pkg [ OPTIONS ]
2791
2792 Return line item hashes for each package item on this invoice. Nearly 
2793 equivalent to 
2794
2795 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2796
2797 The only OPTIONS accepted is 'section', which may point to a hashref 
2798 with a key named 'condensed', which may have a true value.  If it 
2799 does, this method tries to merge identical items into items with 
2800 'quantity' equal to the number of items (not the sum of their 
2801 separate quantities, for some reason).
2802
2803 =cut
2804
2805 sub _items_nontax {
2806   my $self = shift;
2807   # The order of these is important.  Bundled line items will be merged into
2808   # the most recent non-hidden item, so it needs to be the one with:
2809   # - the same pkgnum
2810   # - the same start date
2811   # - no pkgpart_override
2812   #
2813   # So: sort by pkgnum,
2814   # then by sdate
2815   # then sort the base line item before any overrides
2816   # then sort hidden before non-hidden add-ons
2817   # then sort by override pkgpart (for consistency)
2818   sort { $a->pkgnum <=> $b->pkgnum        or
2819          $a->sdate  <=> $b->sdate         or
2820          ($a->pkgpart_override ? 0 : -1)  or
2821          ($b->pkgpart_override ? 0 : 1)   or
2822          $b->hidden cmp $a->hidden        or
2823          $a->pkgpart_override <=> $b->pkgpart_override
2824        }
2825   # and of course exclude taxes and fees
2826   grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2827 }
2828
2829 sub _items_fee {
2830   my $self = shift;
2831   my %options = @_;
2832   my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2833   my @items;
2834   foreach my $cust_bill_pkg (@cust_bill_pkg) {
2835     # cache this, so we don't look it up again in every section
2836     my $part_fee = $cust_bill_pkg->get('part_fee')
2837        || $cust_bill_pkg->part_fee;
2838     $cust_bill_pkg->set('part_fee', $part_fee);
2839     if (!$part_fee) {
2840       #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2841       warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2842       next;
2843     }
2844     if ( exists($options{section}) and exists($options{section}{category}) )
2845     {
2846       my $categoryname = $options{section}{category};
2847       # then filter for items that have that section
2848       if ( $part_fee->categoryname ne $categoryname ) {
2849         warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
2850         next;
2851       }
2852     } # otherwise include them all in the main section
2853     # XXX what to do when sectioning by location?
2854     
2855     my @ext_desc;
2856     my %base_invnums; # invnum => invoice date
2857     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
2858       if ($_->base_invnum) {
2859         my $base_bill = FS::cust_bill->by_key($_->base_invnum);
2860         my $base_date = $self->time2str_local('short', $base_bill->_date)
2861           if $base_bill;
2862         $base_invnums{$_->base_invnum} = $base_date || '';
2863       }
2864     }
2865     foreach (sort keys(%base_invnums)) {
2866       next if $_ == $self->invnum;
2867       push @ext_desc,
2868         $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_});
2869     }
2870     push @items,
2871       { feepart     => $cust_bill_pkg->feepart,
2872         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2873         description => $part_fee->itemdesc_locale($self->cust_main->locale),
2874         ext_description => \@ext_desc
2875         # sdate/edate?
2876       };
2877   }
2878   @items;
2879 }
2880
2881 sub _items_pkg {
2882   my $self = shift;
2883   my %options = @_;
2884
2885   warn "$me _items_pkg searching for all package line items\n"
2886     if $DEBUG > 1;
2887
2888   my @cust_bill_pkg = $self->_items_nontax;
2889
2890   warn "$me _items_pkg filtering line items\n"
2891     if $DEBUG > 1;
2892   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2893
2894   if ($options{section} && $options{section}->{condensed}) {
2895
2896     warn "$me _items_pkg condensing section\n"
2897       if $DEBUG > 1;
2898
2899     my %itemshash = ();
2900     local $Storable::canonical = 1;
2901     foreach ( @items ) {
2902       my $item = { %$_ };
2903       delete $item->{ref};
2904       delete $item->{ext_description};
2905       my $key = freeze($item);
2906       $itemshash{$key} ||= 0;
2907       $itemshash{$key} ++; # += $item->{quantity};
2908     }
2909     @items = sort { $a->{description} cmp $b->{description} }
2910              map { my $i = thaw($_);
2911                    $i->{quantity} = $itemshash{$_};
2912                    $i->{amount} =
2913                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2914                    $i;
2915                  }
2916              keys %itemshash;
2917   }
2918
2919   warn "$me _items_pkg returning ". scalar(@items). " items\n"
2920     if $DEBUG > 1;
2921
2922   @items;
2923 }
2924
2925 sub _taxsort {
2926   return 0 unless $a->itemdesc cmp $b->itemdesc;
2927   return -1 if $b->itemdesc eq 'Tax';
2928   return 1 if $a->itemdesc eq 'Tax';
2929   return -1 if $b->itemdesc eq 'Other surcharges';
2930   return 1 if $a->itemdesc eq 'Other surcharges';
2931   $a->itemdesc cmp $b->itemdesc;
2932 }
2933
2934 sub _items_tax {
2935   my $self = shift;
2936   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
2937     $self->cust_bill_pkg;
2938   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2939
2940   if ( $self->conf->exists('always_show_tax') ) {
2941     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2942     if (0 == grep { $_->{description} eq $itemdesc } @items) {
2943       push @items,
2944         { 'description' => $itemdesc,
2945           'amount'      => 0.00 };
2946     }
2947   }
2948   @items;
2949 }
2950
2951 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2952
2953 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2954 list of hashrefs describing the line items they generate on the invoice.
2955
2956 OPTIONS may include:
2957
2958 format: the invoice format.
2959
2960 escape_function: the function used to escape strings.
2961
2962 DEPRECATED? (expensive, mostly unused?)
2963 format_function: the function used to format CDRs.
2964
2965 section: a hashref containing 'category' and/or 'locationnum'; if this 
2966 is present, only returns line items that belong to that category and/or
2967 location (whichever is defined).
2968
2969 multisection: a flag indicating that this is a multisection invoice,
2970 which does something complicated.
2971
2972 preref_callback: coderef run for each line item, code should return HTML to be
2973 displayed before that line item (quotations only)
2974
2975 Returns a list of hashrefs, each of which may contain:
2976
2977 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
2978 ext_description, which is an arrayref of detail lines to show below 
2979 the package line.
2980
2981 =cut
2982
2983 sub _items_cust_bill_pkg {
2984   my $self = shift;
2985   my $conf = $self->conf;
2986   my $cust_bill_pkgs = shift;
2987   my %opt = @_;
2988
2989   my $format = $opt{format} || '';
2990   my $escape_function = $opt{escape_function} || sub { shift };
2991   my $format_function = $opt{format_function} || '';
2992   my $no_usage = $opt{no_usage} || '';
2993   my $unsquelched = $opt{unsquelched} || ''; #unused
2994   my ($section, $locationnum, $category);
2995   if ( $opt{section} ) {
2996     $category = $opt{section}->{category};
2997     $locationnum = $opt{section}->{locationnum};
2998   }
2999   my $summary_page = $opt{summary_page} || ''; #unused
3000   my $multisection = defined($category) || defined($locationnum);
3001   my $discount_show_always = 0;
3002
3003   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
3004
3005   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
3006                                    # and location labels
3007
3008   my @b = (); # accumulator for the line item hashes that we'll return
3009   my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
3010             # the 'current' line item hashes for setup, recur, usage, discount
3011   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
3012   {
3013     # if the current line item is waiting to go out, and the one we're about
3014     # to start is not bundled, then push out the current one and start a new
3015     # one.
3016     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3017       if ( $_ && !$cust_bill_pkg->hidden ) {
3018         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
3019         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3020         if (exists($_->{unit_amount})) {
3021           $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3022         }
3023         push @b, { %$_ }
3024           if $_->{amount} != 0
3025           || $discount_show_always
3026           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3027           || (   $_->{_is_setup} && $_->{setup_show_zero} )
3028         ;
3029         $_ = undef;
3030       }
3031     }
3032
3033     if ( $locationnum ) {
3034       # this is a location section; skip packages that aren't at this
3035       # service location.
3036       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
3037       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
3038               != $locationnum;
3039     }
3040
3041     # Consider display records for this item to determine if it belongs
3042     # in this section.  Note that if there are no display records, there
3043     # will be a default pseudo-record that includes all charge types 
3044     # and has no section name.
3045     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
3046                                   ? $cust_bill_pkg->cust_bill_pkg_display
3047                                   : ( $cust_bill_pkg );
3048
3049     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
3050          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
3051       if $DEBUG > 1;
3052
3053     if ( defined($category) ) {
3054       # then this is a package category section; process all display records
3055       # that belong to this section.
3056       @cust_bill_pkg_display = grep { $_->section eq $category }
3057                                 @cust_bill_pkg_display;
3058     } else {
3059       # otherwise, process all display records that aren't usage summaries
3060       # (I don't think there should be usage summaries if you aren't using 
3061       # category sections, but this is the historical behavior)
3062       @cust_bill_pkg_display = grep { !$_->summary }
3063                                 @cust_bill_pkg_display;
3064     }
3065
3066     my $classname = ''; # package class name, will fill in later
3067
3068     foreach my $display (@cust_bill_pkg_display) {
3069
3070       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
3071            $display->billpkgdisplaynum. "\n"
3072         if $DEBUG > 1;
3073
3074       my $type = $display->type;
3075
3076       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
3077       $desc = substr($desc, 0, $maxlength). '...'
3078         if $format eq 'latex' && length($desc) > $maxlength;
3079
3080       my %details_opt = ( 'format'          => $format,
3081                           'escape_function' => $escape_function,
3082                           'format_function' => $format_function,
3083                           'no_usage'        => $opt{'no_usage'},
3084                         );
3085
3086       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
3087
3088         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
3089           if $DEBUG > 1;
3090         # quotation_pkgs are never fees, so don't worry about the case where
3091         # part_pkg is undefined
3092
3093         # and I guess they're never bundled either?
3094         if ( $cust_bill_pkg->setup != 0 ) {
3095           my $description = $desc;
3096           $description .= ' Setup'
3097             if $cust_bill_pkg->recur != 0
3098             || $discount_show_always
3099             || $cust_bill_pkg->recur_show_zero;
3100           #push @b, {
3101           # keep it consistent, please
3102           $s = {
3103             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3104             'description' => $description,
3105             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3106             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
3107             'quantity'    => $cust_bill_pkg->quantity,
3108             'preref_html' => ( $opt{preref_callback}
3109                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3110                                  : ''
3111                              ),
3112           };
3113         }
3114         if ( $cust_bill_pkg->recur != 0 ) {
3115           #push @b, {
3116           $r = {
3117             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3118             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
3119             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3120             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
3121             'quantity'    => $cust_bill_pkg->quantity,
3122            'preref_html'  => ( $opt{preref_callback}
3123                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3124                                  : ''
3125                              ),
3126           };
3127         }
3128
3129       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
3130         # a "normal" package line item (not a quotation, not a fee, not a tax)
3131
3132         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
3133           if $DEBUG > 1;
3134  
3135         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3136         my $part_pkg = $cust_pkg->part_pkg;
3137
3138         # which pkgpart to show for display purposes?
3139         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
3140
3141         # start/end dates for invoice formats that do nonstandard 
3142         # things with them
3143         my %item_dates = ();
3144         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
3145           unless $part_pkg->option('disable_line_item_date_ranges',1);
3146
3147         # not normally used, but pass this to the template anyway
3148         $classname = $part_pkg->classname;
3149
3150         if (    (!$type || $type eq 'S')
3151              && (    $cust_bill_pkg->setup != 0
3152                   || $cust_bill_pkg->setup_show_zero
3153                 )
3154            )
3155          {
3156
3157           warn "$me _items_cust_bill_pkg adding setup\n"
3158             if $DEBUG > 1;
3159
3160           my $description = $desc;
3161           $description .= ' Setup'
3162             if $cust_bill_pkg->recur != 0
3163             || $discount_show_always
3164             || $cust_bill_pkg->recur_show_zero;
3165
3166           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3167                                                               $self->agentnum )
3168             if $part_pkg->is_prepaid #for prepaid, "display the validity period
3169                                      # triggered by the recurring charge freq
3170                                      # (RT#26274)
3171             && $cust_bill_pkg->recur == 0
3172             && ! $cust_bill_pkg->recur_show_zero;
3173
3174           my @d = ();
3175           my $svc_label;
3176
3177           # always pass the svc_label through to the template, even if 
3178           # not displaying it as an ext_description
3179           my @svc_labels = map &{$escape_function}($_),
3180                       $cust_pkg->h_labels_short($self->_date, undef, 'I');
3181
3182           $svc_label = $svc_labels[0];
3183
3184           unless ( $cust_pkg->part_pkg->hide_svc_detail
3185                 || $cust_bill_pkg->hidden )
3186           {
3187
3188             push @d, @svc_labels
3189               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3190             my $lnum = $cust_main ? $cust_main->ship_locationnum
3191                                   : $self->prospect_main->locationnum;
3192             # show the location label if it's not the customer's default
3193             # location, and we're not grouping items by location already
3194             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
3195               my $loc = $cust_pkg->location_label;
3196               $loc = substr($loc, 0, $maxlength). '...'
3197                 if $format eq 'latex' && length($loc) > $maxlength;
3198               push @d, &{$escape_function}($loc);
3199             }
3200
3201           } #unless hiding service details
3202
3203           push @d, $cust_bill_pkg->details(%details_opt)
3204             if $cust_bill_pkg->recur == 0;
3205
3206           if ( $cust_bill_pkg->hidden ) {
3207             $s->{amount}      += $cust_bill_pkg->setup;
3208             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3209             push @{ $s->{ext_description} }, @d;
3210           } else {
3211             $s = {
3212               _is_setup       => 1,
3213               description     => $description,
3214               pkgpart         => $pkgpart,
3215               pkgnum          => $cust_bill_pkg->pkgnum,
3216               amount          => $cust_bill_pkg->setup,
3217               setup_show_zero => $cust_bill_pkg->setup_show_zero,
3218               unit_amount     => $cust_bill_pkg->unitsetup,
3219               quantity        => $cust_bill_pkg->quantity,
3220               ext_description => \@d,
3221               svc_label       => ($svc_label || ''),
3222               locationnum     => $cust_pkg->locationnum, # sure, why not?
3223             };
3224           };
3225
3226         }
3227
3228         if (    ( !$type || $type eq 'R' || $type eq 'U' )
3229              && (
3230                      $cust_bill_pkg->recur != 0
3231                   || $cust_bill_pkg->setup == 0
3232                   || $discount_show_always
3233                   || $cust_bill_pkg->recur_show_zero
3234                 )
3235            )
3236         {
3237
3238           warn "$me _items_cust_bill_pkg adding recur/usage\n"
3239             if $DEBUG > 1;
3240
3241           my $is_summary = $display->summary;
3242           my $description = $desc;
3243           if ( $type eq 'U' and defined($r) ) {
3244             # don't just show the same description as the recur line
3245             $description = $self->mt('Usage charges');
3246           }
3247
3248           my $part_pkg = $cust_pkg->part_pkg;
3249
3250           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3251                                                               $self->agentnum );
3252
3253           my @d = ();
3254           my @seconds = (); # for display of usage info
3255           my $svc_label = '';
3256
3257           #at least until cust_bill_pkg has "past" ranges in addition to
3258           #the "future" sdate/edate ones... see #3032
3259           my @dates = ( $self->_date );
3260           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3261           push @dates, $prev->sdate if $prev;
3262           push @dates, undef if !$prev;
3263
3264           my @svc_labels = map &{$escape_function}($_),
3265                       $cust_pkg->h_labels_short(@dates, 'I');
3266           $svc_label = $svc_labels[0];
3267
3268           # show service labels, unless...
3269                     # the package is set not to display them
3270           unless ( $part_pkg->hide_svc_detail
3271                     # or this is a tax-like line item
3272                 || $cust_bill_pkg->itemdesc
3273                     # or this is a hidden (bundled) line item
3274                 || $cust_bill_pkg->hidden
3275                     # or this is a usage summary line
3276                 || $is_summary && $type && $type eq 'U'
3277                     # or this is a usage line and there's a recurring line
3278                     # for the package in the same section (which will 
3279                     # have service labels already)
3280                 || ($type eq 'U' and defined($r))
3281               )
3282           {
3283
3284             warn "$me _items_cust_bill_pkg adding service details\n"
3285               if $DEBUG > 1;
3286
3287             push @d, @svc_labels
3288               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3289             warn "$me _items_cust_bill_pkg done adding service details\n"
3290               if $DEBUG > 1;
3291
3292             my $lnum = $cust_main ? $cust_main->ship_locationnum
3293                                   : $self->prospect_main->locationnum;
3294             # show the location label if it's not the customer's default
3295             # location, and we're not grouping items by location already
3296             if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
3297               my $loc = $cust_pkg->location_label;
3298               $loc = substr($loc, 0, $maxlength). '...'
3299                 if $format eq 'latex' && length($loc) > $maxlength;
3300               push @d, &{$escape_function}($loc);
3301             }
3302
3303             # Display of seconds_since_sqlradacct:
3304             # On the invoice, when processing @detail_items, look for a field
3305             # named 'seconds'.  This will contain total seconds for each 
3306             # service, in the same order as @ext_description.  For services 
3307             # that don't support this it will show undef.
3308             if ( $conf->exists('svc_acct-usage_seconds') 
3309                  and ! $cust_bill_pkg->pkgpart_override ) {
3310               foreach my $cust_svc ( 
3311                   $cust_pkg->h_cust_svc(@dates, 'I') 
3312                 ) {
3313
3314                 # eval because not having any part_export_usage exports 
3315                 # is a fatal error, last_bill/_date because that's how 
3316                 # sqlradius_hour billing does it
3317                 my $sec = eval {
3318                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
3319                 };
3320                 push @seconds, $sec;
3321               }
3322             } #if svc_acct-usage_seconds
3323
3324           } # if we are showing service labels
3325
3326           unless ( $is_summary ) {
3327             warn "$me _items_cust_bill_pkg adding details\n"
3328               if $DEBUG > 1;
3329
3330             #instead of omitting details entirely in this case (unwanted side
3331             # effects), just omit CDRs
3332             $details_opt{'no_usage'} = 1
3333               if $type && $type eq 'R';
3334
3335             push @d, $cust_bill_pkg->details(%details_opt);
3336           }
3337
3338           warn "$me _items_cust_bill_pkg calculating amount\n"
3339             if $DEBUG > 1;
3340   
3341           my $amount = 0;
3342           if (!$type) {
3343             $amount = $cust_bill_pkg->recur;
3344           } elsif ($type eq 'R') {
3345             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3346           } elsif ($type eq 'U') {
3347             $amount = $cust_bill_pkg->usage;
3348           }
3349   
3350           if ( !$type || $type eq 'R' ) {
3351
3352             warn "$me _items_cust_bill_pkg adding recur\n"
3353               if $DEBUG > 1;
3354
3355             my $unit_amount =
3356               ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
3357                                                 : $amount;
3358
3359             if ( $cust_bill_pkg->hidden ) {
3360               $r->{amount}      += $amount;
3361               $r->{unit_amount} += $unit_amount;
3362               push @{ $r->{ext_description} }, @d;
3363             } else {
3364               $r = {
3365                 description     => $description,
3366                 pkgpart         => $pkgpart,
3367                 pkgnum          => $cust_bill_pkg->pkgnum,
3368                 amount          => $amount,
3369                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3370                 unit_amount     => $unit_amount,
3371                 quantity        => $cust_bill_pkg->quantity,
3372                 %item_dates,
3373                 ext_description => \@d,
3374                 svc_label       => ($svc_label || ''),
3375                 locationnum     => $cust_pkg->locationnum,
3376               };
3377               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3378             }
3379
3380           } else {  # $type eq 'U'
3381
3382             warn "$me _items_cust_bill_pkg adding usage\n"
3383               if $DEBUG > 1;
3384
3385             if ( $cust_bill_pkg->hidden and defined($u) ) {
3386               # if this is a hidden package and there's already a usage
3387               # line for the bundle, add this package's total amount and
3388               # usage details to it
3389               $u->{amount}      += $amount;
3390               push @{ $u->{ext_description} }, @d;
3391             } elsif ( $amount ) {
3392               # create a new usage line
3393               $u = {
3394                 description     => $description,
3395                 pkgpart         => $pkgpart,
3396                 pkgnum          => $cust_bill_pkg->pkgnum,
3397                 amount          => $amount,
3398                 usage_item      => 1,
3399                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3400                 %item_dates,
3401                 ext_description => \@d,
3402                 locationnum     => $cust_pkg->locationnum,
3403               };
3404             } # else this has no usage, so don't create a usage section
3405           }
3406
3407         } # recurring or usage with recurring charge
3408
3409       } else { # taxes and fees
3410
3411         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3412           if $DEBUG > 1;
3413
3414         # items of this kind should normally not have sdate/edate.
3415         push @b, {
3416           'description' => $desc,
3417           'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
3418                                            + $cust_bill_pkg->recur)
3419         };
3420
3421       } # if quotation / package line item / other line item
3422
3423       # decide whether to show active discounts here
3424       if (
3425           # case 1: we are showing a single line for the package
3426           ( !$type )
3427           # case 2: we are showing a setup line for a package that has
3428           # no base recurring fee
3429           or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3430           # case 3: we are showing a recur line for a package that has 
3431           # a base recurring fee
3432           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3433       ) {
3434
3435         my $item_discount = $cust_bill_pkg->_item_discount;
3436         if ( $item_discount ) {
3437           # $item_discount->{amount} is negative
3438
3439           if ( $d and $cust_bill_pkg->hidden ) {
3440             $d->{amount}      += $item_discount->{amount};
3441           } else {
3442             $d = $item_discount;
3443             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
3444           }
3445
3446           # update the active line (before the discount) to show the 
3447           # original price (whether this is a hidden line or not)
3448           #
3449           # quotation discounts keep track of setup and recur; invoice 
3450           # discounts currently don't
3451           if ( exists $item_discount->{setup_amount} ) {
3452
3453             $s->{amount} -= $item_discount->{setup_amount} if $s;
3454             $r->{amount} -= $item_discount->{recur_amount} if $r;
3455
3456           } else {
3457
3458             # $active_line is the line item hashref for the line that will
3459             # show the original price
3460             # (use the recur or single line for the package, unless we're 
3461             # showing a setup line for a package with no recurring fee)
3462             my $active_line = $r;
3463             if ( $type eq 'S' ) {
3464               $active_line = $s;
3465             }
3466             $active_line->{amount} -= $item_discount->{amount};
3467
3468           }
3469
3470         } # if there are any discounts
3471       } # if this is an appropriate place to show discounts
3472
3473     } # foreach $display
3474
3475     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
3476                                 && $conf->exists('discount-show-always'));
3477
3478   }
3479
3480   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3481     if ( $_  ) {
3482       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3483         if exists($_->{amount});
3484       $_->{amount}      =~ s/^\-0\.00$/0.00/;
3485       if (exists($_->{unit_amount})) {
3486         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3487       }
3488
3489       push @b, { %$_ }
3490         if $_->{amount} != 0
3491         || $discount_show_always
3492         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3493         || (   $_->{_is_setup} && $_->{setup_show_zero} )
3494     }
3495   }
3496
3497   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3498     if $DEBUG > 1;
3499
3500   @b;
3501
3502 }
3503
3504 =item _items_discounts_avail
3505
3506 Returns an array of line item hashrefs representing available term discounts
3507 for this invoice.  This makes the same assumptions that apply to term 
3508 discounts in general: that the package is billed monthly, at a flat rate, 
3509 with no usage charges.  A prorated first month will be handled, as will 
3510 a setup fee if the discount is allowed to apply to setup fees.
3511
3512 =cut
3513
3514 sub _items_discounts_avail {
3515   my $self = shift;
3516
3517   #maybe move this method from cust_bill when quotations support discount_plans 
3518   return () unless $self->can('discount_plans');
3519   my %plans = $self->discount_plans;
3520
3521   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3522   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3523
3524   map {
3525     my $months = $_;
3526     my $plan = $plans{$months};
3527
3528     my $term_total = sprintf('%.2f', $plan->discounted_total);
3529     my $percent = sprintf('%.0f', 
3530                           100 * (1 - $term_total / $plan->base_total) );
3531     my $permonth = sprintf('%.2f', $term_total / $months);
3532     my $detail = $self->mt('discount on item'). ' '.
3533                  join(', ', map { "#$_" } $plan->pkgnums)
3534       if $list_pkgnums;
3535
3536     # discounts for non-integer months don't work anyway
3537     $months = sprintf("%d", $months);
3538
3539     +{
3540       description => $self->mt('Save [_1]% by paying for [_2] months',
3541                                 $percent, $months),
3542       amount      => $self->mt('[_1] ([_2] per month)', 
3543                                 $term_total, $money_char.$permonth),
3544       ext_description => ($detail || ''),
3545     }
3546   } #map
3547   sort { $b <=> $a } keys %plans;
3548
3549 }
3550
3551 1;