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