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