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