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