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