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