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