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