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