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