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