don't set %item_dates for custom invoice formats if package option disable_line_item_...
[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->{'pkgpart'} = $line_item->{'pkgpart'};
825       $detail->{'quantity'} = 1;
826       $detail->{'section'} = $multisection ? $previous_section
827                                            : $default_section;
828       $detail->{'description'} = &$escape_function($line_item->{'description'});
829       if ( exists $line_item->{'ext_description'} ) {
830         @{$detail->{'ext_description'}} = map {
831           &$escape_function($_);
832         } @{$line_item->{'ext_description'}};
833       }
834       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
835                             $line_item->{'amount'};
836       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
837
838       push @detail_items, $detail;
839       push @buf, [ $detail->{'description'},
840                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
841                  ];
842     }
843
844   }
845
846   if ( @pr_cust_bill && $self->enable_previous ) {
847     push @buf, ['','-----------'];
848     push @buf, [ $self->mt('Total Previous Balance'),
849                  $money_char. sprintf("%10.2f", $pr_total) ];
850     push @buf, ['',''];
851   }
852  
853   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
854       warn "$me adding DID summary\n"
855         if $DEBUG > 1;
856
857       my ($didsummary,$minutes) = $self->_did_summary;
858       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
859       push @detail_items, 
860        { 'description' => $didsummary_desc,
861            'ext_description' => [ $didsummary, $minutes ],
862        };
863   }
864
865   foreach my $section (@sections, @$late_sections) {
866
867     warn "$me adding section \n". Dumper($section)
868       if $DEBUG > 1;
869
870     # begin some normalization
871     $section->{'subtotal'} = $section->{'amount'}
872       if $multisection
873          && !exists($section->{subtotal})
874          && exists($section->{amount});
875
876     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
877       if ( $invoice_data{finance_section} &&
878            $section->{'description'} eq $invoice_data{finance_section} );
879
880     $section->{'subtotal'} = $other_money_char.
881                              sprintf('%.2f', $section->{'subtotal'})
882       if $multisection;
883
884     # continue some normalization
885     $section->{'amount'}   = $section->{'subtotal'}
886       if $multisection;
887
888
889     if ( $section->{'description'} ) {
890       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
891                    [ '', '' ],
892                  );
893     }
894
895     warn "$me   setting options\n"
896       if $DEBUG > 1;
897
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{'multisection'} = $multisection;
908
909     warn "$me   searching for line items\n"
910       if $DEBUG > 1;
911
912     foreach my $line_item ( $self->_items_pkg(%options) ) {
913
914       warn "$me     adding line item $line_item\n"
915         if $DEBUG > 1;
916
917       my $detail = {
918         ext_description => [],
919       };
920       $detail->{'ref'} = $line_item->{'pkgnum'};
921       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
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   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1230   # the invoice variables
1231   return \%invoice_data if ( $params{'diag'} );
1232
1233   # All sections and items are built; now fill in templates.
1234   my @includelist = ();
1235   push @includelist, 'summary' if $summarypage;
1236   foreach my $include ( @includelist ) {
1237
1238     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1239     my @inc_src;
1240
1241     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1242
1243       @inc_src = $conf->config($inc_file, $agentnum);
1244
1245     } else {
1246
1247       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1248
1249       my $convert_map = $convert_maps{$format}{$include};
1250
1251       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1252                        s/--\@\]/$delimiters{$format}[1]/g;
1253                        $_;
1254                      } 
1255                  &$convert_map( $conf->config($inc_file, $agentnum) );
1256
1257     }
1258
1259     my $inc_tt = new Text::Template (
1260       TYPE       => 'ARRAY',
1261       SOURCE     => [ map "$_\n", @inc_src ],
1262       DELIMITERS => $delimiters{$format},
1263     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1264
1265     unless ( $inc_tt->compile() ) {
1266       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1267       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1268       die $error;
1269     }
1270
1271     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1272
1273     $invoice_data{$include} =~ s/\n+$//
1274       if ($format eq 'latex');
1275   }
1276
1277   $invoice_lines = 0;
1278   my $wasfunc = 0;
1279   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1280     /invoice_lines\((\d*)\)/;
1281     $invoice_lines += $1 || scalar(@buf);
1282     $wasfunc=1;
1283   }
1284   die "no invoice_lines() functions in template?"
1285     if ( $format eq 'template' && !$wasfunc );
1286
1287   if ($format eq 'template') {
1288
1289     if ( $invoice_lines ) {
1290       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1291       $invoice_data{'total_pages'}++
1292         if scalar(@buf) % $invoice_lines;
1293     }
1294
1295     #setup subroutine for the template
1296     $invoice_data{invoice_lines} = sub {
1297       my $lines = shift || scalar(@buf);
1298       map { 
1299         scalar(@buf)
1300           ? shift @buf
1301           : [ '', '' ];
1302       }
1303       ( 1 .. $lines );
1304     };
1305
1306     my $lines;
1307     my @collect;
1308     while (@buf) {
1309       push @collect, split("\n",
1310         $text_template->fill_in( HASH => \%invoice_data )
1311       );
1312       $invoice_data{'page'}++;
1313     }
1314     map "$_\n", @collect;
1315
1316   } else { # this is where we actually create the invoice
1317
1318     warn "filling in template for invoice ". $self->invnum. "\n"
1319       if $DEBUG;
1320     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1321       if $DEBUG > 1;
1322
1323     $text_template->fill_in(HASH => \%invoice_data);
1324   }
1325 }
1326
1327 sub notice_name { '('.shift->table.')'; }
1328
1329 sub template_conf { 'invoice_'; }
1330
1331 # helper routine for generating date ranges
1332 sub _prior_month30s {
1333   my $self = shift;
1334   my @ranges = (
1335    [ 1,       2592000 ], # 0-30 days ago
1336    [ 2592000, 5184000 ], # 30-60 days ago
1337    [ 5184000, 7776000 ], # 60-90 days ago
1338    [ 7776000, 0       ], # 90+   days ago
1339   );
1340
1341   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1342           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1343       ] }
1344   @ranges;
1345 }
1346
1347 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1348
1349 Returns an postscript invoice, as a scalar.
1350
1351 Options can be passed as a hashref (recommended) or as a list of time, template
1352 and then any key/value pairs for any other options.
1353
1354 I<time> an optional value used to control the printing of overdue messages.  The
1355 default is now.  It isn't the date of the invoice; that's the `_date' field.
1356 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1357 L<Time::Local> and L<Date::Parse> for conversion functions.
1358
1359 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1360
1361 =cut
1362
1363 sub print_ps {
1364   my $self = shift;
1365
1366   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1367   my $ps = generate_ps($file);
1368   unlink($logofile);
1369   unlink($barcodefile) if $barcodefile;
1370
1371   $ps;
1372 }
1373
1374 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1375
1376 Returns an PDF invoice, as a scalar.
1377
1378 Options can be passed as a hashref (recommended) or as a list of time, template
1379 and then any key/value pairs for any other options.
1380
1381 I<time> an optional value used to control the printing of overdue messages.  The
1382 default is now.  It isn't the date of the invoice; that's the `_date' field.
1383 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1384 L<Time::Local> and L<Date::Parse> for conversion functions.
1385
1386 I<template>, if specified, is the name of a suffix for alternate invoices.
1387
1388 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1389
1390 =cut
1391
1392 sub print_pdf {
1393   my $self = shift;
1394
1395   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1396   my $pdf = generate_pdf($file);
1397   unlink($logofile);
1398   unlink($barcodefile) if $barcodefile;
1399
1400   $pdf;
1401 }
1402
1403 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1404
1405 Returns an HTML invoice, as a scalar.
1406
1407 I<time> an optional value used to control the printing of overdue messages.  The
1408 default is now.  It isn't the date of the invoice; that's the `_date' field.
1409 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1410 L<Time::Local> and L<Date::Parse> for conversion functions.
1411
1412 I<template>, if specified, is the name of a suffix for alternate invoices.
1413
1414 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1415
1416 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1417 when emailing the invoice as part of a multipart/related MIME email.
1418
1419 =cut
1420
1421 sub print_html {
1422   my $self = shift;
1423   my %params;
1424   if ( ref($_[0]) ) {
1425     %params = %{ shift() }; 
1426   }else{
1427     $params{'time'} = shift;
1428     $params{'template'} = shift;
1429     $params{'cid'} = shift;
1430   }
1431
1432   $params{'format'} = 'html';
1433   
1434   $self->print_generic( %params );
1435 }
1436
1437 # quick subroutine for print_latex
1438 #
1439 # There are ten characters that LaTeX treats as special characters, which
1440 # means that they do not simply typeset themselves: 
1441 #      # $ % & ~ _ ^ \ { }
1442 #
1443 # TeX ignores blanks following an escaped character; if you want a blank (as
1444 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1445
1446 sub _latex_escape {
1447   my $value = shift;
1448   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1449   $value =~ s/([<>])/\$$1\$/g;
1450   $value;
1451 }
1452
1453 sub _html_escape {
1454   my $value = shift;
1455   encode_entities($value);
1456   $value;
1457 }
1458
1459 sub _html_escape_nbsp {
1460   my $value = _html_escape(shift);
1461   $value =~ s/ +/&nbsp;/g;
1462   $value;
1463 }
1464
1465 #utility methods for print_*
1466
1467 sub _translate_old_latex_format {
1468   warn "_translate_old_latex_format called\n"
1469     if $DEBUG; 
1470
1471   my @template = ();
1472   while ( @_ ) {
1473     my $line = shift;
1474   
1475     if ( $line =~ /^%%Detail\s*$/ ) {
1476   
1477       push @template, q![@--!,
1478                       q!  foreach my $_tr_line (@detail_items) {!,
1479                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1480                       q!      $_tr_line->{'description'} .= !, 
1481                       q!        "\\tabularnewline\n~~".!,
1482                       q!        join( "\\tabularnewline\n~~",!,
1483                       q!          @{$_tr_line->{'ext_description'}}!,
1484                       q!        );!,
1485                       q!    }!;
1486
1487       while ( ( my $line_item_line = shift )
1488               !~ /^%%EndDetail\s*$/                            ) {
1489         $line_item_line =~ s/'/\\'/g;    # nice LTS
1490         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1491         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1492         push @template, "    \$OUT .= '$line_item_line';";
1493       }
1494
1495       push @template, '}',
1496                       '--@]';
1497       #' doh, gvim
1498     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1499
1500       push @template, '[@--',
1501                       '  foreach my $_tr_line (@total_items) {';
1502
1503       while ( ( my $total_item_line = shift )
1504               !~ /^%%EndTotalDetails\s*$/                      ) {
1505         $total_item_line =~ s/'/\\'/g;    # nice LTS
1506         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1507         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1508         push @template, "    \$OUT .= '$total_item_line';";
1509       }
1510
1511       push @template, '}',
1512                       '--@]';
1513
1514     } else {
1515       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1516       push @template, $line;  
1517     }
1518   
1519   }
1520
1521   if ($DEBUG) {
1522     warn "$_\n" foreach @template;
1523   }
1524
1525   (@template);
1526 }
1527
1528 sub terms {
1529   my $self = shift;
1530   my $conf = $self->conf;
1531
1532   #check for an invoice-specific override
1533   return $self->invoice_terms if $self->invoice_terms;
1534   
1535   #check for a customer- specific override
1536   my $cust_main = $self->cust_main;
1537   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1538
1539   #use configured default
1540   $conf->config('invoice_default_terms') || '';
1541 }
1542
1543 sub due_date {
1544   my $self = shift;
1545   my $duedate = '';
1546   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1547     $duedate = $self->_date() + ( $1 * 86400 );
1548   }
1549   $duedate;
1550 }
1551
1552 sub due_date2str {
1553   my $self = shift;
1554   $self->due_date ? time2str(shift, $self->due_date) : '';
1555 }
1556
1557 sub balance_due_msg {
1558   my $self = shift;
1559   my $msg = $self->mt('Balance Due');
1560   return $msg unless $self->terms;
1561   if ( $self->due_date ) {
1562     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1563       $self->due_date2str($date_format);
1564   } elsif ( $self->terms ) {
1565     $msg .= ' - '. $self->terms;
1566   }
1567   $msg;
1568 }
1569
1570 sub balance_due_date {
1571   my $self = shift;
1572   my $conf = $self->conf;
1573   my $duedate = '';
1574   if (    $conf->exists('invoice_default_terms') 
1575        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1576     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1577   }
1578   $duedate;
1579 }
1580
1581 sub credit_balance_msg { 
1582   my $self = shift;
1583   $self->mt('Credit Balance Remaining')
1584 }
1585
1586 =item _date_pretty
1587
1588 Returns a string with the date, for example: "3/20/2008"
1589
1590 =cut
1591
1592 sub _date_pretty {
1593   my $self = shift;
1594   time2str($date_format, $self->_date);
1595 }
1596
1597 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1598
1599 Generate section information for all items appearing on this invoice.
1600 This will only be called for multi-section invoices.
1601
1602 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
1603 related display records (L<FS::cust_bill_pkg_display>) and organize 
1604 them into two groups ("early" and "late" according to whether they come 
1605 before or after the total), then into sections.  A subtotal is calculated 
1606 for each section.
1607
1608 Section descriptions are returned in sort weight order.  Each consists 
1609 of a hash containing:
1610
1611 description: the package category name, escaped
1612 subtotal: the total charges in that section
1613 tax_section: a flag indicating that the section contains only tax charges
1614 summarized: same as tax_section, for some reason
1615 sort_weight: the package category's sort weight
1616
1617 If 'condense' is set on the display record, it also contains everything 
1618 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1619 coderefs to generate parts of the invoice.  This is not advised.
1620
1621 Arguments:
1622
1623 LATE: an arrayref to push the "late" section hashes onto.  The "early"
1624 group is simply returned from the method.
1625
1626 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1627 Turning this on has the following effects:
1628 - Ignores display items with the 'summary' flag.
1629 - Combines all items into the "early" group.
1630 - Creates sections for all non-disabled package categories, even if they 
1631 have no charges on this invoice, as well as a section with no name.
1632
1633 ESCAPE: an escape function to use for section titles.
1634
1635 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
1636 sorted list.  If there are any of these, section subtotals exclude 
1637 usage charges.
1638
1639 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
1640 passed through to C<_condense_section()>.
1641
1642 =cut
1643
1644 use vars qw(%pkg_category_cache);
1645 sub _items_sections {
1646   my $self = shift;
1647   my $late = shift;
1648   my $summarypage = shift;
1649   my $escape = shift;
1650   my $extra_sections = shift;
1651   my $format = shift;
1652
1653   my %subtotal = ();
1654   my %late_subtotal = ();
1655   my %not_tax = ();
1656
1657   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1658   {
1659
1660       my $usage = $cust_bill_pkg->usage;
1661
1662       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1663         next if ( $display->summary && $summarypage );
1664
1665         my $section = $display->section;
1666         my $type    = $display->type;
1667
1668         $not_tax{$section} = 1
1669           unless $cust_bill_pkg->pkgnum == 0;
1670
1671         # there's actually a very important piece of logic buried in here:
1672         # incrementing $late_subtotal{$section} CREATES 
1673         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
1674         # to define the list of late sections, and likewise keys(%subtotal).
1675         # When _items_cust_bill_pkg is called to generate line items for 
1676         # real, it will be called with 'section' => $section for each 
1677         # of these.
1678         if ( $display->post_total && !$summarypage ) {
1679           if (! $type || $type eq 'S') {
1680             $late_subtotal{$section} += $cust_bill_pkg->setup
1681               if $cust_bill_pkg->setup != 0
1682               || $cust_bill_pkg->setup_show_zero;
1683           }
1684
1685           if (! $type) {
1686             $late_subtotal{$section} += $cust_bill_pkg->recur
1687               if $cust_bill_pkg->recur != 0
1688               || $cust_bill_pkg->recur_show_zero;
1689           }
1690
1691           if ($type && $type eq 'R') {
1692             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1693               if $cust_bill_pkg->recur != 0
1694               || $cust_bill_pkg->recur_show_zero;
1695           }
1696           
1697           if ($type && $type eq 'U') {
1698             $late_subtotal{$section} += $usage
1699               unless scalar(@$extra_sections);
1700           }
1701
1702         } else {
1703
1704           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1705
1706           if (! $type || $type eq 'S') {
1707             $subtotal{$section} += $cust_bill_pkg->setup
1708               if $cust_bill_pkg->setup != 0
1709               || $cust_bill_pkg->setup_show_zero;
1710           }
1711
1712           if (! $type) {
1713             $subtotal{$section} += $cust_bill_pkg->recur
1714               if $cust_bill_pkg->recur != 0
1715               || $cust_bill_pkg->recur_show_zero;
1716           }
1717
1718           if ($type && $type eq 'R') {
1719             $subtotal{$section} += $cust_bill_pkg->recur - $usage
1720               if $cust_bill_pkg->recur != 0
1721               || $cust_bill_pkg->recur_show_zero;
1722           }
1723           
1724           if ($type && $type eq 'U') {
1725             $subtotal{$section} += $usage
1726               unless scalar(@$extra_sections);
1727           }
1728
1729         }
1730
1731       }
1732
1733   }
1734
1735   %pkg_category_cache = ();
1736
1737   push @$late, map { { 'description' => &{$escape}($_),
1738                        'subtotal'    => $late_subtotal{$_},
1739                        'post_total'  => 1,
1740                        'sort_weight' => ( _pkg_category($_)
1741                                             ? _pkg_category($_)->weight
1742                                             : 0
1743                                        ),
1744                        ((_pkg_category($_) && _pkg_category($_)->condense)
1745                                            ? $self->_condense_section($format)
1746                                            : ()
1747                        ),
1748                    } }
1749                  sort _sectionsort keys %late_subtotal;
1750
1751   my @sections;
1752   if ( $summarypage ) {
1753     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1754                 map { $_->categoryname } qsearch('pkg_category', {});
1755     push @sections, '' if exists($subtotal{''});
1756   } else {
1757     @sections = keys %subtotal;
1758   }
1759
1760   my @early = map { { 'description' => &{$escape}($_),
1761                       'subtotal'    => $subtotal{$_},
1762                       'summarized'  => $not_tax{$_} ? '' : 'Y',
1763                       'tax_section' => $not_tax{$_} ? '' : 'Y',
1764                       'sort_weight' => ( _pkg_category($_)
1765                                            ? _pkg_category($_)->weight
1766                                            : 0
1767                                        ),
1768                        ((_pkg_category($_) && _pkg_category($_)->condense)
1769                                            ? $self->_condense_section($format)
1770                                            : ()
1771                        ),
1772                     }
1773                   } @sections;
1774   push @early, @$extra_sections if $extra_sections;
1775
1776   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1777
1778 }
1779
1780 #helper subs for above
1781
1782 sub _sectionsort {
1783   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1784 }
1785
1786 sub _pkg_category {
1787   my $categoryname = shift;
1788   $pkg_category_cache{$categoryname} ||=
1789     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1790 }
1791
1792 my %condensed_format = (
1793   'label' => [ qw( Description Qty Amount ) ],
1794   'fields' => [
1795                 sub { shift->{description} },
1796                 sub { shift->{quantity} },
1797                 sub { my($href, %opt) = @_;
1798                       ($opt{dollar} || ''). $href->{amount};
1799                     },
1800               ],
1801   'align'  => [ qw( l r r ) ],
1802   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
1803   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
1804 );
1805
1806 sub _condense_section {
1807   my ( $self, $format ) = ( shift, shift );
1808   ( 'condensed' => 1,
1809     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1810       qw( description_generator
1811           header_generator
1812           total_generator
1813           total_line_generator
1814         )
1815   );
1816 }
1817
1818 sub _condensed_generator_defaults {
1819   my ( $self, $format ) = ( shift, shift );
1820   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1821 }
1822
1823 my %html_align = (
1824   'c' => 'center',
1825   'l' => 'left',
1826   'r' => 'right',
1827 );
1828
1829 sub _condensed_header_generator {
1830   my ( $self, $format ) = ( shift, shift );
1831
1832   my ( $f, $prefix, $suffix, $separator, $column ) =
1833     _condensed_generator_defaults($format);
1834
1835   if ($format eq 'latex') {
1836     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1837     $suffix = "\\\\\n\\hline";
1838     $separator = "&\n";
1839     $column =
1840       sub { my ($d,$a,$s,$w) = @_;
1841             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1842           };
1843   } elsif ( $format eq 'html' ) {
1844     $prefix = '<th></th>';
1845     $suffix = '';
1846     $separator = '';
1847     $column =
1848       sub { my ($d,$a,$s,$w) = @_;
1849             return qq!<th align="$html_align{$a}">$d</th>!;
1850       };
1851   }
1852
1853   sub {
1854     my @args = @_;
1855     my @result = ();
1856
1857     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
1858       push @result,
1859         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1860     }
1861
1862     $prefix. join($separator, @result). $suffix;
1863   };
1864
1865 }
1866
1867 sub _condensed_description_generator {
1868   my ( $self, $format ) = ( shift, shift );
1869
1870   my ( $f, $prefix, $suffix, $separator, $column ) =
1871     _condensed_generator_defaults($format);
1872
1873   my $money_char = '$';
1874   if ($format eq 'latex') {
1875     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1876     $suffix = '\\\\';
1877     $separator = " & \n";
1878     $column =
1879       sub { my ($d,$a,$s,$w) = @_;
1880             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1881           };
1882     $money_char = '\\dollar';
1883   }elsif ( $format eq 'html' ) {
1884     $prefix = '"><td align="center"></td>';
1885     $suffix = '';
1886     $separator = '';
1887     $column =
1888       sub { my ($d,$a,$s,$w) = @_;
1889             return qq!<td align="$html_align{$a}">$d</td>!;
1890       };
1891     #$money_char = $conf->config('money_char') || '$';
1892     $money_char = '';  # this is madness
1893   }
1894
1895   sub {
1896     #my @args = @_;
1897     my $href = shift;
1898     my @result = ();
1899
1900     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
1901       my $dollar = '';
1902       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1903       push @result,
1904         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1905                     map { $f->{$_}->[$i] } qw(align span width)
1906                   );
1907     }
1908
1909     $prefix. join( $separator, @result ). $suffix;
1910   };
1911
1912 }
1913
1914 sub _condensed_total_generator {
1915   my ( $self, $format ) = ( shift, shift );
1916
1917   my ( $f, $prefix, $suffix, $separator, $column ) =
1918     _condensed_generator_defaults($format);
1919   my $style = '';
1920
1921   if ($format eq 'latex') {
1922     $prefix = "& ";
1923     $suffix = "\\\\\n";
1924     $separator = " & \n";
1925     $column =
1926       sub { my ($d,$a,$s,$w) = @_;
1927             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1928           };
1929   }elsif ( $format eq 'html' ) {
1930     $prefix = '';
1931     $suffix = '';
1932     $separator = '';
1933     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1934     $column =
1935       sub { my ($d,$a,$s,$w) = @_;
1936             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1937       };
1938   }
1939
1940
1941   sub {
1942     my @args = @_;
1943     my @result = ();
1944
1945     #  my $r = &{$f->{fields}->[$i]}(@args);
1946     #  $r .= ' Total' unless $i;
1947
1948     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
1949       push @result,
1950         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1951                     map { $f->{$_}->[$i] } qw(align span width)
1952                   );
1953     }
1954
1955     $prefix. join( $separator, @result ). $suffix;
1956   };
1957
1958 }
1959
1960 =item total_line_generator FORMAT
1961
1962 Returns a coderef used for generation of invoice total line items for this
1963 usage_class.  FORMAT is either html or latex
1964
1965 =cut
1966
1967 # should not be used: will have issues with hash element names (description vs
1968 # total_item and amount vs total_amount -- another array of functions?
1969
1970 sub _condensed_total_line_generator {
1971   my ( $self, $format ) = ( shift, shift );
1972
1973   my ( $f, $prefix, $suffix, $separator, $column ) =
1974     _condensed_generator_defaults($format);
1975   my $style = '';
1976
1977   if ($format eq 'latex') {
1978     $prefix = "& ";
1979     $suffix = "\\\\\n";
1980     $separator = " & \n";
1981     $column =
1982       sub { my ($d,$a,$s,$w) = @_;
1983             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1984           };
1985   }elsif ( $format eq 'html' ) {
1986     $prefix = '';
1987     $suffix = '';
1988     $separator = '';
1989     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1990     $column =
1991       sub { my ($d,$a,$s,$w) = @_;
1992             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1993       };
1994   }
1995
1996
1997   sub {
1998     my @args = @_;
1999     my @result = ();
2000
2001     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2002       push @result,
2003         &{$column}( &{$f->{fields}->[$i]}(@args),
2004                     map { $f->{$_}->[$i] } qw(align span width)
2005                   );
2006     }
2007
2008     $prefix. join( $separator, @result ). $suffix;
2009   };
2010
2011 }
2012
2013 #  sub _items { # seems to be unused
2014 #    my $self = shift;
2015 #  
2016 #    #my @display = scalar(@_)
2017 #    #              ? @_
2018 #    #              : qw( _items_previous _items_pkg );
2019 #    #              #: qw( _items_pkg );
2020 #    #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2021 #    my @display = qw( _items_previous _items_pkg );
2022 #  
2023 #    my @b = ();
2024 #    foreach my $display ( @display ) {
2025 #      push @b, $self->$display(@_);
2026 #    }
2027 #    @b;
2028 #  }
2029
2030 =item _items_pkg [ OPTIONS ]
2031
2032 Return line item hashes for each package item on this invoice. Nearly 
2033 equivalent to 
2034
2035 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2036
2037 The only OPTIONS accepted is 'section', which may point to a hashref 
2038 with a key named 'condensed', which may have a true value.  If it 
2039 does, this method tries to merge identical items into items with 
2040 'quantity' equal to the number of items (not the sum of their 
2041 separate quantities, for some reason).
2042
2043 =cut
2044
2045 sub _items_pkg {
2046   my $self = shift;
2047   my %options = @_;
2048
2049   warn "$me _items_pkg searching for all package line items\n"
2050     if $DEBUG > 1;
2051
2052   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2053
2054   warn "$me _items_pkg filtering line items\n"
2055     if $DEBUG > 1;
2056   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2057
2058   if ($options{section} && $options{section}->{condensed}) {
2059
2060     warn "$me _items_pkg condensing section\n"
2061       if $DEBUG > 1;
2062
2063     my %itemshash = ();
2064     local $Storable::canonical = 1;
2065     foreach ( @items ) {
2066       my $item = { %$_ };
2067       delete $item->{ref};
2068       delete $item->{ext_description};
2069       my $key = freeze($item);
2070       $itemshash{$key} ||= 0;
2071       $itemshash{$key} ++; # += $item->{quantity};
2072     }
2073     @items = sort { $a->{description} cmp $b->{description} }
2074              map { my $i = thaw($_);
2075                    $i->{quantity} = $itemshash{$_};
2076                    $i->{amount} =
2077                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2078                    $i;
2079                  }
2080              keys %itemshash;
2081   }
2082
2083   warn "$me _items_pkg returning ". scalar(@items). " items\n"
2084     if $DEBUG > 1;
2085
2086   @items;
2087 }
2088
2089 sub _taxsort {
2090   return 0 unless $a->itemdesc cmp $b->itemdesc;
2091   return -1 if $b->itemdesc eq 'Tax';
2092   return 1 if $a->itemdesc eq 'Tax';
2093   return -1 if $b->itemdesc eq 'Other surcharges';
2094   return 1 if $a->itemdesc eq 'Other surcharges';
2095   $a->itemdesc cmp $b->itemdesc;
2096 }
2097
2098 sub _items_tax {
2099   my $self = shift;
2100   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2101   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2102 }
2103
2104 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2105
2106 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2107 list of hashrefs describing the line items they generate on the invoice.
2108
2109 OPTIONS may include:
2110
2111 format: the invoice format.
2112
2113 escape_function: the function used to escape strings.
2114
2115 DEPRECATED? (expensive, mostly unused?)
2116 format_function: the function used to format CDRs.
2117
2118 section: a hashref containing 'description'; if this is present, 
2119 cust_bill_pkg_display records not belonging to this section are 
2120 ignored.
2121
2122 multisection: a flag indicating that this is a multisection invoice,
2123 which does something complicated.
2124
2125 Returns a list of hashrefs, each of which may contain:
2126
2127 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
2128 ext_description, which is an arrayref of detail lines to show below 
2129 the package line.
2130
2131 =cut
2132
2133 sub _items_cust_bill_pkg {
2134   my $self = shift;
2135   my $conf = $self->conf;
2136   my $cust_bill_pkgs = shift;
2137   my %opt = @_;
2138
2139   my $format = $opt{format} || '';
2140   my $escape_function = $opt{escape_function} || sub { shift };
2141   my $format_function = $opt{format_function} || '';
2142   my $no_usage = $opt{no_usage} || '';
2143   my $unsquelched = $opt{unsquelched} || ''; #unused
2144   my $section = $opt{section}->{description} if $opt{section};
2145   my $summary_page = $opt{summary_page} || ''; #unused
2146   my $multisection = $opt{multisection} || '';
2147   my $discount_show_always = 0;
2148
2149   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2150
2151   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2152                                    # and location labels
2153
2154   my @b = ();
2155   my ($s, $r, $u) = ( undef, undef, undef );
2156   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2157   {
2158
2159     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2160       if ( $_ && !$cust_bill_pkg->hidden ) {
2161         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2162         $_->{amount}      =~ s/^\-0\.00$/0.00/;
2163         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2164         push @b, { %$_ }
2165           if $_->{amount} != 0
2166           || $discount_show_always
2167           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2168           || (   $_->{_is_setup} && $_->{setup_show_zero} )
2169         ;
2170         $_ = undef;
2171       }
2172     }
2173
2174     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2175                                   ? $cust_bill_pkg->cust_bill_pkg_display
2176                                   : ( $cust_bill_pkg );
2177
2178     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2179          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2180       if $DEBUG > 1;
2181
2182     foreach my $display ( grep { defined($section)
2183                             ? $_->section eq $section
2184                             : 1
2185                           }
2186                           grep { !$_->summary || $multisection }
2187                           @cust_bill_pkg_display
2188                         )
2189       {
2190
2191       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2192            $display->billpkgdisplaynum. "\n"
2193         if $DEBUG > 1;
2194
2195       my $type = $display->type;
2196
2197       my $desc = $cust_bill_pkg->desc;
2198       $desc = substr($desc, 0, $maxlength). '...'
2199         if $format eq 'latex' && length($desc) > $maxlength;
2200
2201       my %details_opt = ( 'format'          => $format,
2202                           'escape_function' => $escape_function,
2203                           'format_function' => $format_function,
2204                           'no_usage'        => $opt{'no_usage'},
2205                         );
2206
2207       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2208
2209         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2210           if $DEBUG > 1;
2211
2212         if ( $cust_bill_pkg->setup != 0 ) {
2213           my $description = $desc;
2214           $description .= ' Setup'
2215             if $cust_bill_pkg->recur != 0
2216             || $discount_show_always
2217             || $cust_bill_pkg->recur_show_zero;
2218           push @b, {
2219             'description' => $description,
2220             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2221           };
2222         }
2223         if ( $cust_bill_pkg->recur != 0 ) {
2224           push @b, {
2225             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2226             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2227           };
2228         }
2229
2230       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2231
2232         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2233           if $DEBUG > 1;
2234  
2235         my $cust_pkg = $cust_bill_pkg->cust_pkg;
2236
2237         # which pkgpart to show for display purposes?
2238         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2239
2240         # start/end dates for invoice formats that do nonstandard 
2241         # things with them
2242         my %item_dates = ();
2243         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2244           unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2245
2246         if (    (!$type || $type eq 'S')
2247              && (    $cust_bill_pkg->setup != 0
2248                   || $cust_bill_pkg->setup_show_zero
2249                 )
2250            )
2251          {
2252
2253           warn "$me _items_cust_bill_pkg adding setup\n"
2254             if $DEBUG > 1;
2255
2256           my $description = $desc;
2257           $description .= ' Setup'
2258             if $cust_bill_pkg->recur != 0
2259             || $discount_show_always
2260             || $cust_bill_pkg->recur_show_zero;
2261
2262           my @d = ();
2263           unless ( $cust_pkg->part_pkg->hide_svc_detail
2264                 || $cust_bill_pkg->hidden )
2265           {
2266
2267             push @d, map &{$escape_function}($_),
2268                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
2269               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2270
2271             if ( ! $cust_pkg->locationnum or
2272                    $cust_pkg->locationnum != $cust_main->ship_locationnum  ) {
2273               my $loc = $cust_pkg->location_label;
2274               $loc = substr($loc, 0, $maxlength). '...'
2275                 if $format eq 'latex' && length($loc) > $maxlength;
2276               push @d, &{$escape_function}($loc);
2277             }
2278
2279           } #unless hiding service details
2280
2281           push @d, $cust_bill_pkg->details(%details_opt)
2282             if $cust_bill_pkg->recur == 0;
2283
2284           if ( $cust_bill_pkg->hidden ) {
2285             $s->{amount}      += $cust_bill_pkg->setup;
2286             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2287             push @{ $s->{ext_description} }, @d;
2288           } else {
2289             $s = {
2290               _is_setup       => 1,
2291               description     => $description,
2292               pkgpart         => $pkgpart,
2293               pkgnum          => $cust_bill_pkg->pkgnum,
2294               amount          => $cust_bill_pkg->setup,
2295               setup_show_zero => $cust_bill_pkg->setup_show_zero,
2296               unit_amount     => $cust_bill_pkg->unitsetup,
2297               quantity        => $cust_bill_pkg->quantity,
2298               ext_description => \@d,
2299             };
2300           };
2301
2302         }
2303
2304         if (    ( !$type || $type eq 'R' || $type eq 'U' )
2305              && (
2306                      $cust_bill_pkg->recur != 0
2307                   || $cust_bill_pkg->setup == 0
2308                   || $discount_show_always
2309                   || $cust_bill_pkg->recur_show_zero
2310                 )
2311            )
2312         {
2313
2314           warn "$me _items_cust_bill_pkg adding recur/usage\n"
2315             if $DEBUG > 1;
2316
2317           my $is_summary = $display->summary;
2318           my $description = ($is_summary && $type && $type eq 'U')
2319                             ? "Usage charges" : $desc;
2320
2321           #pry be a bit more efficient to look some of this conf stuff up
2322           # outside the loop
2323           unless (
2324             $conf->exists('disable_line_item_date_ranges')
2325               || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
2326           ) {
2327             my $time_period;
2328             my $date_style = $conf->config( 'cust_bill-line_item-date_style',
2329                                             $cust_main->agentnum
2330                                           );
2331             if ( defined($date_style) && $date_style eq 'month_of' ) {
2332               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2333             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2334               my $desc = $conf->config( 'cust_bill-line_item-date_description',
2335                                          $cust_main->agentnum
2336                                       );
2337               $desc .= ' ' unless $desc =~ /\s$/;
2338               $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2339             } else {
2340               $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
2341                            " - ". time2str($date_format, $cust_bill_pkg->edate);
2342             }
2343             $description .= " ($time_period)";
2344           }
2345
2346           my @d = ();
2347           my @seconds = (); # for display of usage info
2348
2349           #at least until cust_bill_pkg has "past" ranges in addition to
2350           #the "future" sdate/edate ones... see #3032
2351           my @dates = ( $self->_date );
2352           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2353           push @dates, $prev->sdate if $prev;
2354           push @dates, undef if !$prev;
2355
2356           unless ( $cust_pkg->part_pkg->hide_svc_detail
2357                 || $cust_bill_pkg->itemdesc
2358                 || $cust_bill_pkg->hidden
2359                 || $is_summary && $type && $type eq 'U'
2360               )
2361           {
2362
2363             warn "$me _items_cust_bill_pkg adding service details\n"
2364               if $DEBUG > 1;
2365
2366             push @d, map &{$escape_function}($_),
2367                          $cust_pkg->h_labels_short(@dates, 'I')
2368                                                    #$cust_bill_pkg->edate,
2369                                                    #$cust_bill_pkg->sdate)
2370               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2371
2372             warn "$me _items_cust_bill_pkg done adding service details\n"
2373               if $DEBUG > 1;
2374
2375             if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2376               my $loc = $cust_pkg->location_label;
2377               $loc = substr($loc, 0, $maxlength). '...'
2378                 if $format eq 'latex' && length($loc) > $maxlength;
2379               push @d, &{$escape_function}($loc);
2380             }
2381
2382             # Display of seconds_since_sqlradacct:
2383             # On the invoice, when processing @detail_items, look for a field
2384             # named 'seconds'.  This will contain total seconds for each 
2385             # service, in the same order as @ext_description.  For services 
2386             # that don't support this it will show undef.
2387             if ( $conf->exists('svc_acct-usage_seconds') 
2388                  and ! $cust_bill_pkg->pkgpart_override ) {
2389               foreach my $cust_svc ( 
2390                   $cust_pkg->h_cust_svc(@dates, 'I') 
2391                 ) {
2392
2393                 # eval because not having any part_export_usage exports 
2394                 # is a fatal error, last_bill/_date because that's how 
2395                 # sqlradius_hour billing does it
2396                 my $sec = eval {
2397                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2398                 };
2399                 push @seconds, $sec;
2400               }
2401             } #if svc_acct-usage_seconds
2402
2403           }
2404
2405           unless ( $is_summary ) {
2406             warn "$me _items_cust_bill_pkg adding details\n"
2407               if $DEBUG > 1;
2408
2409             #instead of omitting details entirely in this case (unwanted side
2410             # effects), just omit CDRs
2411             $details_opt{'no_usage'} = 1
2412               if $type && $type eq 'R';
2413
2414             push @d, $cust_bill_pkg->details(%details_opt);
2415           }
2416
2417           warn "$me _items_cust_bill_pkg calculating amount\n"
2418             if $DEBUG > 1;
2419   
2420           my $amount = 0;
2421           if (!$type) {
2422             $amount = $cust_bill_pkg->recur;
2423           } elsif ($type eq 'R') {
2424             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2425           } elsif ($type eq 'U') {
2426             $amount = $cust_bill_pkg->usage;
2427           }
2428   
2429           my $unit_amount =
2430             ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2431                                               : $amount;
2432
2433           if ( !$type || $type eq 'R' ) {
2434
2435             warn "$me _items_cust_bill_pkg adding recur\n"
2436               if $DEBUG > 1;
2437
2438             if ( $cust_bill_pkg->hidden ) {
2439               $r->{amount}      += $amount;
2440               $r->{unit_amount} += $unit_amount;
2441               push @{ $r->{ext_description} }, @d;
2442             } else {
2443               $r = {
2444                 description     => $description,
2445                 pkgpart         => $pkgpart,
2446                 pkgnum          => $cust_bill_pkg->pkgnum,
2447                 amount          => $amount,
2448                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2449                 unit_amount     => $unit_amount,
2450                 quantity        => $cust_bill_pkg->quantity,
2451                 %item_dates,
2452                 ext_description => \@d,
2453               };
2454               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2455             }
2456
2457           } else {  # $type eq 'U'
2458
2459             warn "$me _items_cust_bill_pkg adding usage\n"
2460               if $DEBUG > 1;
2461
2462             if ( $cust_bill_pkg->hidden ) {
2463               $u->{amount}      += $amount;
2464               $u->{unit_amount} += $unit_amount,
2465               push @{ $u->{ext_description} }, @d;
2466             } else {
2467               $u = {
2468                 description     => $description,
2469                 pkgpart         => $pkgpart,
2470                 pkgnum          => $cust_bill_pkg->pkgnum,
2471                 amount          => $amount,
2472                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2473                 unit_amount     => $unit_amount,
2474                 quantity        => $cust_bill_pkg->quantity,
2475                 %item_dates,
2476                 ext_description => \@d,
2477               };
2478             }
2479           }
2480
2481         } # recurring or usage with recurring charge
2482
2483       } else { #pkgnum tax or one-shot line item (??)
2484
2485         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2486           if $DEBUG > 1;
2487
2488         if ( $cust_bill_pkg->setup != 0 ) {
2489           push @b, {
2490             'description' => $desc,
2491             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2492           };
2493         }
2494         if ( $cust_bill_pkg->recur != 0 ) {
2495           push @b, {
2496             'description' => "$desc (".
2497                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2498                              time2str($date_format, $cust_bill_pkg->edate). ')',
2499             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2500           };
2501         }
2502
2503       }
2504
2505     }
2506
2507     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2508                                 && $conf->exists('discount-show-always'));
2509
2510   }
2511
2512   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2513     if ( $_  ) {
2514       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2515       $_->{amount}      =~ s/^\-0\.00$/0.00/;
2516       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2517       push @b, { %$_ }
2518         if $_->{amount} != 0
2519         || $discount_show_always
2520         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2521         || (   $_->{_is_setup} && $_->{setup_show_zero} )
2522     }
2523   }
2524
2525   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2526     if $DEBUG > 1;
2527
2528   @b;
2529
2530 }
2531
2532 =item _items_discounts_avail
2533
2534 Returns an array of line item hashrefs representing available term discounts
2535 for this invoice.  This makes the same assumptions that apply to term 
2536 discounts in general: that the package is billed monthly, at a flat rate, 
2537 with no usage charges.  A prorated first month will be handled, as will 
2538 a setup fee if the discount is allowed to apply to setup fees.
2539
2540 =cut
2541
2542 sub _items_discounts_avail {
2543   my $self = shift;
2544
2545   #maybe move this method from cust_bill when quotations support discount_plans 
2546   return () unless $self->can('discount_plans');
2547   my %plans = $self->discount_plans;
2548
2549   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2550   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2551
2552   map {
2553     my $months = $_;
2554     my $plan = $plans{$months};
2555
2556     my $term_total = sprintf('%.2f', $plan->discounted_total);
2557     my $percent = sprintf('%.0f', 
2558                           100 * (1 - $term_total / $plan->base_total) );
2559     my $permonth = sprintf('%.2f', $term_total / $months);
2560     my $detail = $self->mt('discount on item'). ' '.
2561                  join(', ', map { "#$_" } $plan->pkgnums)
2562       if $list_pkgnums;
2563
2564     # discounts for non-integer months don't work anyway
2565     $months = sprintf("%d", $months);
2566
2567     +{
2568       description => $self->mt('Save [_1]% by paying for [_2] months',
2569                                 $percent, $months),
2570       amount      => $self->mt('[_1] ([_2] per month)', 
2571                                 $term_total, $money_char.$permonth),
2572       ext_description => ($detail || ''),
2573     }
2574   } #map
2575   sort { $b <=> $a } keys %plans;
2576
2577 }
2578
2579 1;