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