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