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