don't try to produce plain text quotations without a template, #34460, from #31786
[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     if ( $conf->config($tc.'template') ) {
2097
2098       warn "$me generating plain text invoice"
2099         if $DEBUG;
2100
2101       # 'print_text' argument is no longer used
2102       @text = $self->print_text(\%args);
2103
2104     } else {
2105
2106       warn "$me no plain text version exists; sending empty message body"
2107         if $DEBUG;
2108
2109     }
2110
2111   }
2112
2113   my $text_part = build MIME::Entity (
2114     'Type'        => 'text/plain',
2115     'Encoding'    => 'quoted-printable',
2116     'Charset'     => 'UTF-8',
2117     #'Encoding'    => '7bit',
2118     'Data'        => \@text,
2119     'Disposition' => 'inline',
2120   );
2121
2122   if (!$html) {
2123
2124     if ( $conf->exists($tc.'html') ) {
2125       warn "$me generating HTML invoice"
2126         if $DEBUG;
2127
2128       $args{'from'} =~ /\@([\w\.\-]+)/;
2129       my $from = $1 || 'example.com';
2130       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2131
2132       my $logo;
2133       my $agentnum = $cust_main ? $cust_main->agentnum
2134                                 : $self->prospect_main->agentnum;
2135       if ( defined($args{'template'}) && length($args{'template'})
2136            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
2137          )
2138       {
2139         $logo = 'logo_'. $args{'template'}. '.png';
2140       } else {
2141         $logo = "logo.png";
2142       }
2143       my $image_data = $conf->config_binary( $logo, $agentnum);
2144
2145       push @related_parts, build MIME::Entity
2146         'Type'       => 'image/png',
2147         'Encoding'   => 'base64',
2148         'Data'       => $image_data,
2149         'Filename'   => 'logo.png',
2150         'Content-ID' => "<$content_id>",
2151       ;
2152    
2153       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
2154         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2155         push @related_parts, build MIME::Entity
2156           'Type'       => 'image/png',
2157           'Encoding'   => 'base64',
2158           'Data'       => $self->invoice_barcode(0),
2159           'Filename'   => 'barcode.png',
2160           'Content-ID' => "<$barcode_content_id>",
2161         ;
2162         $args{'barcode_cid'} = $barcode_content_id;
2163       }
2164
2165       $html = $self->print_html({ 'cid'=>$content_id, %args });
2166     }
2167
2168   }
2169
2170   if ( $html ) {
2171
2172     warn "$me creating HTML/text multipart message"
2173       if $DEBUG;
2174
2175     $return{'nobody'} = 1;
2176
2177     my $alternative = build MIME::Entity
2178       'Type'        => 'multipart/alternative',
2179       #'Encoding'    => '7bit',
2180       'Disposition' => 'inline'
2181     ;
2182
2183     if ( @text ) {
2184       $alternative->add_part($text_part);
2185     }
2186
2187     $alternative->attach(
2188       'Type'        => 'text/html',
2189       'Encoding'    => 'quoted-printable',
2190       'Data'        => [ '<html>',
2191                          '  <head>',
2192                          '    <title>',
2193                          '      '. encode_entities($return{'subject'}), 
2194                          '    </title>',
2195                          '  </head>',
2196                          '  <body bgcolor="#e8e8e8">',
2197                          $html,
2198                          '  </body>',
2199                          '</html>',
2200                        ],
2201       'Disposition' => 'inline',
2202       #'Filename'    => 'invoice.pdf',
2203     );
2204
2205     unshift @related_parts, $alternative;
2206
2207     $related = build MIME::Entity 'Type'     => 'multipart/related',
2208                                   'Encoding' => '7bit';
2209
2210     #false laziness w/Misc::send_email
2211     $related->head->replace('Content-type',
2212       $related->mime_type.
2213       '; boundary="'. $related->head->multipart_boundary. '"'.
2214       '; type=multipart/alternative'
2215     );
2216
2217     $related->add_part($_) foreach @related_parts;
2218
2219   }
2220
2221   my @otherparts = ();
2222   if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
2223
2224     push @otherparts, build MIME::Entity
2225       'Type'        => 'text/csv',
2226       'Encoding'    => '7bit',
2227       'Data'        => [ map { "$_\n" }
2228                            $self->call_details('prepend_billed_number' => 1)
2229                        ],
2230       'Disposition' => 'attachment',
2231       'Filename'    => 'usage-'. $self->invnum. '.csv',
2232     ;
2233
2234   }
2235
2236   if ( $conf->exists($tc.'email_pdf') ) {
2237
2238     #attaching pdf too:
2239     # multipart/mixed
2240     #   multipart/related
2241     #     multipart/alternative
2242     #       text/plain
2243     #       text/html
2244     #     image/png
2245     #   application/pdf
2246
2247     my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
2248     push @otherparts, $pdf;
2249   }
2250
2251   if (@otherparts) {
2252     $return{'content-type'} = 'multipart/mixed'; # of the outer container
2253     if ( $html ) {
2254       $return{'mimeparts'} = [ $related, @otherparts ];
2255       $return{'type'} = 'multipart/related'; # of the first part
2256     } else {
2257       $return{'mimeparts'} = [ $text_part, @otherparts ];
2258       $return{'type'} = 'text/plain';
2259     }
2260   } elsif ( $html ) { # no PDF or CSV, strip the outer container
2261     $return{'mimeparts'} = \@related_parts;
2262     $return{'content-type'} = 'multipart/related';
2263     $return{'type'} = 'multipart/alternative';
2264   } else { # no HTML either
2265     $return{'body'} = \@text;
2266     $return{'content-type'} = 'text/plain';
2267   }
2268
2269   %return;
2270
2271 }
2272
2273 =item mimebuild_pdf
2274
2275 Returns a list suitable for passing to MIME::Entity->build(), representing
2276 this invoice as PDF attachment.
2277
2278 =cut
2279
2280 sub mimebuild_pdf {
2281   my $self = shift;
2282   (
2283     'Type'        => 'application/pdf',
2284     'Encoding'    => 'base64',
2285     'Data'        => [ $self->print_pdf(@_) ],
2286     'Disposition' => 'attachment',
2287     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
2288   );
2289 }
2290
2291 =item _items_sections OPTIONS
2292
2293 Generate section information for all items appearing on this invoice.
2294 This will only be called for multi-section invoices.
2295
2296 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
2297 related display records (L<FS::cust_bill_pkg_display>) and organize 
2298 them into two groups ("early" and "late" according to whether they come 
2299 before or after the total), then into sections.  A subtotal is calculated 
2300 for each section.
2301
2302 Section descriptions are returned in sort weight order.  Each consists 
2303 of a hash containing:
2304
2305 description: the package category name, escaped
2306 subtotal: the total charges in that section
2307 tax_section: a flag indicating that the section contains only tax charges
2308 summarized: same as tax_section, for some reason
2309 sort_weight: the package category's sort weight
2310
2311 If 'condense' is set on the display record, it also contains everything 
2312 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
2313 coderefs to generate parts of the invoice.  This is not advised.
2314
2315 The method returns two arrayrefs, one of "early" sections and one of "late"
2316 sections.
2317
2318 OPTIONS may include:
2319
2320 by_location: a flag to divide the invoice into sections by location.  
2321 Each section hash will have a 'location' element containing a hashref of 
2322 the location fields (see L<FS::cust_location>).  The section description
2323 will be the location label, but the template can use any of the location 
2324 fields to create a suitable label.
2325
2326 by_category: a flag to divide the invoice into sections using display 
2327 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
2328 behavior.  Each section hash will have a 'category' element containing
2329 the section name from the display record (which probably equals the 
2330 category name of the package, but may not in some cases).
2331
2332 summary: a flag indicating that this is a summary-format invoice.
2333 Turning this on has the following effects:
2334 - Ignores display items with the 'summary' flag.
2335 - Places all sections in the "early" group even if they have post_total.
2336 - Creates sections for all non-disabled package categories, even if they 
2337 have no charges on this invoice, as well as a section with no name.
2338
2339 escape: an escape function to use for section titles.
2340
2341 extra_sections: an arrayref of additional sections to return after the 
2342 sorted list.  If there are any of these, section subtotals exclude 
2343 usage charges.
2344
2345 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
2346 passed through to C<_condense_section()>.
2347
2348 =cut
2349
2350 use vars qw(%pkg_category_cache);
2351 sub _items_sections {
2352   my $self = shift;
2353   my %opt = @_;
2354   
2355   my $escape = $opt{escape};
2356   my @extra_sections = @{ $opt{extra_sections} || [] };
2357
2358   # $subtotal{$locationnum}{$categoryname} = amount.
2359   # if we're not using by_location, $locationnum is undef.
2360   # if we're not using by_category, you guessed it, $categoryname is undef.
2361   # if we're not using either one, we shouldn't be here in the first place...
2362   my %subtotal = ();
2363   my %late_subtotal = ();
2364   my %not_tax = ();
2365
2366   # About tax items + multisection invoices:
2367   # If either invoice_*summary option is enabled, AND there is a 
2368   # package category with the name of the tax, then there will be 
2369   # a display record assigning the tax item to that category.
2370   #
2371   # However, the taxes are always placed in the "Taxes, Surcharges,
2372   # and Fees" section regardless of that.  The only effect of the 
2373   # display record is to create a subtotal for the summary page.
2374
2375   # cache these
2376   my $pkg_hash = $self->cust_pkg_hash;
2377
2378   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2379   {
2380
2381       my $usage = $cust_bill_pkg->usage;
2382
2383       my $locationnum;
2384       if ( $opt{by_location} ) {
2385         if ( $cust_bill_pkg->pkgnum ) {
2386           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2387         } else {
2388           $locationnum = '';
2389         }
2390       } else {
2391         $locationnum = undef;
2392       }
2393
2394       # as in _items_cust_pkg, if a line item has no display records,
2395       # cust_bill_pkg_display() returns a default record for it
2396
2397       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2398         next if ( $display->summary && $opt{summary} );
2399
2400         my $section = $display->section;
2401         my $type    = $display->type;
2402         # Set $section = undef if we're sectioning by location and this
2403         # line item _has_ a location (i.e. isn't a fee).
2404         $section = undef if $locationnum;
2405
2406         # set this flag if the section is not tax-only
2407         $not_tax{$locationnum}{$section} = 1
2408           if $cust_bill_pkg->pkgnum  or $cust_bill_pkg->feepart;
2409
2410         # there's actually a very important piece of logic buried in here:
2411         # incrementing $late_subtotal{$section} CREATES 
2412         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
2413         # to define the list of late sections, and likewise keys(%subtotal).
2414         # When _items_cust_bill_pkg is called to generate line items for 
2415         # real, it will be called with 'section' => $section for each 
2416         # of these.
2417         if ( $display->post_total && !$opt{summary} ) {
2418           if (! $type || $type eq 'S') {
2419             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2420               if $cust_bill_pkg->setup != 0
2421               || $cust_bill_pkg->setup_show_zero;
2422           }
2423
2424           if (! $type) {
2425             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2426               if $cust_bill_pkg->recur != 0
2427               || $cust_bill_pkg->recur_show_zero;
2428           }
2429
2430           if ($type && $type eq 'R') {
2431             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2432               if $cust_bill_pkg->recur != 0
2433               || $cust_bill_pkg->recur_show_zero;
2434           }
2435           
2436           if ($type && $type eq 'U') {
2437             $late_subtotal{$locationnum}{$section} += $usage
2438               unless scalar(@extra_sections);
2439           }
2440
2441         } else { # it's a pre-total (normal) section
2442
2443           # skip tax items unless they're explicitly included in a section
2444           next if $cust_bill_pkg->pkgnum == 0 and
2445                   ! $cust_bill_pkg->feepart   and
2446                   ! $section;
2447
2448           if ( $type eq 'S' ) {
2449             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2450               if $cust_bill_pkg->setup != 0
2451               || $cust_bill_pkg->setup_show_zero;
2452           } elsif ( $type eq 'R' ) {
2453             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2454               if $cust_bill_pkg->recur != 0
2455               || $cust_bill_pkg->recur_show_zero;
2456           } elsif ( $type eq 'U' ) {
2457             $subtotal{$locationnum}{$section} += $usage
2458               unless scalar(@extra_sections);
2459           } elsif ( !$type ) {
2460             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2461                                                + $cust_bill_pkg->recur;
2462           }
2463
2464         }
2465
2466       }
2467
2468   }
2469
2470   %pkg_category_cache = ();
2471
2472   # summary invoices need subtotals for all non-disabled package categories,
2473   # even if they're zero
2474   # but currently assume that there are no location sections, or at least
2475   # that the summary page doesn't care about them
2476   if ( $opt{summary} ) {
2477     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2478       $subtotal{''}{$category->categoryname} ||= 0;
2479     }
2480     $subtotal{''}{''} ||= 0;
2481   }
2482
2483   my @sections;
2484   foreach my $post_total (0,1) {
2485     my @these;
2486     my $s = $post_total ? \%late_subtotal : \%subtotal;
2487     foreach my $locationnum (keys %$s) {
2488       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2489         my $section = {
2490                         'subtotal'    => $s->{$locationnum}{$sectionname},
2491                         'post_total'  => $post_total,
2492                         'sort_weight' => 0,
2493                       };
2494         if ( $locationnum ) {
2495           $section->{'locationnum'} = $locationnum;
2496           my $location = FS::cust_location->by_key($locationnum);
2497           $section->{'description'} = &{ $escape }($location->location_label);
2498           # Better ideas? This will roughly group them by proximity, 
2499           # which alpha sorting on any of the address fields won't.
2500           # Sorting by locationnum is meaningless.
2501           # We have to sort on _something_ or the order may change 
2502           # randomly from one invoice to the next, which will confuse
2503           # people.
2504           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2505                                       $locationnum;
2506           $section->{'location'} = {
2507             label_prefix => &{ $escape }($location->label_prefix),
2508             map { $_ => &{ $escape }($location->get($_)) }
2509               $location->fields
2510           };
2511         } else {
2512           $section->{'category'} = $sectionname;
2513           $section->{'description'} = &{ $escape }($sectionname);
2514           if ( _pkg_category($sectionname) ) {
2515             $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2516             if ( _pkg_category($sectionname)->condense ) {
2517               $section = { %$section, $self->_condense_section($opt{format}) };
2518             }
2519           }
2520         }
2521         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2522           # then it's a tax-only section
2523           $section->{'summarized'} = 'Y';
2524           $section->{'tax_section'} = 'Y';
2525         }
2526         push @these, $section;
2527       } # foreach $sectionname
2528     } #foreach $locationnum
2529     push @these, @extra_sections if $post_total == 0;
2530     # need an alpha sort for location sections, because postal codes can 
2531     # be non-numeric
2532     $sections[ $post_total ] = [ sort {
2533       $opt{'by_location'} ? 
2534         ($a->{sort_weight} cmp $b->{sort_weight}) :
2535         ($a->{sort_weight} <=> $b->{sort_weight})
2536       } @these ];
2537   } #foreach $post_total
2538
2539   return @sections; # early, late
2540 }
2541
2542 #helper subs for above
2543
2544 sub cust_pkg_hash {
2545   my $self = shift;
2546   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2547 }
2548
2549 sub _pkg_category {
2550   my $categoryname = shift;
2551   $pkg_category_cache{$categoryname} ||=
2552     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2553 }
2554
2555 my %condensed_format = (
2556   'label' => [ qw( Description Qty Amount ) ],
2557   'fields' => [
2558                 sub { shift->{description} },
2559                 sub { shift->{quantity} },
2560                 sub { my($href, %opt) = @_;
2561                       ($opt{dollar} || ''). $href->{amount};
2562                     },
2563               ],
2564   'align'  => [ qw( l r r ) ],
2565   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2566   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2567 );
2568
2569 sub _condense_section {
2570   my ( $self, $format ) = ( shift, shift );
2571   ( 'condensed' => 1,
2572     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2573       qw( description_generator
2574           header_generator
2575           total_generator
2576           total_line_generator
2577         )
2578   );
2579 }
2580
2581 sub _condensed_generator_defaults {
2582   my ( $self, $format ) = ( shift, shift );
2583   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2584 }
2585
2586 my %html_align = (
2587   'c' => 'center',
2588   'l' => 'left',
2589   'r' => 'right',
2590 );
2591
2592 sub _condensed_header_generator {
2593   my ( $self, $format ) = ( shift, shift );
2594
2595   my ( $f, $prefix, $suffix, $separator, $column ) =
2596     _condensed_generator_defaults($format);
2597
2598   if ($format eq 'latex') {
2599     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2600     $suffix = "\\\\\n\\hline";
2601     $separator = "&\n";
2602     $column =
2603       sub { my ($d,$a,$s,$w) = @_;
2604             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2605           };
2606   } elsif ( $format eq 'html' ) {
2607     $prefix = '<th></th>';
2608     $suffix = '';
2609     $separator = '';
2610     $column =
2611       sub { my ($d,$a,$s,$w) = @_;
2612             return qq!<th align="$html_align{$a}">$d</th>!;
2613       };
2614   }
2615
2616   sub {
2617     my @args = @_;
2618     my @result = ();
2619
2620     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2621       push @result,
2622         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2623     }
2624
2625     $prefix. join($separator, @result). $suffix;
2626   };
2627
2628 }
2629
2630 sub _condensed_description_generator {
2631   my ( $self, $format ) = ( shift, shift );
2632
2633   my ( $f, $prefix, $suffix, $separator, $column ) =
2634     _condensed_generator_defaults($format);
2635
2636   my $money_char = '$';
2637   if ($format eq 'latex') {
2638     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2639     $suffix = '\\\\';
2640     $separator = " & \n";
2641     $column =
2642       sub { my ($d,$a,$s,$w) = @_;
2643             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2644           };
2645     $money_char = '\\dollar';
2646   }elsif ( $format eq 'html' ) {
2647     $prefix = '"><td align="center"></td>';
2648     $suffix = '';
2649     $separator = '';
2650     $column =
2651       sub { my ($d,$a,$s,$w) = @_;
2652             return qq!<td align="$html_align{$a}">$d</td>!;
2653       };
2654     #$money_char = $conf->config('money_char') || '$';
2655     $money_char = '';  # this is madness
2656   }
2657
2658   sub {
2659     #my @args = @_;
2660     my $href = shift;
2661     my @result = ();
2662
2663     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2664       my $dollar = '';
2665       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2666       push @result,
2667         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2668                     map { $f->{$_}->[$i] } qw(align span width)
2669                   );
2670     }
2671
2672     $prefix. join( $separator, @result ). $suffix;
2673   };
2674
2675 }
2676
2677 sub _condensed_total_generator {
2678   my ( $self, $format ) = ( shift, shift );
2679
2680   my ( $f, $prefix, $suffix, $separator, $column ) =
2681     _condensed_generator_defaults($format);
2682   my $style = '';
2683
2684   if ($format eq 'latex') {
2685     $prefix = "& ";
2686     $suffix = "\\\\\n";
2687     $separator = " & \n";
2688     $column =
2689       sub { my ($d,$a,$s,$w) = @_;
2690             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2691           };
2692   }elsif ( $format eq 'html' ) {
2693     $prefix = '';
2694     $suffix = '';
2695     $separator = '';
2696     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2697     $column =
2698       sub { my ($d,$a,$s,$w) = @_;
2699             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2700       };
2701   }
2702
2703
2704   sub {
2705     my @args = @_;
2706     my @result = ();
2707
2708     #  my $r = &{$f->{fields}->[$i]}(@args);
2709     #  $r .= ' Total' unless $i;
2710
2711     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2712       push @result,
2713         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2714                     map { $f->{$_}->[$i] } qw(align span width)
2715                   );
2716     }
2717
2718     $prefix. join( $separator, @result ). $suffix;
2719   };
2720
2721 }
2722
2723 =item total_line_generator FORMAT
2724
2725 Returns a coderef used for generation of invoice total line items for this
2726 usage_class.  FORMAT is either html or latex
2727
2728 =cut
2729
2730 # should not be used: will have issues with hash element names (description vs
2731 # total_item and amount vs total_amount -- another array of functions?
2732
2733 sub _condensed_total_line_generator {
2734   my ( $self, $format ) = ( shift, shift );
2735
2736   my ( $f, $prefix, $suffix, $separator, $column ) =
2737     _condensed_generator_defaults($format);
2738   my $style = '';
2739
2740   if ($format eq 'latex') {
2741     $prefix = "& ";
2742     $suffix = "\\\\\n";
2743     $separator = " & \n";
2744     $column =
2745       sub { my ($d,$a,$s,$w) = @_;
2746             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2747           };
2748   }elsif ( $format eq 'html' ) {
2749     $prefix = '';
2750     $suffix = '';
2751     $separator = '';
2752     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2753     $column =
2754       sub { my ($d,$a,$s,$w) = @_;
2755             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2756       };
2757   }
2758
2759
2760   sub {
2761     my @args = @_;
2762     my @result = ();
2763
2764     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2765       push @result,
2766         &{$column}( &{$f->{fields}->[$i]}(@args),
2767                     map { $f->{$_}->[$i] } qw(align span width)
2768                   );
2769     }
2770
2771     $prefix. join( $separator, @result ). $suffix;
2772   };
2773
2774 }
2775
2776 =item _items_pkg [ OPTIONS ]
2777
2778 Return line item hashes for each package item on this invoice. Nearly 
2779 equivalent to 
2780
2781 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2782
2783 OPTIONS are passed through to _items_cust_bill_pkg, and should include
2784 'format' and 'escape_function' at minimum.
2785
2786 To produce items for a specific invoice section, OPTIONS should include
2787 'section', a hashref containing 'category' and/or 'locationnum' keys.
2788
2789 'section' may also contain a key named 'condensed'. If this is present
2790 and has a true value, _items_pkg will try to merge identical items into items
2791 with 'quantity' equal to the number of items (not the sum of their separate
2792 quantities, for some reason).
2793
2794 =cut
2795
2796 sub _items_nontax {
2797   my $self = shift;
2798   # The order of these is important.  Bundled line items will be merged into
2799   # the most recent non-hidden item, so it needs to be the one with:
2800   # - the same pkgnum
2801   # - the same start date
2802   # - no pkgpart_override
2803   #
2804   # So: sort by pkgnum,
2805   # then by sdate
2806   # then sort the base line item before any overrides
2807   # then sort hidden before non-hidden add-ons
2808   # then sort by override pkgpart (for consistency)
2809   sort { $a->pkgnum <=> $b->pkgnum        or
2810          $a->sdate  <=> $b->sdate         or
2811          ($a->pkgpart_override ? 0 : -1)  or
2812          ($b->pkgpart_override ? 0 : 1)   or
2813          $b->hidden cmp $a->hidden        or
2814          $a->pkgpart_override <=> $b->pkgpart_override
2815        }
2816   # and of course exclude taxes and fees
2817   grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2818 }
2819
2820 sub _items_fee {
2821   my $self = shift;
2822   my %options = @_;
2823   my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2824   my $escape_function = $options{escape_function};
2825
2826   my @items;
2827   foreach my $cust_bill_pkg (@cust_bill_pkg) {
2828     # cache this, so we don't look it up again in every section
2829     my $part_fee = $cust_bill_pkg->get('part_fee')
2830        || $cust_bill_pkg->part_fee;
2831     $cust_bill_pkg->set('part_fee', $part_fee);
2832     if (!$part_fee) {
2833       #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2834       warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2835       next;
2836     }
2837     if ( exists($options{section}) and exists($options{section}{category}) )
2838     {
2839       my $categoryname = $options{section}{category};
2840       # then filter for items that have that section
2841       if ( $part_fee->categoryname ne $categoryname ) {
2842         warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
2843         next;
2844       }
2845     } # otherwise include them all in the main section
2846     # XXX what to do when sectioning by location?
2847     
2848     my @ext_desc;
2849     my %base_invnums; # invnum => invoice date
2850     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
2851       if ($_->base_invnum) {
2852         my $base_bill = FS::cust_bill->by_key($_->base_invnum);
2853         my $base_date = $self->time2str_local('short', $base_bill->_date)
2854           if $base_bill;
2855         $base_invnums{$_->base_invnum} = $base_date || '';
2856       }
2857     }
2858     foreach (sort keys(%base_invnums)) {
2859       next if $_ == $self->invnum;
2860       # per convention, we must escape ext_description lines
2861       push @ext_desc,
2862         &{$escape_function}(
2863           $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
2864         );
2865     }
2866     my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
2867     # but not escape the base description line
2868
2869     push @items,
2870       { feepart     => $cust_bill_pkg->feepart,
2871         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2872         description => $desc,
2873         ext_description => \@ext_desc
2874         # sdate/edate?
2875       };
2876   }
2877   @items;
2878 }
2879
2880 sub _items_pkg {
2881   my $self = shift;
2882   my %options = @_;
2883
2884   warn "$me _items_pkg searching for all package line items\n"
2885     if $DEBUG > 1;
2886
2887   my @cust_bill_pkg = $self->_items_nontax;
2888
2889   warn "$me _items_pkg filtering line items\n"
2890     if $DEBUG > 1;
2891   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2892
2893   if ($options{section} && $options{section}->{condensed}) {
2894
2895     warn "$me _items_pkg condensing section\n"
2896       if $DEBUG > 1;
2897
2898     my %itemshash = ();
2899     local $Storable::canonical = 1;
2900     foreach ( @items ) {
2901       my $item = { %$_ };
2902       delete $item->{ref};
2903       delete $item->{ext_description};
2904       my $key = freeze($item);
2905       $itemshash{$key} ||= 0;
2906       $itemshash{$key} ++; # += $item->{quantity};
2907     }
2908     @items = sort { $a->{description} cmp $b->{description} }
2909              map { my $i = thaw($_);
2910                    $i->{quantity} = $itemshash{$_};
2911                    $i->{amount} =
2912                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2913                    $i;
2914                  }
2915              keys %itemshash;
2916   }
2917
2918   warn "$me _items_pkg returning ". scalar(@items). " items\n"
2919     if $DEBUG > 1;
2920
2921   @items;
2922 }
2923
2924 sub _taxsort {
2925   return 0 unless $a->itemdesc cmp $b->itemdesc;
2926   return -1 if $b->itemdesc eq 'Tax';
2927   return 1 if $a->itemdesc eq 'Tax';
2928   return -1 if $b->itemdesc eq 'Other surcharges';
2929   return 1 if $a->itemdesc eq 'Other surcharges';
2930   $a->itemdesc cmp $b->itemdesc;
2931 }
2932
2933 sub _items_tax {
2934   my $self = shift;
2935   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
2936     $self->cust_bill_pkg;
2937   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2938
2939   if ( $self->conf->exists('always_show_tax') ) {
2940     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2941     if (0 == grep { $_->{description} eq $itemdesc } @items) {
2942       push @items,
2943         { 'description' => $itemdesc,
2944           'amount'      => 0.00 };
2945     }
2946   }
2947   @items;
2948 }
2949
2950 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2951
2952 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2953 list of hashrefs describing the line items they generate on the invoice.
2954
2955 OPTIONS may include:
2956
2957 format: the invoice format.
2958
2959 escape_function: the function used to escape strings.
2960
2961 DEPRECATED? (expensive, mostly unused?)
2962 format_function: the function used to format CDRs.
2963
2964 section: a hashref containing 'category' and/or 'locationnum'; if this 
2965 is present, only returns line items that belong to that category and/or
2966 location (whichever is defined).
2967
2968 multisection: a flag indicating that this is a multisection invoice,
2969 which does something complicated.
2970
2971 preref_callback: coderef run for each line item, code should return HTML to be
2972 displayed before that line item (quotations only)
2973
2974 Returns a list of hashrefs, each of which may contain:
2975
2976 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
2977 ext_description, which is an arrayref of detail lines to show below 
2978 the package line.
2979
2980 =cut
2981
2982 sub _items_cust_bill_pkg {
2983   my $self = shift;
2984   my $conf = $self->conf;
2985   my $cust_bill_pkgs = shift;
2986   my %opt = @_;
2987
2988   my $format = $opt{format} || '';
2989   my $escape_function = $opt{escape_function} || sub { shift };
2990   my $format_function = $opt{format_function} || '';
2991   my $no_usage = $opt{no_usage} || '';
2992   my $unsquelched = $opt{unsquelched} || ''; #unused
2993   my ($section, $locationnum, $category);
2994   if ( $opt{section} ) {
2995     $category = $opt{section}->{category};
2996     $locationnum = $opt{section}->{locationnum};
2997   }
2998   my $summary_page = $opt{summary_page} || ''; #unused
2999   my $multisection = defined($category) || defined($locationnum);
3000   my $discount_show_always = 0;
3001
3002   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
3003
3004   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
3005
3006   # for location labels: use default location on the invoice date
3007   my $default_locationnum;
3008   if ( $self->custnum ) {
3009     my $h_cust_main;
3010     my @h_search = FS::h_cust_main->sql_h_search($self->_date);
3011     $h_cust_main = qsearchs({
3012         'table'     => 'h_cust_main',
3013         'hashref'   => { custnum => $self->custnum },
3014         'extra_sql' => $h_search[1],
3015         'addl_from' => $h_search[3],
3016     }) || $cust_main;
3017     $default_locationnum = $h_cust_main->ship_locationnum;
3018   } elsif ( $self->prospectnum ) {
3019     my $cust_location = qsearchs('cust_location',
3020       { prospectnum => $self->prospectnum,
3021         disabled => '' });
3022     $default_locationnum = $cust_location->locationnum if $cust_location;
3023   }
3024
3025   my @b = (); # accumulator for the line item hashes that we'll return
3026   my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
3027             # the 'current' line item hashes for setup, recur, usage, discount
3028   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
3029   {
3030     # if the current line item is waiting to go out, and the one we're about
3031     # to start is not bundled, then push out the current one and start a new
3032     # one.
3033     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3034       if ( $_ && !$cust_bill_pkg->hidden ) {
3035         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
3036         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3037         if (exists($_->{unit_amount})) {
3038           $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3039         }
3040         push @b, { %$_ }
3041           if $_->{amount} != 0
3042           || $discount_show_always
3043           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3044           || (   $_->{_is_setup} && $_->{setup_show_zero} )
3045         ;
3046         $_ = undef;
3047       }
3048     }
3049
3050     if ( $locationnum ) {
3051       # this is a location section; skip packages that aren't at this
3052       # service location.
3053       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
3054       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
3055               != $locationnum;
3056     }
3057
3058     # Consider display records for this item to determine if it belongs
3059     # in this section.  Note that if there are no display records, there
3060     # will be a default pseudo-record that includes all charge types 
3061     # and has no section name.
3062     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
3063                                   ? $cust_bill_pkg->cust_bill_pkg_display
3064                                   : ( $cust_bill_pkg );
3065
3066     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
3067          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
3068       if $DEBUG > 1;
3069
3070     if ( defined($category) ) {
3071       # then this is a package category section; process all display records
3072       # that belong to this section.
3073       @cust_bill_pkg_display = grep { $_->section eq $category }
3074                                 @cust_bill_pkg_display;
3075     } else {
3076       # otherwise, process all display records that aren't usage summaries
3077       # (I don't think there should be usage summaries if you aren't using 
3078       # category sections, but this is the historical behavior)
3079       @cust_bill_pkg_display = grep { !$_->summary }
3080                                 @cust_bill_pkg_display;
3081     }
3082
3083     my $classname = ''; # package class name, will fill in later
3084
3085     foreach my $display (@cust_bill_pkg_display) {
3086
3087       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
3088            $display->billpkgdisplaynum. "\n"
3089         if $DEBUG > 1;
3090
3091       my $type = $display->type;
3092
3093       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
3094       $desc = substr($desc, 0, $maxlength). '...'
3095         if $format eq 'latex' && length($desc) > $maxlength;
3096
3097       my %details_opt = ( 'format'          => $format,
3098                           'escape_function' => $escape_function,
3099                           'format_function' => $format_function,
3100                           'no_usage'        => $opt{'no_usage'},
3101                         );
3102
3103       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
3104         # XXX this should be pulled out into quotation_pkg
3105
3106         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
3107           if $DEBUG > 1;
3108         # quotation_pkgs are never fees, so don't worry about the case where
3109         # part_pkg is undefined
3110
3111         # and I guess they're never bundled either?
3112         if ( $cust_bill_pkg->setup != 0 ) {
3113           my $description = $desc;
3114           $description .= ' Setup'
3115             if $cust_bill_pkg->recur != 0
3116             || $discount_show_always
3117             || $cust_bill_pkg->recur_show_zero;
3118           #push @b, {
3119           # keep it consistent, please
3120           $s = {
3121             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3122             'description' => $description,
3123             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3124             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
3125             'quantity'    => $cust_bill_pkg->quantity,
3126             'preref_html' => ( $opt{preref_callback}
3127                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3128                                  : ''
3129                              ),
3130           };
3131         }
3132         if ( $cust_bill_pkg->recur != 0 ) {
3133           #push @b, {
3134           $r = {
3135             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3136             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
3137             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3138             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
3139             'quantity'    => $cust_bill_pkg->quantity,
3140            'preref_html'  => ( $opt{preref_callback}
3141                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3142                                  : ''
3143                              ),
3144           };
3145         }
3146
3147       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
3148         # a "normal" package line item (not a quotation, not a fee, not a tax)
3149
3150         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
3151           if $DEBUG > 1;
3152  
3153         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3154         my $part_pkg = $cust_pkg->part_pkg;
3155
3156         # which pkgpart to show for display purposes?
3157         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
3158
3159         # start/end dates for invoice formats that do nonstandard 
3160         # things with them
3161         my %item_dates = ();
3162         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
3163           unless $part_pkg->option('disable_line_item_date_ranges',1);
3164
3165         # not normally used, but pass this to the template anyway
3166         $classname = $part_pkg->classname;
3167
3168         if (    (!$type || $type eq 'S')
3169              && (    $cust_bill_pkg->setup != 0
3170                   || $cust_bill_pkg->setup_show_zero
3171                 )
3172            )
3173          {
3174
3175           warn "$me _items_cust_bill_pkg adding setup\n"
3176             if $DEBUG > 1;
3177
3178           my $description = $desc;
3179           $description .= ' Setup'
3180             if $cust_bill_pkg->recur != 0
3181             || $discount_show_always
3182             || $cust_bill_pkg->recur_show_zero;
3183
3184           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3185                                                               $self->agentnum )
3186             if $part_pkg->is_prepaid #for prepaid, "display the validity period
3187                                      # triggered by the recurring charge freq
3188                                      # (RT#26274)
3189             && $cust_bill_pkg->recur == 0
3190             && ! $cust_bill_pkg->recur_show_zero;
3191
3192           my @d = ();
3193           my $svc_label;
3194
3195           # always pass the svc_label through to the template, even if 
3196           # not displaying it as an ext_description
3197           my @svc_labels = map &{$escape_function}($_),
3198                       $cust_pkg->h_labels_short($self->_date, undef, 'I');
3199
3200           $svc_label = $svc_labels[0];
3201
3202           unless ( $cust_pkg->part_pkg->hide_svc_detail
3203                 || $cust_bill_pkg->hidden )
3204           {
3205
3206             push @d, @svc_labels
3207               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3208             # show the location label if it's not the customer's default
3209             # location, and we're not grouping items by location already
3210             if ( $cust_pkg->locationnum != $default_locationnum
3211                   and !defined($locationnum) ) {
3212               my $loc = $cust_pkg->location_label;
3213               $loc = substr($loc, 0, $maxlength). '...'
3214                 if $format eq 'latex' && length($loc) > $maxlength;
3215               push @d, &{$escape_function}($loc);
3216             }
3217
3218           } #unless hiding service details
3219
3220           push @d, $cust_bill_pkg->details(%details_opt)
3221             if $cust_bill_pkg->recur == 0;
3222
3223           if ( $cust_bill_pkg->hidden ) {
3224             $s->{amount}      += $cust_bill_pkg->setup;
3225             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3226             push @{ $s->{ext_description} }, @d;
3227           } else {
3228             $s = {
3229               _is_setup       => 1,
3230               description     => $description,
3231               pkgpart         => $pkgpart,
3232               pkgnum          => $cust_bill_pkg->pkgnum,
3233               amount          => $cust_bill_pkg->setup,
3234               setup_show_zero => $cust_bill_pkg->setup_show_zero,
3235               unit_amount     => $cust_bill_pkg->unitsetup,
3236               quantity        => $cust_bill_pkg->quantity,
3237               ext_description => \@d,
3238               svc_label       => ($svc_label || ''),
3239               locationnum     => $cust_pkg->locationnum, # sure, why not?
3240             };
3241           };
3242
3243         }
3244
3245         if (    ( !$type || $type eq 'R' || $type eq 'U' )
3246              && (
3247                      $cust_bill_pkg->recur != 0
3248                   || $cust_bill_pkg->setup == 0
3249                   || $discount_show_always
3250                   || $cust_bill_pkg->recur_show_zero
3251                 )
3252            )
3253         {
3254
3255           warn "$me _items_cust_bill_pkg adding recur/usage\n"
3256             if $DEBUG > 1;
3257
3258           my $is_summary = $display->summary;
3259           my $description = $desc;
3260           if ( $type eq 'U' and defined($r) ) {
3261             # don't just show the same description as the recur line
3262             $description = $self->mt('Usage charges');
3263           }
3264
3265           my $part_pkg = $cust_pkg->part_pkg;
3266
3267           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3268                                                               $self->agentnum );
3269
3270           my @d = ();
3271           my @seconds = (); # for display of usage info
3272           my $svc_label = '';
3273
3274           #at least until cust_bill_pkg has "past" ranges in addition to
3275           #the "future" sdate/edate ones... see #3032
3276           my @dates = ( $self->_date );
3277           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3278           push @dates, $prev->sdate if $prev;
3279           push @dates, undef if !$prev;
3280
3281           my @svc_labels = map &{$escape_function}($_),
3282                       $cust_pkg->h_labels_short(@dates, 'I');
3283           $svc_label = $svc_labels[0];
3284
3285           # show service labels, unless...
3286                     # the package is set not to display them
3287           unless ( $part_pkg->hide_svc_detail
3288                     # or this is a tax-like line item
3289                 || $cust_bill_pkg->itemdesc
3290                     # or this is a hidden (bundled) line item
3291                 || $cust_bill_pkg->hidden
3292                     # or this is a usage summary line
3293                 || $is_summary && $type && $type eq 'U'
3294                     # or this is a usage line and there's a recurring line
3295                     # for the package in the same section (which will 
3296                     # have service labels already)
3297                 || ($type eq 'U' and defined($r))
3298               )
3299           {
3300
3301             warn "$me _items_cust_bill_pkg adding service details\n"
3302               if $DEBUG > 1;
3303
3304             push @d, @svc_labels
3305               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3306             warn "$me _items_cust_bill_pkg done adding service details\n"
3307               if $DEBUG > 1;
3308
3309             # show the location label if it's not the customer's default
3310             # location, and we're not grouping items by location already
3311             if ( $cust_pkg->locationnum != $default_locationnum
3312                   and !defined($locationnum) ) {
3313               my $loc = $cust_pkg->location_label;
3314               $loc = substr($loc, 0, $maxlength). '...'
3315                 if $format eq 'latex' && length($loc) > $maxlength;
3316               push @d, &{$escape_function}($loc);
3317             }
3318
3319             # Display of seconds_since_sqlradacct:
3320             # On the invoice, when processing @detail_items, look for a field
3321             # named 'seconds'.  This will contain total seconds for each 
3322             # service, in the same order as @ext_description.  For services 
3323             # that don't support this it will show undef.
3324             if ( $conf->exists('svc_acct-usage_seconds') 
3325                  and ! $cust_bill_pkg->pkgpart_override ) {
3326               foreach my $cust_svc ( 
3327                   $cust_pkg->h_cust_svc(@dates, 'I') 
3328                 ) {
3329
3330                 # eval because not having any part_export_usage exports 
3331                 # is a fatal error, last_bill/_date because that's how 
3332                 # sqlradius_hour billing does it
3333                 my $sec = eval {
3334                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
3335                 };
3336                 push @seconds, $sec;
3337               }
3338             } #if svc_acct-usage_seconds
3339
3340           } # if we are showing service labels
3341
3342           unless ( $is_summary ) {
3343             warn "$me _items_cust_bill_pkg adding details\n"
3344               if $DEBUG > 1;
3345
3346             #instead of omitting details entirely in this case (unwanted side
3347             # effects), just omit CDRs
3348             $details_opt{'no_usage'} = 1
3349               if $type && $type eq 'R';
3350
3351             push @d, $cust_bill_pkg->details(%details_opt);
3352           }
3353
3354           warn "$me _items_cust_bill_pkg calculating amount\n"
3355             if $DEBUG > 1;
3356   
3357           my $amount = 0;
3358           if (!$type) {
3359             $amount = $cust_bill_pkg->recur;
3360           } elsif ($type eq 'R') {
3361             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3362           } elsif ($type eq 'U') {
3363             $amount = $cust_bill_pkg->usage;
3364           }
3365   
3366           if ( !$type || $type eq 'R' ) {
3367
3368             warn "$me _items_cust_bill_pkg adding recur\n"
3369               if $DEBUG > 1;
3370
3371             my $unit_amount =
3372               ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
3373                                                 : $amount;
3374
3375             if ( $cust_bill_pkg->hidden ) {
3376               $r->{amount}      += $amount;
3377               $r->{unit_amount} += $unit_amount;
3378               push @{ $r->{ext_description} }, @d;
3379             } else {
3380               $r = {
3381                 description     => $description,
3382                 pkgpart         => $pkgpart,
3383                 pkgnum          => $cust_bill_pkg->pkgnum,
3384                 amount          => $amount,
3385                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3386                 unit_amount     => $unit_amount,
3387                 quantity        => $cust_bill_pkg->quantity,
3388                 %item_dates,
3389                 ext_description => \@d,
3390                 svc_label       => ($svc_label || ''),
3391                 locationnum     => $cust_pkg->locationnum,
3392               };
3393               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3394             }
3395
3396           } else {  # $type eq 'U'
3397
3398             warn "$me _items_cust_bill_pkg adding usage\n"
3399               if $DEBUG > 1;
3400
3401             if ( $cust_bill_pkg->hidden and defined($u) ) {
3402               # if this is a hidden package and there's already a usage
3403               # line for the bundle, add this package's total amount and
3404               # usage details to it
3405               $u->{amount}      += $amount;
3406               push @{ $u->{ext_description} }, @d;
3407             } elsif ( $amount ) {
3408               # create a new usage line
3409               $u = {
3410                 description     => $description,
3411                 pkgpart         => $pkgpart,
3412                 pkgnum          => $cust_bill_pkg->pkgnum,
3413                 amount          => $amount,
3414                 usage_item      => 1,
3415                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3416                 %item_dates,
3417                 ext_description => \@d,
3418                 locationnum     => $cust_pkg->locationnum,
3419               };
3420             } # else this has no usage, so don't create a usage section
3421           }
3422
3423         } # recurring or usage with recurring charge
3424
3425       } else { # taxes and fees
3426
3427         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3428           if $DEBUG > 1;
3429
3430         # items of this kind should normally not have sdate/edate.
3431         push @b, {
3432           'description' => $desc,
3433           'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
3434                                            + $cust_bill_pkg->recur)
3435         };
3436
3437       } # if quotation / package line item / other line item
3438
3439       # decide whether to show active discounts here
3440       if (
3441           # case 1: we are showing a single line for the package
3442           ( !$type )
3443           # case 2: we are showing a setup line for a package that has
3444           # no base recurring fee
3445           or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3446           # case 3: we are showing a recur line for a package that has 
3447           # a base recurring fee
3448           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3449       ) {
3450
3451         my $item_discount = $cust_bill_pkg->_item_discount;
3452         if ( $item_discount ) {
3453           # $item_discount->{amount} is negative
3454
3455           if ( $d and $cust_bill_pkg->hidden ) {
3456             $d->{amount}      += $item_discount->{amount};
3457           } else {
3458             $d = $item_discount;
3459             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
3460           }
3461
3462           # update the active line (before the discount) to show the 
3463           # original price (whether this is a hidden line or not)
3464           #
3465           # quotation discounts keep track of setup and recur; invoice 
3466           # discounts currently don't
3467           if ( exists $item_discount->{setup_amount} ) {
3468
3469             $s->{amount} -= $item_discount->{setup_amount} if $s;
3470             $r->{amount} -= $item_discount->{recur_amount} if $r;
3471
3472           } else {
3473
3474             # $active_line is the line item hashref for the line that will
3475             # show the original price
3476             # (use the recur or single line for the package, unless we're 
3477             # showing a setup line for a package with no recurring fee)
3478             my $active_line = $r;
3479             if ( $type eq 'S' ) {
3480               $active_line = $s;
3481             }
3482             $active_line->{amount} -= $item_discount->{amount};
3483
3484           }
3485
3486         } # if there are any discounts
3487       } # if this is an appropriate place to show discounts
3488
3489     } # foreach $display
3490
3491     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
3492                                 && $conf->exists('discount-show-always'));
3493
3494   }
3495
3496   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3497     if ( $_  ) {
3498       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3499         if exists($_->{amount});
3500       $_->{amount}      =~ s/^\-0\.00$/0.00/;
3501       if (exists($_->{unit_amount})) {
3502         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3503       }
3504
3505       push @b, { %$_ }
3506         if $_->{amount} != 0
3507         || $discount_show_always
3508         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3509         || (   $_->{_is_setup} && $_->{setup_show_zero} )
3510     }
3511   }
3512
3513   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3514     if $DEBUG > 1;
3515
3516   @b;
3517
3518 }
3519
3520 =item _items_discounts_avail
3521
3522 Returns an array of line item hashrefs representing available term discounts
3523 for this invoice.  This makes the same assumptions that apply to term 
3524 discounts in general: that the package is billed monthly, at a flat rate, 
3525 with no usage charges.  A prorated first month will be handled, as will 
3526 a setup fee if the discount is allowed to apply to setup fees.
3527
3528 =cut
3529
3530 sub _items_discounts_avail {
3531   my $self = shift;
3532
3533   #maybe move this method from cust_bill when quotations support discount_plans 
3534   return () unless $self->can('discount_plans');
3535   my %plans = $self->discount_plans;
3536
3537   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3538   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3539
3540   map {
3541     my $months = $_;
3542     my $plan = $plans{$months};
3543
3544     my $term_total = sprintf('%.2f', $plan->discounted_total);
3545     my $percent = sprintf('%.0f', 
3546                           100 * (1 - $term_total / $plan->base_total) );
3547     my $permonth = sprintf('%.2f', $term_total / $months);
3548     my $detail = $self->mt('discount on item'). ' '.
3549                  join(', ', map { "#$_" } $plan->pkgnums)
3550       if $list_pkgnums;
3551
3552     # discounts for non-integer months don't work anyway
3553     $months = sprintf("%d", $months);
3554
3555     +{
3556       description => $self->mt('Save [_1]% by paying for [_2] months',
3557                                 $percent, $months),
3558       amount      => $self->mt('[_1] ([_2] per month)', 
3559                                 $term_total, $money_char.$permonth),
3560       ext_description => ($detail || ''),
3561     }
3562   } #map
3563   sort { $b <=> $a } keys %plans;
3564
3565 }
3566
3567 1;