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