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