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