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