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