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