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