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