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