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