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