remove cruft
[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 $multilocation = scalar($cust_main->cust_location); #too expensive?
898     my %options = ();
899     $options{'section'} = $section if $multisection;
900     $options{'format'} = $format;
901     $options{'escape_function'} = $escape_function;
902     $options{'no_usage'} = 1 unless $unsquelched;
903     $options{'unsquelched'} = $unsquelched;
904     $options{'summary_page'} = $summarypage;
905     $options{'skip_usage'} =
906       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
907     $options{'multilocation'} = $multilocation;
908     $options{'multisection'} = $multisection;
909
910     warn "$me   searching for line items\n"
911       if $DEBUG > 1;
912
913     foreach my $line_item ( $self->_items_pkg(%options) ) {
914
915       warn "$me     adding line item $line_item\n"
916         if $DEBUG > 1;
917
918       my $detail = {
919         ext_description => [],
920       };
921       $detail->{'ref'} = $line_item->{'pkgnum'};
922       $detail->{'quantity'} = $line_item->{'quantity'};
923       $detail->{'section'} = $section;
924       $detail->{'description'} = &$escape_function($line_item->{'description'});
925       if ( exists $line_item->{'ext_description'} ) {
926         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
927       }
928       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
929                               $line_item->{'amount'};
930       if ( exists $line_item->{'unit_amount'} ) {
931         $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
932                                    $line_item->{'unit_amount'};
933       }
934       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
935
936       $detail->{'sdate'} = $line_item->{'sdate'};
937       $detail->{'edate'} = $line_item->{'edate'};
938       $detail->{'seconds'} = $line_item->{'seconds'};
939   
940       push @detail_items, $detail;
941       push @buf, ( [ $detail->{'description'},
942                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
943                    ],
944                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
945                  );
946     }
947
948     if ( $section->{'description'} ) {
949       push @buf, ( ['','-----------'],
950                    [ $section->{'description'}. ' sub-total',
951                       $section->{'subtotal'} # already formatted this 
952                    ],
953                    [ '', '' ],
954                    [ '', '' ],
955                  );
956     }
957   
958   }
959
960   $invoice_data{current_less_finance} =
961     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
962
963   # create a major section for previous balance if we have major sections,
964   # or if previous_section is in summary form
965   if ( ( $multisection && $self->enable_previous )
966     || $conf->exists('previous_balance-summary_only') )
967   {
968     unshift @sections, $previous_section if $pr_total;
969   }
970
971   warn "$me adding taxes\n"
972     if $DEBUG > 1;
973
974   foreach my $tax ( $self->_items_tax ) {
975
976     $taxtotal += $tax->{'amount'};
977
978     my $description = &$escape_function( $tax->{'description'} );
979     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
980
981     if ( $multisection ) {
982
983       my $money = $old_latex ? '' : $money_char;
984       push @detail_items, {
985         ext_description => [],
986         ref          => '',
987         quantity     => '',
988         description  => $description,
989         amount       => $money. $amount,
990         product_code => '',
991         section      => $tax_section,
992       };
993
994     } else {
995
996       push @total_items, {
997         'total_item'   => $description,
998         'total_amount' => $other_money_char. $amount,
999       };
1000
1001     }
1002
1003     push @buf,[ $description,
1004                 $money_char. $amount,
1005               ];
1006
1007   }
1008   
1009   if ( $taxtotal ) {
1010     my $total = {};
1011     $total->{'total_item'} = $self->mt('Sub-total');
1012     $total->{'total_amount'} =
1013       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1014
1015     if ( $multisection ) {
1016       $tax_section->{'subtotal'} = $other_money_char.
1017                                    sprintf('%.2f', $taxtotal);
1018       $tax_section->{'pretotal'} = 'New charges sub-total '.
1019                                    $total->{'total_amount'};
1020       push @sections, $tax_section if $taxtotal;
1021     }else{
1022       unshift @total_items, $total;
1023     }
1024   }
1025   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1026
1027   push @buf,['','-----------'];
1028   push @buf,[$self->mt( 
1029               (!$self->enable_previous)
1030                ? 'Total Charges'
1031                : 'Total New Charges'
1032              ),
1033              $money_char. sprintf("%10.2f",$self->charged) ];
1034   push @buf,['',''];
1035
1036   # calculate total, possibly including total owed on previous
1037   # invoices
1038   {
1039     my $total = {};
1040     my $item = 'Total';
1041     $item = $conf->config('previous_balance-exclude_from_total')
1042          || 'Total New Charges'
1043       if $conf->exists('previous_balance-exclude_from_total');
1044     my $amount = $self->charged;
1045     if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1046       $amount += $pr_total;
1047     }
1048
1049     $total->{'total_item'} = &$embolden_function($self->mt($item));
1050     $total->{'total_amount'} =
1051       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
1052     if ( $multisection ) {
1053       if ( $adjust_section->{'sort_weight'} ) {
1054         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1055           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
1056       } else {
1057         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1058           $other_money_char.  sprintf('%.2f', $self->charged );
1059       } 
1060     }else{
1061       push @total_items, $total;
1062     }
1063     push @buf,['','-----------'];
1064     push @buf,[$item,
1065                $money_char.
1066                sprintf( '%10.2f', $amount )
1067               ];
1068     push @buf,['',''];
1069   }
1070
1071   # if we're showing previous invoices, also show previous
1072   # credits and payments 
1073   if ( $self->enable_previous 
1074         and $self->can('_items_credits')
1075         and $self->can('_items_payments') )
1076     {
1077     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1078   
1079     # credits
1080     my $credittotal = 0;
1081     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1082
1083       my $total;
1084       $total->{'total_item'} = &$escape_function($credit->{'description'});
1085       $credittotal += $credit->{'amount'};
1086       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1087       $adjusttotal += $credit->{'amount'};
1088       if ( $multisection ) {
1089         my $money = $old_latex ? '' : $money_char;
1090         push @detail_items, {
1091           ext_description => [],
1092           ref          => '',
1093           quantity     => '',
1094           description  => &$escape_function($credit->{'description'}),
1095           amount       => $money. $credit->{'amount'},
1096           product_code => '',
1097           section      => $adjust_section,
1098         };
1099       } else {
1100         push @total_items, $total;
1101       }
1102
1103     }
1104     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1105
1106     #credits (again)
1107     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1108       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1109     }
1110
1111     # payments
1112     my $paymenttotal = 0;
1113     foreach my $payment ( $self->_items_payments ) {
1114       my $total = {};
1115       $total->{'total_item'} = &$escape_function($payment->{'description'});
1116       $paymenttotal += $payment->{'amount'};
1117       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1118       $adjusttotal += $payment->{'amount'};
1119       if ( $multisection ) {
1120         my $money = $old_latex ? '' : $money_char;
1121         push @detail_items, {
1122           ext_description => [],
1123           ref          => '',
1124           quantity     => '',
1125           description  => &$escape_function($payment->{'description'}),
1126           amount       => $money. $payment->{'amount'},
1127           product_code => '',
1128           section      => $adjust_section,
1129         };
1130       }else{
1131         push @total_items, $total;
1132       }
1133       push @buf, [ $payment->{'description'},
1134                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
1135                  ];
1136     }
1137     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1138   
1139     if ( $multisection ) {
1140       $adjust_section->{'subtotal'} = $other_money_char.
1141                                       sprintf('%.2f', $adjusttotal);
1142       push @sections, $adjust_section
1143         unless $adjust_section->{sort_weight};
1144     }
1145
1146     # create Balance Due message
1147     { 
1148       my $total;
1149       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1150       $total->{'total_amount'} =
1151         &$embolden_function(
1152           $other_money_char. sprintf('%.2f', $summarypage 
1153                                                ? $self->charged +
1154                                                  $self->billing_balance
1155                                                : $self->owed + $pr_total
1156                                     )
1157         );
1158       if ( $multisection && !$adjust_section->{sort_weight} ) {
1159         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1160                                          $total->{'total_amount'};
1161       }else{
1162         push @total_items, $total;
1163       }
1164       push @buf,['','-----------'];
1165       push @buf,[$self->balance_due_msg, $money_char. 
1166         sprintf("%10.2f", $balance_due ) ];
1167     }
1168
1169     if ( $conf->exists('previous_balance-show_credit')
1170         and $cust_main->balance < 0 ) {
1171       my $credit_total = {
1172         'total_item'    => &$embolden_function($self->credit_balance_msg),
1173         'total_amount'  => &$embolden_function(
1174           $other_money_char. sprintf('%.2f', -$cust_main->balance)
1175         ),
1176       };
1177       if ( $multisection ) {
1178         $adjust_section->{'posttotal'} .= $newline_token .
1179           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1180       }
1181       else {
1182         push @total_items, $credit_total;
1183       }
1184       push @buf,['','-----------'];
1185       push @buf,[$self->credit_balance_msg, $money_char. 
1186         sprintf("%10.2f", -$cust_main->balance ) ];
1187     }
1188   }
1189
1190   if ( $multisection ) {
1191     if (    $conf->exists('svc_phone_sections')
1192          && $self->can('_items_svc_phone_sections')
1193        )
1194     {
1195       my $total;
1196       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1197       $total->{'total_amount'} =
1198         &$embolden_function(
1199           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1200         );
1201       my $last_section = pop @sections;
1202       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1203                                      $total->{'total_amount'};
1204       push @sections, $last_section;
1205     }
1206     push @sections, @$late_sections
1207       if $unsquelched;
1208   }
1209
1210   # make a discounts-available section, even without multisection
1211   if ( $conf->exists('discount-show_available') 
1212        and my @discounts_avail = $self->_items_discounts_avail ) {
1213     my $discount_section = {
1214       'description' => $self->mt('Discounts Available'),
1215       'subtotal'    => '',
1216       'no_subtotal' => 1,
1217     };
1218
1219     push @sections, $discount_section;
1220     push @detail_items, map { +{
1221         'ref'         => '', #should this be something else?
1222         'section'     => $discount_section,
1223         'description' => &$escape_function( $_->{description} ),
1224         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1225         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1226     } } @discounts_avail;
1227   }
1228
1229   # All sections and items are built; now fill in templates.
1230   my @includelist = ();
1231   push @includelist, 'summary' if $summarypage;
1232   foreach my $include ( @includelist ) {
1233
1234     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1235     my @inc_src;
1236
1237     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1238
1239       @inc_src = $conf->config($inc_file, $agentnum);
1240
1241     } else {
1242
1243       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1244
1245       my $convert_map = $convert_maps{$format}{$include};
1246
1247       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1248                        s/--\@\]/$delimiters{$format}[1]/g;
1249                        $_;
1250                      } 
1251                  &$convert_map( $conf->config($inc_file, $agentnum) );
1252
1253     }
1254
1255     my $inc_tt = new Text::Template (
1256       TYPE       => 'ARRAY',
1257       SOURCE     => [ map "$_\n", @inc_src ],
1258       DELIMITERS => $delimiters{$format},
1259     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1260
1261     unless ( $inc_tt->compile() ) {
1262       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1263       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1264       die $error;
1265     }
1266
1267     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1268
1269     $invoice_data{$include} =~ s/\n+$//
1270       if ($format eq 'latex');
1271   }
1272
1273   $invoice_lines = 0;
1274   my $wasfunc = 0;
1275   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1276     /invoice_lines\((\d*)\)/;
1277     $invoice_lines += $1 || scalar(@buf);
1278     $wasfunc=1;
1279   }
1280   die "no invoice_lines() functions in template?"
1281     if ( $format eq 'template' && !$wasfunc );
1282
1283   if ($format eq 'template') {
1284
1285     if ( $invoice_lines ) {
1286       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1287       $invoice_data{'total_pages'}++
1288         if scalar(@buf) % $invoice_lines;
1289     }
1290
1291     #setup subroutine for the template
1292     $invoice_data{invoice_lines} = sub {
1293       my $lines = shift || scalar(@buf);
1294       map { 
1295         scalar(@buf)
1296           ? shift @buf
1297           : [ '', '' ];
1298       }
1299       ( 1 .. $lines );
1300     };
1301
1302     my $lines;
1303     my @collect;
1304     while (@buf) {
1305       push @collect, split("\n",
1306         $text_template->fill_in( HASH => \%invoice_data )
1307       );
1308       $invoice_data{'page'}++;
1309     }
1310     map "$_\n", @collect;
1311
1312   } else { # this is where we actually create the invoice
1313
1314     warn "filling in template for invoice ". $self->invnum. "\n"
1315       if $DEBUG;
1316     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1317       if $DEBUG > 1;
1318
1319     $text_template->fill_in(HASH => \%invoice_data);
1320   }
1321 }
1322
1323 sub notice_name { '('.shift->table.')'; }
1324
1325 sub template_conf { 'invoice_'; }
1326
1327 # helper routine for generating date ranges
1328 sub _prior_month30s {
1329   my $self = shift;
1330   my @ranges = (
1331    [ 1,       2592000 ], # 0-30 days ago
1332    [ 2592000, 5184000 ], # 30-60 days ago
1333    [ 5184000, 7776000 ], # 60-90 days ago
1334    [ 7776000, 0       ], # 90+   days ago
1335   );
1336
1337   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1338           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1339       ] }
1340   @ranges;
1341 }
1342
1343 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1344
1345 Returns an postscript invoice, as a scalar.
1346
1347 Options can be passed as a hashref (recommended) or as a list of time, template
1348 and then any key/value pairs for any other options.
1349
1350 I<time> an optional value used to control the printing of overdue messages.  The
1351 default is now.  It isn't the date of the invoice; that's the `_date' field.
1352 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1353 L<Time::Local> and L<Date::Parse> for conversion functions.
1354
1355 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1356
1357 =cut
1358
1359 sub print_ps {
1360   my $self = shift;
1361
1362   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1363   my $ps = generate_ps($file);
1364   unlink($logofile);
1365   unlink($barcodefile) if $barcodefile;
1366
1367   $ps;
1368 }
1369
1370 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1371
1372 Returns an PDF invoice, as a scalar.
1373
1374 Options can be passed as a hashref (recommended) or as a list of time, template
1375 and then any key/value pairs for any other options.
1376
1377 I<time> an optional value used to control the printing of overdue messages.  The
1378 default is now.  It isn't the date of the invoice; that's the `_date' field.
1379 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1380 L<Time::Local> and L<Date::Parse> for conversion functions.
1381
1382 I<template>, if specified, is the name of a suffix for alternate invoices.
1383
1384 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1385
1386 =cut
1387
1388 sub print_pdf {
1389   my $self = shift;
1390
1391   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1392   my $pdf = generate_pdf($file);
1393   unlink($logofile);
1394   unlink($barcodefile) if $barcodefile;
1395
1396   $pdf;
1397 }
1398
1399 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1400
1401 Returns an HTML invoice, as a scalar.
1402
1403 I<time> an optional value used to control the printing of overdue messages.  The
1404 default is now.  It isn't the date of the invoice; that's the `_date' field.
1405 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1406 L<Time::Local> and L<Date::Parse> for conversion functions.
1407
1408 I<template>, if specified, is the name of a suffix for alternate invoices.
1409
1410 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1411
1412 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1413 when emailing the invoice as part of a multipart/related MIME email.
1414
1415 =cut
1416
1417 sub print_html {
1418   my $self = shift;
1419   my %params;
1420   if ( ref($_[0]) ) {
1421     %params = %{ shift() }; 
1422   }else{
1423     $params{'time'} = shift;
1424     $params{'template'} = shift;
1425     $params{'cid'} = shift;
1426   }
1427
1428   $params{'format'} = 'html';
1429   
1430   $self->print_generic( %params );
1431 }
1432
1433 # quick subroutine for print_latex
1434 #
1435 # There are ten characters that LaTeX treats as special characters, which
1436 # means that they do not simply typeset themselves: 
1437 #      # $ % & ~ _ ^ \ { }
1438 #
1439 # TeX ignores blanks following an escaped character; if you want a blank (as
1440 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1441
1442 sub _latex_escape {
1443   my $value = shift;
1444   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1445   $value =~ s/([<>])/\$$1\$/g;
1446   $value;
1447 }
1448
1449 sub _html_escape {
1450   my $value = shift;
1451   encode_entities($value);
1452   $value;
1453 }
1454
1455 sub _html_escape_nbsp {
1456   my $value = _html_escape(shift);
1457   $value =~ s/ +/&nbsp;/g;
1458   $value;
1459 }
1460
1461 #utility methods for print_*
1462
1463 sub _translate_old_latex_format {
1464   warn "_translate_old_latex_format called\n"
1465     if $DEBUG; 
1466
1467   my @template = ();
1468   while ( @_ ) {
1469     my $line = shift;
1470   
1471     if ( $line =~ /^%%Detail\s*$/ ) {
1472   
1473       push @template, q![@--!,
1474                       q!  foreach my $_tr_line (@detail_items) {!,
1475                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1476                       q!      $_tr_line->{'description'} .= !, 
1477                       q!        "\\tabularnewline\n~~".!,
1478                       q!        join( "\\tabularnewline\n~~",!,
1479                       q!          @{$_tr_line->{'ext_description'}}!,
1480                       q!        );!,
1481                       q!    }!;
1482
1483       while ( ( my $line_item_line = shift )
1484               !~ /^%%EndDetail\s*$/                            ) {
1485         $line_item_line =~ s/'/\\'/g;    # nice LTS
1486         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1487         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1488         push @template, "    \$OUT .= '$line_item_line';";
1489       }
1490
1491       push @template, '}',
1492                       '--@]';
1493       #' doh, gvim
1494     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1495
1496       push @template, '[@--',
1497                       '  foreach my $_tr_line (@total_items) {';
1498
1499       while ( ( my $total_item_line = shift )
1500               !~ /^%%EndTotalDetails\s*$/                      ) {
1501         $total_item_line =~ s/'/\\'/g;    # nice LTS
1502         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1503         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1504         push @template, "    \$OUT .= '$total_item_line';";
1505       }
1506
1507       push @template, '}',
1508                       '--@]';
1509
1510     } else {
1511       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1512       push @template, $line;  
1513     }
1514   
1515   }
1516
1517   if ($DEBUG) {
1518     warn "$_\n" foreach @template;
1519   }
1520
1521   (@template);
1522 }
1523
1524 sub terms {
1525   my $self = shift;
1526   my $conf = $self->conf;
1527
1528   #check for an invoice-specific override
1529   return $self->invoice_terms if $self->invoice_terms;
1530   
1531   #check for a customer- specific override
1532   my $cust_main = $self->cust_main;
1533   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1534
1535   #use configured default
1536   $conf->config('invoice_default_terms') || '';
1537 }
1538
1539 sub due_date {
1540   my $self = shift;
1541   my $duedate = '';
1542   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1543     $duedate = $self->_date() + ( $1 * 86400 );
1544   }
1545   $duedate;
1546 }
1547
1548 sub due_date2str {
1549   my $self = shift;
1550   $self->due_date ? time2str(shift, $self->due_date) : '';
1551 }
1552
1553 sub balance_due_msg {
1554   my $self = shift;
1555   my $msg = $self->mt('Balance Due');
1556   return $msg unless $self->terms;
1557   if ( $self->due_date ) {
1558     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1559       $self->due_date2str($date_format);
1560   } elsif ( $self->terms ) {
1561     $msg .= ' - '. $self->terms;
1562   }
1563   $msg;
1564 }
1565
1566 sub balance_due_date {
1567   my $self = shift;
1568   my $conf = $self->conf;
1569   my $duedate = '';
1570   if (    $conf->exists('invoice_default_terms') 
1571        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1572     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1573   }
1574   $duedate;
1575 }
1576
1577 sub credit_balance_msg { 
1578   my $self = shift;
1579   $self->mt('Credit Balance Remaining')
1580 }
1581
1582 =item _date_pretty
1583
1584 Returns a string with the date, for example: "3/20/2008"
1585
1586 =cut
1587
1588 sub _date_pretty {
1589   my $self = shift;
1590   time2str($date_format, $self->_date);
1591 }
1592
1593 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1594
1595 Generate section information for all items appearing on this invoice.
1596 This will only be called for multi-section invoices.
1597
1598 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
1599 related display records (L<FS::cust_bill_pkg_display>) and organize 
1600 them into two groups ("early" and "late" according to whether they come 
1601 before or after the total), then into sections.  A subtotal is calculated 
1602 for each section.
1603
1604 Section descriptions are returned in sort weight order.  Each consists 
1605 of a hash containing:
1606
1607 description: the package category name, escaped
1608 subtotal: the total charges in that section
1609 tax_section: a flag indicating that the section contains only tax charges
1610 summarized: same as tax_section, for some reason
1611 sort_weight: the package category's sort weight
1612
1613 If 'condense' is set on the display record, it also contains everything 
1614 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1615 coderefs to generate parts of the invoice.  This is not advised.
1616
1617 Arguments:
1618
1619 LATE: an arrayref to push the "late" section hashes onto.  The "early"
1620 group is simply returned from the method.
1621
1622 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1623 Turning this on has the following effects:
1624 - Ignores display items with the 'summary' flag.
1625 - Combines all items into the "early" group.
1626 - Creates sections for all non-disabled package categories, even if they 
1627 have no charges on this invoice, as well as a section with no name.
1628
1629 ESCAPE: an escape function to use for section titles.
1630
1631 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
1632 sorted list.  If there are any of these, section subtotals exclude 
1633 usage charges.
1634
1635 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
1636 passed through to C<_condense_section()>.
1637
1638 =cut
1639
1640 use vars qw(%pkg_category_cache);
1641 sub _items_sections {
1642   my $self = shift;
1643   my $late = shift;
1644   my $summarypage = shift;
1645   my $escape = shift;
1646   my $extra_sections = shift;
1647   my $format = shift;
1648
1649   my %subtotal = ();
1650   my %late_subtotal = ();
1651   my %not_tax = ();
1652
1653   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1654   {
1655
1656       my $usage = $cust_bill_pkg->usage;
1657
1658       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1659         next if ( $display->summary && $summarypage );
1660
1661         my $section = $display->section;
1662         my $type    = $display->type;
1663
1664         $not_tax{$section} = 1
1665           unless $cust_bill_pkg->pkgnum == 0;
1666
1667         if ( $display->post_total && !$summarypage ) {
1668           if (! $type || $type eq 'S') {
1669             $late_subtotal{$section} += $cust_bill_pkg->setup
1670               if $cust_bill_pkg->setup != 0
1671               || $cust_bill_pkg->setup_show_zero;
1672           }
1673
1674           if (! $type) {
1675             $late_subtotal{$section} += $cust_bill_pkg->recur
1676               if $cust_bill_pkg->recur != 0
1677               || $cust_bill_pkg->recur_show_zero;
1678           }
1679
1680           if ($type && $type eq 'R') {
1681             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1682               if $cust_bill_pkg->recur != 0
1683               || $cust_bill_pkg->recur_show_zero;
1684           }
1685           
1686           if ($type && $type eq 'U') {
1687             $late_subtotal{$section} += $usage
1688               unless scalar(@$extra_sections);
1689           }
1690
1691         } else {
1692
1693           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1694
1695           if (! $type || $type eq 'S') {
1696             $subtotal{$section} += $cust_bill_pkg->setup
1697               if $cust_bill_pkg->setup != 0
1698               || $cust_bill_pkg->setup_show_zero;
1699           }
1700
1701           if (! $type) {
1702             $subtotal{$section} += $cust_bill_pkg->recur
1703               if $cust_bill_pkg->recur != 0
1704               || $cust_bill_pkg->recur_show_zero;
1705           }
1706
1707           if ($type && $type eq 'R') {
1708             $subtotal{$section} += $cust_bill_pkg->recur - $usage
1709               if $cust_bill_pkg->recur != 0
1710               || $cust_bill_pkg->recur_show_zero;
1711           }
1712           
1713           if ($type && $type eq 'U') {
1714             $subtotal{$section} += $usage
1715               unless scalar(@$extra_sections);
1716           }
1717
1718         }
1719
1720       }
1721
1722   }
1723
1724   %pkg_category_cache = ();
1725
1726   push @$late, map { { 'description' => &{$escape}($_),
1727                        'subtotal'    => $late_subtotal{$_},
1728                        'post_total'  => 1,
1729                        'sort_weight' => ( _pkg_category($_)
1730                                             ? _pkg_category($_)->weight
1731                                             : 0
1732                                        ),
1733                        ((_pkg_category($_) && _pkg_category($_)->condense)
1734                                            ? $self->_condense_section($format)
1735                                            : ()
1736                        ),
1737                    } }
1738                  sort _sectionsort keys %late_subtotal;
1739
1740   my @sections;
1741   if ( $summarypage ) {
1742     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1743                 map { $_->categoryname } qsearch('pkg_category', {});
1744     push @sections, '' if exists($subtotal{''});
1745   } else {
1746     @sections = keys %subtotal;
1747   }
1748
1749   my @early = map { { 'description' => &{$escape}($_),
1750                       'subtotal'    => $subtotal{$_},
1751                       'summarized'  => $not_tax{$_} ? '' : 'Y',
1752                       'tax_section' => $not_tax{$_} ? '' : 'Y',
1753                       'sort_weight' => ( _pkg_category($_)
1754                                            ? _pkg_category($_)->weight
1755                                            : 0
1756                                        ),
1757                        ((_pkg_category($_) && _pkg_category($_)->condense)
1758                                            ? $self->_condense_section($format)
1759                                            : ()
1760                        ),
1761                     }
1762                   } @sections;
1763   push @early, @$extra_sections if $extra_sections;
1764
1765   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1766
1767 }
1768
1769 #helper subs for above
1770
1771 sub _sectionsort {
1772   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1773 }
1774
1775 sub _pkg_category {
1776   my $categoryname = shift;
1777   $pkg_category_cache{$categoryname} ||=
1778     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1779 }
1780
1781 my %condensed_format = (
1782   'label' => [ qw( Description Qty Amount ) ],
1783   'fields' => [
1784                 sub { shift->{description} },
1785                 sub { shift->{quantity} },
1786                 sub { my($href, %opt) = @_;
1787                       ($opt{dollar} || ''). $href->{amount};
1788                     },
1789               ],
1790   'align'  => [ qw( l r r ) ],
1791   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
1792   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
1793 );
1794
1795 sub _condense_section {
1796   my ( $self, $format ) = ( shift, shift );
1797   ( 'condensed' => 1,
1798     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1799       qw( description_generator
1800           header_generator
1801           total_generator
1802           total_line_generator
1803         )
1804   );
1805 }
1806
1807 sub _condensed_generator_defaults {
1808   my ( $self, $format ) = ( shift, shift );
1809   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1810 }
1811
1812 my %html_align = (
1813   'c' => 'center',
1814   'l' => 'left',
1815   'r' => 'right',
1816 );
1817
1818 sub _condensed_header_generator {
1819   my ( $self, $format ) = ( shift, shift );
1820
1821   my ( $f, $prefix, $suffix, $separator, $column ) =
1822     _condensed_generator_defaults($format);
1823
1824   if ($format eq 'latex') {
1825     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1826     $suffix = "\\\\\n\\hline";
1827     $separator = "&\n";
1828     $column =
1829       sub { my ($d,$a,$s,$w) = @_;
1830             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1831           };
1832   } elsif ( $format eq 'html' ) {
1833     $prefix = '<th></th>';
1834     $suffix = '';
1835     $separator = '';
1836     $column =
1837       sub { my ($d,$a,$s,$w) = @_;
1838             return qq!<th align="$html_align{$a}">$d</th>!;
1839       };
1840   }
1841
1842   sub {
1843     my @args = @_;
1844     my @result = ();
1845
1846     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
1847       push @result,
1848         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1849     }
1850
1851     $prefix. join($separator, @result). $suffix;
1852   };
1853
1854 }
1855
1856 sub _condensed_description_generator {
1857   my ( $self, $format ) = ( shift, shift );
1858
1859   my ( $f, $prefix, $suffix, $separator, $column ) =
1860     _condensed_generator_defaults($format);
1861
1862   my $money_char = '$';
1863   if ($format eq 'latex') {
1864     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1865     $suffix = '\\\\';
1866     $separator = " & \n";
1867     $column =
1868       sub { my ($d,$a,$s,$w) = @_;
1869             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1870           };
1871     $money_char = '\\dollar';
1872   }elsif ( $format eq 'html' ) {
1873     $prefix = '"><td align="center"></td>';
1874     $suffix = '';
1875     $separator = '';
1876     $column =
1877       sub { my ($d,$a,$s,$w) = @_;
1878             return qq!<td align="$html_align{$a}">$d</td>!;
1879       };
1880     #$money_char = $conf->config('money_char') || '$';
1881     $money_char = '';  # this is madness
1882   }
1883
1884   sub {
1885     #my @args = @_;
1886     my $href = shift;
1887     my @result = ();
1888
1889     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
1890       my $dollar = '';
1891       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1892       push @result,
1893         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1894                     map { $f->{$_}->[$i] } qw(align span width)
1895                   );
1896     }
1897
1898     $prefix. join( $separator, @result ). $suffix;
1899   };
1900
1901 }
1902
1903 sub _condensed_total_generator {
1904   my ( $self, $format ) = ( shift, shift );
1905
1906   my ( $f, $prefix, $suffix, $separator, $column ) =
1907     _condensed_generator_defaults($format);
1908   my $style = '';
1909
1910   if ($format eq 'latex') {
1911     $prefix = "& ";
1912     $suffix = "\\\\\n";
1913     $separator = " & \n";
1914     $column =
1915       sub { my ($d,$a,$s,$w) = @_;
1916             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1917           };
1918   }elsif ( $format eq 'html' ) {
1919     $prefix = '';
1920     $suffix = '';
1921     $separator = '';
1922     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1923     $column =
1924       sub { my ($d,$a,$s,$w) = @_;
1925             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1926       };
1927   }
1928
1929
1930   sub {
1931     my @args = @_;
1932     my @result = ();
1933
1934     #  my $r = &{$f->{fields}->[$i]}(@args);
1935     #  $r .= ' Total' unless $i;
1936
1937     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
1938       push @result,
1939         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1940                     map { $f->{$_}->[$i] } qw(align span width)
1941                   );
1942     }
1943
1944     $prefix. join( $separator, @result ). $suffix;
1945   };
1946
1947 }
1948
1949 =item total_line_generator FORMAT
1950
1951 Returns a coderef used for generation of invoice total line items for this
1952 usage_class.  FORMAT is either html or latex
1953
1954 =cut
1955
1956 # should not be used: will have issues with hash element names (description vs
1957 # total_item and amount vs total_amount -- another array of functions?
1958
1959 sub _condensed_total_line_generator {
1960   my ( $self, $format ) = ( shift, shift );
1961
1962   my ( $f, $prefix, $suffix, $separator, $column ) =
1963     _condensed_generator_defaults($format);
1964   my $style = '';
1965
1966   if ($format eq 'latex') {
1967     $prefix = "& ";
1968     $suffix = "\\\\\n";
1969     $separator = " & \n";
1970     $column =
1971       sub { my ($d,$a,$s,$w) = @_;
1972             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1973           };
1974   }elsif ( $format eq 'html' ) {
1975     $prefix = '';
1976     $suffix = '';
1977     $separator = '';
1978     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1979     $column =
1980       sub { my ($d,$a,$s,$w) = @_;
1981             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1982       };
1983   }
1984
1985
1986   sub {
1987     my @args = @_;
1988     my @result = ();
1989
1990     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
1991       push @result,
1992         &{$column}( &{$f->{fields}->[$i]}(@args),
1993                     map { $f->{$_}->[$i] } qw(align span width)
1994                   );
1995     }
1996
1997     $prefix. join( $separator, @result ). $suffix;
1998   };
1999
2000 }
2001
2002 #  sub _items { # seems to be unused
2003 #    my $self = shift;
2004 #  
2005 #    #my @display = scalar(@_)
2006 #    #              ? @_
2007 #    #              : qw( _items_previous _items_pkg );
2008 #    #              #: qw( _items_pkg );
2009 #    #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2010 #    my @display = qw( _items_previous _items_pkg );
2011 #  
2012 #    my @b = ();
2013 #    foreach my $display ( @display ) {
2014 #      push @b, $self->$display(@_);
2015 #    }
2016 #    @b;
2017 #  }
2018
2019 =item _items_pkg [ OPTIONS ]
2020
2021 Return line item hashes for each package item on this invoice. Nearly 
2022 equivalent to 
2023
2024 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2025
2026 The only OPTIONS accepted is 'section', which may point to a hashref 
2027 with a key named 'condensed', which may have a true value.  If it 
2028 does, this method tries to merge identical items into items with 
2029 'quantity' equal to the number of items (not the sum of their 
2030 separate quantities, for some reason).
2031
2032 =cut
2033
2034 sub _items_pkg {
2035   my $self = shift;
2036   my %options = @_;
2037
2038   warn "$me _items_pkg searching for all package line items\n"
2039     if $DEBUG > 1;
2040
2041   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2042
2043   warn "$me _items_pkg filtering line items\n"
2044     if $DEBUG > 1;
2045   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2046
2047   if ($options{section} && $options{section}->{condensed}) {
2048
2049     warn "$me _items_pkg condensing section\n"
2050       if $DEBUG > 1;
2051
2052     my %itemshash = ();
2053     local $Storable::canonical = 1;
2054     foreach ( @items ) {
2055       my $item = { %$_ };
2056       delete $item->{ref};
2057       delete $item->{ext_description};
2058       my $key = freeze($item);
2059       $itemshash{$key} ||= 0;
2060       $itemshash{$key} ++; # += $item->{quantity};
2061     }
2062     @items = sort { $a->{description} cmp $b->{description} }
2063              map { my $i = thaw($_);
2064                    $i->{quantity} = $itemshash{$_};
2065                    $i->{amount} =
2066                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2067                    $i;
2068                  }
2069              keys %itemshash;
2070   }
2071
2072   warn "$me _items_pkg returning ". scalar(@items). " items\n"
2073     if $DEBUG > 1;
2074
2075   @items;
2076 }
2077
2078 sub _taxsort {
2079   return 0 unless $a->itemdesc cmp $b->itemdesc;
2080   return -1 if $b->itemdesc eq 'Tax';
2081   return 1 if $a->itemdesc eq 'Tax';
2082   return -1 if $b->itemdesc eq 'Other surcharges';
2083   return 1 if $a->itemdesc eq 'Other surcharges';
2084   $a->itemdesc cmp $b->itemdesc;
2085 }
2086
2087 sub _items_tax {
2088   my $self = shift;
2089   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2090   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2091 }
2092
2093 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2094
2095 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2096 list of hashrefs describing the line items they generate on the invoice.
2097
2098 OPTIONS may include:
2099
2100 format: the invoice format.
2101
2102 escape_function: the function used to escape strings.
2103
2104 DEPRECATED? (expensive, mostly unused?)
2105 format_function: the function used to format CDRs.
2106
2107 section: a hashref containing 'description'; if this is present, 
2108 cust_bill_pkg_display records not belonging to this section are 
2109 ignored.
2110
2111 multisection: a flag indicating that this is a multisection invoice,
2112 which does something complicated.
2113
2114 multilocation: a flag to display the location label for the package.
2115
2116 Returns a list of hashrefs, each of which may contain:
2117
2118 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
2119 ext_description, which is an arrayref of detail lines to show below 
2120 the package line.
2121
2122 =cut
2123
2124 sub _items_cust_bill_pkg {
2125   my $self = shift;
2126   my $conf = $self->conf;
2127   my $cust_bill_pkgs = shift;
2128   my %opt = @_;
2129
2130   my $format = $opt{format} || '';
2131   my $escape_function = $opt{escape_function} || sub { shift };
2132   my $format_function = $opt{format_function} || '';
2133   my $no_usage = $opt{no_usage} || '';
2134   my $unsquelched = $opt{unsquelched} || ''; #unused
2135   my $section = $opt{section}->{description} if $opt{section};
2136   my $summary_page = $opt{summary_page} || ''; #unused
2137   my $multilocation = $opt{multilocation} || '';
2138   my $multisection = $opt{multisection} || '';
2139   my $discount_show_always = 0;
2140
2141   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2142
2143   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2144
2145   my @b = ();
2146   my ($s, $r, $u) = ( undef, undef, undef );
2147   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2148   {
2149
2150     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2151       if ( $_ && !$cust_bill_pkg->hidden ) {
2152         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2153         $_->{amount}      =~ s/^\-0\.00$/0.00/;
2154         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2155         push @b, { %$_ }
2156           if $_->{amount} != 0
2157           || $discount_show_always
2158           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2159           || (   $_->{_is_setup} && $_->{setup_show_zero} )
2160         ;
2161         $_ = undef;
2162       }
2163     }
2164
2165     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2166                                   ? $cust_bill_pkg->cust_bill_pkg_display
2167                                   : ( $cust_bill_pkg );
2168
2169     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2170          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2171       if $DEBUG > 1;
2172
2173     foreach my $display ( grep { defined($section)
2174                                  ? $_->section eq $section
2175                                  : 1
2176                                }
2177                           #grep { !$_->summary || !$summary_page } # bunk!
2178                           grep { !$_->summary || $multisection }
2179                           @cust_bill_pkg_display
2180                         )
2181     {
2182
2183       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2184            $display->billpkgdisplaynum. "\n"
2185         if $DEBUG > 1;
2186
2187       my $type = $display->type;
2188
2189       my $desc = $cust_bill_pkg->desc;
2190       $desc = substr($desc, 0, $maxlength). '...'
2191         if $format eq 'latex' && length($desc) > $maxlength;
2192
2193       my %details_opt = ( 'format'          => $format,
2194                           'escape_function' => $escape_function,
2195                           'format_function' => $format_function,
2196                           'no_usage'        => $opt{'no_usage'},
2197                         );
2198
2199       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2200
2201         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2202           if $DEBUG > 1;
2203
2204         if ( $cust_bill_pkg->setup != 0 ) {
2205           my $description = $desc;
2206           $description .= ' Setup'
2207             if $cust_bill_pkg->recur != 0
2208             || $discount_show_always
2209             || $cust_bill_pkg->recur_show_zero;
2210           push @b, {
2211             'description' => $description,
2212             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2213           };
2214         }
2215         if ( $cust_bill_pkg->recur != 0 ) {
2216           push @b, {
2217             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2218             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2219           };
2220         }
2221
2222       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2223
2224         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2225           if $DEBUG > 1;
2226  
2227         my $cust_pkg = $cust_bill_pkg->cust_pkg;
2228
2229         # start/end dates for invoice formats that do nonstandard 
2230         # things with them
2231         my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
2232
2233         if (    (!$type || $type eq 'S')
2234              && (    $cust_bill_pkg->setup != 0
2235                   || $cust_bill_pkg->setup_show_zero
2236                 )
2237            )
2238          {
2239
2240           warn "$me _items_cust_bill_pkg adding setup\n"
2241             if $DEBUG > 1;
2242
2243           my $description = $desc;
2244           $description .= ' Setup'
2245             if $cust_bill_pkg->recur != 0
2246             || $discount_show_always
2247             || $cust_bill_pkg->recur_show_zero;
2248
2249           my @d = ();
2250           unless ( $cust_pkg->part_pkg->hide_svc_detail
2251                 || $cust_bill_pkg->hidden )
2252           {
2253
2254             push @d, map &{$escape_function}($_),
2255                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
2256               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2257
2258             if ( $multilocation ) {
2259               my $loc = $cust_pkg->location_label;
2260               $loc = substr($loc, 0, $maxlength). '...'
2261                 if $format eq 'latex' && length($loc) > $maxlength;
2262               push @d, &{$escape_function}($loc);
2263             }
2264
2265           } #unless hiding service details
2266
2267           push @d, $cust_bill_pkg->details(%details_opt)
2268             if $cust_bill_pkg->recur == 0;
2269
2270           if ( $cust_bill_pkg->hidden ) {
2271             $s->{amount}      += $cust_bill_pkg->setup;
2272             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2273             push @{ $s->{ext_description} }, @d;
2274           } else {
2275             $s = {
2276               _is_setup       => 1,
2277               description     => $description,
2278               #pkgpart         => $part_pkg->pkgpart,
2279               pkgnum          => $cust_bill_pkg->pkgnum,
2280               amount          => $cust_bill_pkg->setup,
2281               setup_show_zero => $cust_bill_pkg->setup_show_zero,
2282               unit_amount     => $cust_bill_pkg->unitsetup,
2283               quantity        => $cust_bill_pkg->quantity,
2284               ext_description => \@d,
2285             };
2286           };
2287
2288         }
2289
2290         if (    ( !$type || $type eq 'R' || $type eq 'U' )
2291              && (
2292                      $cust_bill_pkg->recur != 0
2293                   || $cust_bill_pkg->setup == 0
2294                   || $discount_show_always
2295                   || $cust_bill_pkg->recur_show_zero
2296                 )
2297            )
2298         {
2299
2300           warn "$me _items_cust_bill_pkg adding recur/usage\n"
2301             if $DEBUG > 1;
2302
2303           my $is_summary = $display->summary;
2304           my $description = ($is_summary && $type && $type eq 'U')
2305                             ? "Usage charges" : $desc;
2306
2307           #pry be a bit more efficient to look some of this conf stuff up
2308           # outside the loop
2309           unless (
2310             $conf->exists('disable_line_item_date_ranges')
2311               || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
2312           ) {
2313             my $time_period;
2314             my $date_style = $conf->config( 'cust_bill-line_item-date_style',
2315                                             $cust_main->agentnum
2316                                           );
2317             if ( defined($date_style) && $date_style eq 'month_of' ) {
2318               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2319             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2320               my $desc = $conf->config( 'cust_bill-line_item-date_description',
2321                                          $cust_main->agentnum
2322                                       );
2323               $desc .= ' ' unless $desc =~ /\s$/;
2324               $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2325             } else {
2326               $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
2327                            " - ". time2str($date_format, $cust_bill_pkg->edate);
2328             }
2329             $description .= " ($time_period)";
2330           }
2331
2332           my @d = ();
2333           my @seconds = (); # for display of usage info
2334
2335           #at least until cust_bill_pkg has "past" ranges in addition to
2336           #the "future" sdate/edate ones... see #3032
2337           my @dates = ( $self->_date );
2338           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2339           push @dates, $prev->sdate if $prev;
2340           push @dates, undef if !$prev;
2341
2342           unless ( $cust_pkg->part_pkg->hide_svc_detail
2343                 || $cust_bill_pkg->itemdesc
2344                 || $cust_bill_pkg->hidden
2345                 || $is_summary && $type && $type eq 'U' )
2346           {
2347
2348             warn "$me _items_cust_bill_pkg adding service details\n"
2349               if $DEBUG > 1;
2350
2351             push @d, map &{$escape_function}($_),
2352                          $cust_pkg->h_labels_short(@dates, 'I')
2353                                                    #$cust_bill_pkg->edate,
2354                                                    #$cust_bill_pkg->sdate)
2355               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2356
2357             warn "$me _items_cust_bill_pkg done adding service details\n"
2358               if $DEBUG > 1;
2359
2360             if ( $multilocation ) {
2361               my $loc = $cust_pkg->location_label;
2362               $loc = substr($loc, 0, $maxlength). '...'
2363                 if $format eq 'latex' && length($loc) > $maxlength;
2364               push @d, &{$escape_function}($loc);
2365             }
2366
2367             # Display of seconds_since_sqlradacct:
2368             # On the invoice, when processing @detail_items, look for a field
2369             # named 'seconds'.  This will contain total seconds for each 
2370             # service, in the same order as @ext_description.  For services 
2371             # that don't support this it will show undef.
2372             if ( $conf->exists('svc_acct-usage_seconds') 
2373                  and ! $cust_bill_pkg->pkgpart_override ) {
2374               foreach my $cust_svc ( 
2375                   $cust_pkg->h_cust_svc(@dates, 'I') 
2376                 ) {
2377
2378                 # eval because not having any part_export_usage exports 
2379                 # is a fatal error, last_bill/_date because that's how 
2380                 # sqlradius_hour billing does it
2381                 my $sec = eval {
2382                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2383                 };
2384                 push @seconds, $sec;
2385               }
2386             } #if svc_acct-usage_seconds
2387
2388           }
2389
2390           unless ( $is_summary ) {
2391             warn "$me _items_cust_bill_pkg adding details\n"
2392               if $DEBUG > 1;
2393
2394             #instead of omitting details entirely in this case (unwanted side
2395             # effects), just omit CDRs
2396             $details_opt{'no_usage'} = 1
2397               if $type && $type eq 'R';
2398
2399             push @d, $cust_bill_pkg->details(%details_opt);
2400           }
2401
2402           warn "$me _items_cust_bill_pkg calculating amount\n"
2403             if $DEBUG > 1;
2404   
2405           my $amount = 0;
2406           if (!$type) {
2407             $amount = $cust_bill_pkg->recur;
2408           } elsif ($type eq 'R') {
2409             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2410           } elsif ($type eq 'U') {
2411             $amount = $cust_bill_pkg->usage;
2412           }
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} += $cust_bill_pkg->unitrecur;
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     => $cust_bill_pkg->unitrecur,
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} += $cust_bill_pkg->unitrecur;
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     => $cust_bill_pkg->unitrecur,
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;