freeside inc. web services for address normalizaion and printing, RT#33849
[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
6              $date_format
7            );
8              # but NOT $conf
9 use vars qw( $invoice_lines @buf ); #yuck
10 use List::Util qw(sum); #can't import first, it conflicts with cust_main.first
11 use Date::Format;
12 use Date::Language;
13 use Text::Template 1.20;
14 use File::Temp 0.14;
15 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
16 use IO::Scalar;
17 use HTML::Entities;
18 use Cwd;
19 use FS::UID;
20 use FS::Misc qw( send_email );
21 use FS::Record qw( qsearch qsearchs );
22 use FS::Conf;
23 use FS::Misc qw( generate_ps generate_pdf );
24 use FS::pkg_category;
25 use FS::pkg_class;
26 use FS::invoice_mode;
27 use FS::L10N;
28
29 $DEBUG = 0;
30 $me = '[FS::Template_Mixin]';
31 FS::UID->install_callback( sub { 
32   my $conf = new FS::Conf; #global
33   $money_char  = $conf->config('money_char')  || '$';  
34   $date_format = $conf->config('date_format') || '%x'; #/YY
35 } );
36
37 =item conf [ MODE ]
38
39 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
40
41 If the "mode" pseudo-field is set on the object, the configuration handle
42 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
43 locale).
44
45 =cut
46
47 sub conf {
48   my $self = shift;
49   my $mode = $self->get('mode');
50   if ($self->{_conf} and !defined($mode)) {
51     return $self->{_conf};
52   }
53
54   my $cust_main = $self->cust_main;
55   my $locale = $cust_main ? $cust_main->locale : '';
56   my $conf;
57   if ( $mode ) {
58     if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
59       $mode = $mode->modenum;
60     } elsif ( $mode =~ /\D/ ) {
61       die "invalid invoice mode $mode";
62     }
63     $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
64     if (!$conf) {
65       $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
66       # it doesn't have a locale, but system conf still might
67       $conf->set('locale' => $locale) if $conf;
68     }
69   }
70   # if $mode is unspecified, or if there is no invoice_conf matching this mode
71   # and locale, then use the system config only (but with the locale)
72   $conf ||= FS::Conf->new({ 'locale' => $locale });
73   # cache it
74   return $self->{_conf} = $conf;
75 }
76
77 =item print_text OPTIONS
78
79 Returns an text invoice, as a list of lines.
80
81 Options can be passed as a hash.
82
83 I<time>, if specified, is used to control the printing of overdue messages.  The
84 default is now.  It isn't the date of the invoice; that's the `_date' field.
85 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
86 L<Time::Local> and L<Date::Parse> for conversion functions.
87
88 I<template>, if specified, is the name of a suffix for alternate invoices.
89
90 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
91
92 =cut
93
94 sub print_text {
95   my $self = shift;
96   my %params;
97   if ( ref($_[0]) ) {
98     %params = %{ shift() };
99   } else {
100     %params = @_;
101   }
102
103   $params{'format'} = 'template'; # for some reason
104
105   $self->print_generic( %params );
106 }
107
108 =item print_latex HASHREF
109
110 Internal method - returns a filename of a filled-in LaTeX template for this
111 invoice (Note: add ".tex" to get the actual filename), and a filename of
112 an associated logo (with the .eps extension included).
113
114 See print_ps and print_pdf for methods that return PostScript and PDF output.
115
116 Options can be passed as a hash.
117
118 I<time>, if specified, is used to control the printing of overdue messages.  The
119 default is now.  It isn't the date of the invoice; that's the `_date' field.
120 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
121 L<Time::Local> and L<Date::Parse> for conversion functions.
122
123 I<template>, if specified, is the name of a suffix for alternate invoices.  
124 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
125 customize invoice templates for different purposes.
126
127 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
128
129 =cut
130
131 sub print_latex {
132   my $self = shift;
133   my %params;
134
135   if ( ref($_[0]) ) {
136     %params = %{ shift() };
137   } else {
138     %params = @_;
139   }
140
141   $params{'format'} = 'latex';
142   my $conf = $self->conf;
143
144   # this needs to go away
145   my $template = $params{'template'};
146   # and this especially
147   $template ||= $self->_agent_template
148     if $self->can('_agent_template');
149
150   my $pkey = $self->primary_key;
151   my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
152
153   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
154   my $lh = new File::Temp(
155     TEMPLATE => $tmp_template,
156     DIR      => $dir,
157     SUFFIX   => '.eps',
158     UNLINK   => 0,
159   ) or die "can't open temp file: $!\n";
160
161   my $agentnum = $self->agentnum;
162
163   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
164     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
165       or die "can't write temp file: $!\n";
166   } else {
167     print $lh $conf->config_binary('logo.eps', $agentnum)
168       or die "can't write temp file: $!\n";
169   }
170   close $lh;
171   $params{'logo_file'} = $lh->filename;
172
173   if( $conf->exists('invoice-barcode') 
174         && $self->can('invoice_barcode')
175         && $self->invnum ) { # don't try to barcode statements
176       my $png_file = $self->invoice_barcode($dir);
177       my $eps_file = $png_file;
178       $eps_file =~ s/\.png$/.eps/g;
179       $png_file =~ /(barcode.*png)/;
180       $png_file = $1;
181       $eps_file =~ /(barcode.*eps)/;
182       $eps_file = $1;
183
184       my $curr_dir = cwd();
185       chdir($dir); 
186       # after painfuly long experimentation, it was determined that sam2p won't
187       # accept : and other chars in the path, no matter how hard I tried to
188       # escape them, hence the chdir (and chdir back, just to be safe)
189       system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
190         or die "sam2p failed: $!\n";
191       unlink($png_file);
192       chdir($curr_dir);
193
194       $params{'barcode_file'} = $eps_file;
195   }
196
197   my @filled_in = $self->print_generic( %params );
198   
199   my $fh = new File::Temp( TEMPLATE => $tmp_template,
200                            DIR      => $dir,
201                            SUFFIX   => '.tex',
202                            UNLINK   => 0,
203                          ) or die "can't open temp file: $!\n";
204   binmode($fh, ':utf8'); # language support
205   print $fh join('', @filled_in );
206   close $fh;
207
208   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
209   return ($1, $params{'logo_file'}, $params{'barcode_file'});
210
211 }
212
213 sub agentnum {
214   my $self = shift;
215   my $cust_main = $self->cust_main;
216   $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
217 }
218
219 =item print_generic OPTION => VALUE ...
220
221 Internal method - returns a filled-in template for this invoice as a scalar.
222
223 See print_ps and print_pdf for methods that return PostScript and PDF output.
224
225 Required options
226
227 =over 4
228
229 =item format
230
231 The B<format> option is required and should be set to html, latex (print and PDF) or template (plaintext).
232
233 =back
234
235 Additional options
236
237 =over 4
238
239 =item notice_name
240
241 Overrides "Invoice" as the name of the sent document.
242
243 =item today
244
245 Used to control the printing of overdue messages.  The
246 default is now.  It isn't the date of the invoice; that's the `_date' field.
247 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
248 L<Time::Local> and L<Date::Parse> for conversion functions.
249
250 =item logo_file
251
252 Logo file (path to temporary EPS file on the local filesystem)
253
254 =item cid
255
256 CID for inline (emailed) images (logo)
257
258 =item locale
259
260 Override customer's locale
261
262 =item unsquelch_cdr
263
264 Overrides any per customer cdr squelching when true
265
266 =item no_number
267
268 Supress the (invoice, quotation, statement, etc.) number
269
270 =item no_date
271
272 Supress the date
273
274 =item no_coupon
275
276 Supress the payment coupon
277
278 =item barcode_file
279
280 Barcode file (path to temporary EPS file on the local filesystem)
281
282 =item barcode_img
283
284 Flag indicating the barcode image should be a link (normal HTML dipaly)
285
286 =item barcode_cid
287
288 Barcode CID for inline (emailed) images
289
290 =item preref_callback
291
292 Coderef run for each line item, code should return HTML to be displayed
293 before that line item (quotations only)
294
295 =item template
296
297 Dprecated.  Used as a suffix for a configuration template.  Please 
298 don't use this, it deprecated in favor of more flexible alternatives.
299
300 =back
301
302 =cut
303
304 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
305 # (alignment in text invoice?) problems to change them all to '%.2f' ?
306 # yes: fixed width/plain text printing will be borked
307 sub print_generic {
308   my( $self, %params ) = @_;
309   my $conf = $self->conf;
310
311   my $today = $params{today} ? $params{today} : time;
312   warn "$me print_generic called on $self with suffix $params{template}\n"
313     if $DEBUG;
314
315   my $format = $params{format};
316   die "Unknown format: $format"
317     unless $format =~ /^(latex|html|template)$/;
318
319   my $cust_main = $self->cust_main || $self->prospect_main;
320   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
321     unless $cust_main->payname
322         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
323
324   my $locale = $params{'locale'} || $cust_main->locale;
325
326   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
327                      'html'     => [ '<%=', '%>' ],
328                      'template' => [ '{', '}' ],
329                    );
330
331   warn "$me print_generic creating template\n"
332     if $DEBUG > 1;
333
334   # set the notice name here, and nowhere else.
335   my $notice_name =  $params{notice_name}
336                   || $conf->config('notice_name')
337                   || $self->notice_name;
338
339   #create the template
340   my $template = $params{template} ? $params{template} : $self->_agent_template;
341   my $templatefile = $self->template_conf. $format;
342   $templatefile .= "_$template"
343     if length($template) && $conf->exists($templatefile."_$template");
344
345   # the base template
346   my @invoice_template = map "$_\n", $conf->config($templatefile)
347     or die "cannot load config data $templatefile";
348
349   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
350     #change this to a die when the old code is removed
351     # it's been almost ten years, changing it to a die on the next release.
352     warn "old-style invoice template $templatefile; ".
353          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
354          #$old_latex = 'true';
355          #@invoice_template = _translate_old_latex_format(@invoice_template);
356   } 
357
358   warn "$me print_generic creating T:T object\n"
359     if $DEBUG > 1;
360
361   my $text_template = new Text::Template(
362     TYPE => 'ARRAY',
363     SOURCE => \@invoice_template,
364     DELIMITERS => $delimiters{$format},
365   );
366
367   warn "$me print_generic compiling T:T object\n"
368     if $DEBUG > 1;
369
370   $text_template->compile()
371     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
372
373
374   # additional substitution could possibly cause breakage in existing templates
375   my %convert_maps = ( 
376     'latex' => {
377                  'notes'         => sub { map "$_", @_ },
378                  'footer'        => sub { map "$_", @_ },
379                  'smallfooter'   => sub { map "$_", @_ },
380                  'returnaddress' => sub { map "$_", @_ },
381                  'coupon'        => sub { map "$_", @_ },
382                  'summary'       => sub { map "$_", @_ },
383                },
384     'html'  => {
385                  'notes' =>
386                    sub {
387                      map { 
388                        s/%%(.*)$/<!-- $1 -->/g;
389                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
390                        s/\\begin\{enumerate\}/<ol>/g;
391                        s/\\item /  <li>/g;
392                        s/\\end\{enumerate\}/<\/ol>/g;
393                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
394                        s/\\\\\*/<br>/g;
395                        s/\\dollar ?/\$/g;
396                        s/\\#/#/g;
397                        s/~/&nbsp;/g;
398                        $_;
399                      }  @_
400                    },
401                  'footer' =>
402                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
403                  'smallfooter' =>
404                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
405                  'returnaddress' =>
406                    sub {
407                      map { 
408                        s/~/&nbsp;/g;
409                        s/\\\\\*?\s*$/<BR>/;
410                        s/\\hyphenation\{[\w\s\-]+}//;
411                        s/\\([&])/$1/g;
412                        $_;
413                      }  @_
414                    },
415                  'coupon'        => sub { "" },
416                  'summary'       => sub { "" },
417                },
418     'template' => {
419                  'notes' =>
420                    sub {
421                      map { 
422                        s/%%.*$//g;
423                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
424                        s/\\begin\{enumerate\}//g;
425                        s/\\item /  * /g;
426                        s/\\end\{enumerate\}//g;
427                        s/\\textbf\{(.*)\}/$1/g;
428                        s/\\\\\*/ /;
429                        s/\\dollar ?/\$/g;
430                        $_;
431                      }  @_
432                    },
433                  'footer' =>
434                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
435                  'smallfooter' =>
436                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
437                  'returnaddress' =>
438                    sub {
439                      map { 
440                        s/~/ /g;
441                        s/\\\\\*?\s*$/\n/;             # dubious
442                        s/\\hyphenation\{[\w\s\-]+}//;
443                        $_;
444                      }  @_
445                    },
446                  'coupon'        => sub { "" },
447                  'summary'       => sub { "" },
448                },
449   );
450
451
452   # hashes for differing output formats
453   my %nbsps = ( 'latex'    => '~',
454                 'html'     => '',    # '&nbps;' would be nice
455                 'template' => '',    # not used
456               );
457   my $nbsp = $nbsps{$format};
458
459   my %escape_functions = ( 'latex'    => \&_latex_escape,
460                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
461                            'template' => sub { shift },
462                          );
463   my $escape_function = $escape_functions{$format};
464   my $escape_function_nonbsp = ($format eq 'html')
465                                  ? \&_html_escape : $escape_function;
466
467   my %newline_tokens = (  'latex'     => '\\\\',
468                           'html'      => '<br>',
469                           'template'  => "\n",
470                         );
471   my $newline_token = $newline_tokens{$format};
472
473   warn "$me generating template variables\n"
474     if $DEBUG > 1;
475
476   # generate template variables
477   my $returnaddress;
478
479   if (
480          defined( $conf->config_orbase( "invoice_${format}returnaddress",
481                                         $template
482                                       )
483                 )
484        && length( $conf->config_orbase( "invoice_${format}returnaddress",
485                                         $template
486                                       )
487                 )
488   ) {
489
490     $returnaddress = join("\n",
491       $conf->config_orbase("invoice_${format}returnaddress", $template)
492     );
493
494   } elsif ( grep /\S/,
495             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
496
497     my $convert_map = $convert_maps{$format}{'returnaddress'};
498     $returnaddress =
499       join( "\n",
500             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
501                                                  $template
502                                                )
503                          )
504           );
505   } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
506
507     my $convert_map = $convert_maps{$format}{'returnaddress'};
508     $returnaddress = join( "\n", &$convert_map(
509                                    map { s/( {2,})/'~' x length($1)/eg;
510                                          s/$/\\\\\*/;
511                                          $_
512                                        }
513                                      ( $conf->config('company_name', $cust_main->agentnum),
514                                        $conf->config('company_address', $cust_main->agentnum),
515                                      )
516                                  )
517                      );
518
519   } else {
520
521     my $warning = "Couldn't find a return address; ".
522                   "do you need to set the company_address configuration value?";
523     warn "$warning\n";
524     $returnaddress = $nbsp;
525     #$returnaddress = $warning;
526
527   }
528
529   warn "$me generating invoice data\n"
530     if $DEBUG > 1;
531
532   my $agentnum = $cust_main->agentnum;
533
534   my %invoice_data = (
535
536     #invoice from info
537     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
538     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
539     'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
540     'returnaddress'   => $returnaddress,
541     'agent'           => &$escape_function($cust_main->agent->agent),
542
543     #invoice/quotation info
544     'no_number'       => $params{'no_number'},
545     'invnum'          => ( $params{'no_number'} ? '' : $self->invnum ),
546     'quotationnum'    => $self->quotationnum,
547     'no_date'         => $params{'no_date'},
548     '_date'           => ( $params{'no_date'} ? '' : $self->_date ),
549       # workaround for inconsistent behavior in the early plain text 
550       # templates; see RT#28271
551     'date'            => ( $params{'no_date'}
552                              ? ''
553                              : ($format eq 'template'
554                                ? $self->_date
555                                : $self->time2str_local('long', $self->_date, $format)
556                                )
557                          ),
558     'today'           => $self->time2str_local('long', $today, $format),
559     'terms'           => $self->terms,
560     'template'        => $template, #params{'template'},
561     'notice_name'     => $notice_name, # escape?
562     'current_charges' => sprintf("%.2f", $self->charged),
563     'duedate'         => $self->due_date2str('rdate'), #date_format?
564
565     #customer info
566     'custnum'         => $cust_main->display_custnum,
567     'prospectnum'     => $cust_main->prospectnum,
568     'agent_custid'    => &$escape_function($cust_main->agent_custid),
569     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
570       payname company address1 address2 city state zip fax
571     )),
572
573     #global config
574     'ship_enable'     => $cust_main->invoice_ship_address || $conf->exists('invoice-ship_address'),
575     'unitprices'      => $conf->exists('invoice-unitprice'),
576     'smallernotes'    => $conf->exists('invoice-smallernotes'),
577     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
578     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
579    
580     #layout info -- would be fancy to calc some of this and bury the template
581     #               here in the code
582     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
583     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
584     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
585     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
586     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
587     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
588     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
589     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
590     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
591     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
592
593     # better hang on to conf_dir for a while (for old templates)
594     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
595
596     #these are only used when doing paged plaintext
597     'page'            => 1,
598     'total_pages'     => 1,
599
600   );
601  
602   #localization
603   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
604   # prototype here to silence warnings
605   $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
606
607   my $min_sdate = 999999999999;
608   my $max_edate = 0;
609   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
610     next unless $cust_bill_pkg->pkgnum > 0;
611     $min_sdate = $cust_bill_pkg->sdate
612       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
613     $max_edate = $cust_bill_pkg->edate
614       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
615   }
616
617   $invoice_data{'bill_period'} = '';
618   $invoice_data{'bill_period'} =
619       $self->time2str_local('%e %h', $min_sdate, $format) 
620       . " to " .
621       $self->time2str_local('%e %h', $max_edate, $format)
622     if ($max_edate != 0 && $min_sdate != 999999999999);
623
624   $invoice_data{finance_section} = '';
625   if ( $conf->config('finance_pkgclass') ) {
626     my $pkg_class =
627       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
628     $invoice_data{finance_section} = $pkg_class->categoryname;
629   } 
630   $invoice_data{finance_amount} = '0.00';
631   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
632
633   my $countrydefault = $conf->config('countrydefault') || 'US';
634   foreach ( qw( address1 address2 city state zip country fax) ){
635     my $method = 'ship_'.$_;
636     $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
637   }
638   if ( length($cust_main->ship_company) ) {
639     $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
640   } else {
641     $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
642   }
643   $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
644   $invoice_data{'ship_country'} = ''
645     if ( $invoice_data{'ship_country'} eq $countrydefault );
646   
647   $invoice_data{'cid'} = $params{'cid'}
648     if $params{'cid'};
649
650   if ( $cust_main->country eq $countrydefault ) {
651     $invoice_data{'country'} = '';
652   } else {
653     $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
654   }
655
656   my @address = ();
657   $invoice_data{'address'} = \@address;
658   push @address,
659     $cust_main->payname.
660       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
661         ? " (P.O. #". $cust_main->payinfo. ")"
662         : ''
663       )
664   ;
665   push @address, $cust_main->company
666     if $cust_main->company;
667   push @address, $cust_main->address1;
668   push @address, $cust_main->address2
669     if $cust_main->address2;
670   push @address,
671     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
672   push @address, $invoice_data{'country'}
673     if $invoice_data{'country'};
674   push @address, ''
675     while (scalar(@address) < 5);
676
677   $invoice_data{'logo_file'} = $params{'logo_file'}
678     if $params{'logo_file'};
679   $invoice_data{'barcode_file'} = $params{'barcode_file'}
680     if $params{'barcode_file'};
681   $invoice_data{'barcode_img'} = $params{'barcode_img'}
682     if $params{'barcode_img'};
683   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
684     if $params{'barcode_cid'};
685
686   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
687 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
688   #my $balance_due = $self->owed + $pr_total - $cr_total;
689   my $balance_due = $self->owed;
690   if ( $self->enable_previous ) {
691     $balance_due += $pr_total;
692   }
693   # otherwise the previous balance is not shown, so including it in the
694   # balance due is just confusing
695
696   # the sum of amount owed on all invoices
697   # (this is used in the summary & on the payment coupon)
698   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
699
700   # flag telling this invoice to have a first-page summary
701   my $summarypage = '';
702
703   if ( $self->custnum && $self->invnum ) {
704     # XXX should be an FS::cust_bill method to set the defaults, instead
705     # of checking the type here
706
707     # info from customer's last invoice before this one, for some 
708     # summary formats
709     $invoice_data{'last_bill'} = {};
710  
711     my $last_bill = $self->previous_bill;
712     if ( $last_bill ) {
713
714       # "balance_date_range" unfortunately is unsuitable for this, since it
715       # cares about application dates.  We want to know the sum of all 
716       # _top-level transactions_ dated before the last invoice.
717       #
718       # still do this for the "Previous Balance" line of the summary block
719       my @sql =
720         map "$_ WHERE _date <= ? AND custnum = ?", (
721           "SELECT      COALESCE( SUM(charged), 0 ) FROM cust_bill",
722           "SELECT -1 * COALESCE( SUM(amount),  0 ) FROM cust_credit",
723           "SELECT -1 * COALESCE( SUM(paid),    0 ) FROM cust_pay",
724           "SELECT      COALESCE( SUM(refund),  0 ) FROM cust_refund",
725         );
726
727       # the customer's current balance immediately after generating the last 
728       # bill
729
730       my $last_bill_balance = $last_bill->charged;
731       foreach (@sql) {
732         my $delta = FS::Record->scalar_sql(
733           $_,
734           $last_bill->_date - 1,
735           $self->custnum,
736         );
737         $last_bill_balance += $delta;
738       }
739
740       $last_bill_balance = sprintf("%.2f", $last_bill_balance);
741
742       warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
743         $last_bill->invnum,
744         $self->time2str_local('%D', $last_bill->_date),
745         $last_bill_balance
746       ) if $DEBUG > 0;
747       # ("true_previous_balance" is a terrible name, but at least it's no
748       # longer stored in the database)
749       $invoice_data{'true_previous_balance'} = $last_bill_balance;
750
751       # Now, get all applications of credits/payments dated on or after the
752       # previous bill, to invoices before the current bill. (The
753       # credit/payment date restriction prevents these from intersecting
754       # the "Previous Balance" set.)
755       # These are "adjustments". The past due balance will be shown as
756       # Previous Balance - Adjustments.
757       my $adjustments = 0;
758       @sql = map {
759         "SELECT COALESCE(SUM(y.amount),0) FROM $_ JOIN cust_bill USING (invnum)
760          WHERE cust_bill._date < ?
761            AND x._date >= ?
762            AND cust_bill.custnum = ?"
763         } "cust_credit AS x JOIN cust_credit_bill y USING (crednum)",
764           "cust_pay    AS x JOIN cust_bill_pay    y USING (paynum)"
765       ;
766       foreach (@sql) {
767         my $delta = FS::Record->scalar_sql(
768           $_,
769           $self->_date,
770           $last_bill->_date,
771           $self->custnum,
772         );
773         $adjustments += $delta;
774       }
775       $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments);
776
777       warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
778                    $invoice_data{'balance_adjustments'}
779       ) if $DEBUG > 0;
780
781       # the sum of amount owed on all previous invoices
782       # ($pr_total is used elsewhere but not as $previous_balance)
783       $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
784
785       $invoice_data{'last_bill'}{'_date'} = $last_bill->_date; #unformatted
786       my (@payments, @credits);
787       # for formats that itemize previous payments
788       foreach my $cust_pay ( qsearch('cust_pay', {
789                               'custnum' => $self->custnum,
790                               '_date'   => { op => '>=',
791                                              value => $last_bill->_date }
792                              } ) )
793       {
794         next if $cust_pay->_date > $self->_date;
795         push @payments, {
796             '_date'       => $cust_pay->_date,
797             'date'        => $self->time2str_local('long', $cust_pay->_date, $format),
798             'payinfo'     => $cust_pay->payby_payinfo_pretty,
799             'amount'      => sprintf('%.2f', $cust_pay->paid),
800         };
801         # not concerned about applications
802       }
803       foreach my $cust_credit ( qsearch('cust_credit', {
804                               'custnum' => $self->custnum,
805                               '_date'   => { op => '>=',
806                                              value => $last_bill->_date }
807                              } ) )
808       {
809         next if $cust_credit->_date > $self->_date;
810         push @credits, {
811             '_date'       => $cust_credit->_date,
812             'date'        => $self->time2str_local('long', $cust_credit->_date, $format),
813             'creditreason'=> $cust_credit->reason,
814             'amount'      => sprintf('%.2f', $cust_credit->amount),
815         };
816       }
817       $invoice_data{'previous_payments'} = \@payments;
818       $invoice_data{'previous_credits'}  = \@credits;
819     } else {
820       # there is no $last_bill
821       $invoice_data{'true_previous_balance'} =
822       $invoice_data{'balance_adjustments'}   =
823       $invoice_data{'previous_balance'}      = '0.00';
824       $invoice_data{'previous_payments'} = [];
825       $invoice_data{'previous_credits'} = [];
826     }
827  
828     if ( $conf->exists('invoice_usesummary', $agentnum) ) {
829       $invoice_data{'summarypage'} = $summarypage = 1;
830     }
831
832   } # if this is an invoice
833
834   warn "$me substituting variables in notes, footer, smallfooter\n"
835     if $DEBUG > 1;
836
837   my $tc = $self->template_conf;
838   my @include = ( [ $tc,        'notes' ],
839                   [ 'invoice_', 'footer' ],
840                   [ 'invoice_', 'smallfooter', ],
841                   [ 'invoice_', 'watermark' ],
842                 );
843   push @include, [ $tc,        'coupon', ]
844     unless $params{'no_coupon'};
845
846   foreach my $i (@include) {
847
848     # load the configuration for this sub-template
849
850     my($base, $include) = @$i;
851
852     my $inc_file = $conf->key_orbase("$base$format$include", $template);
853
854     my @inc_src = $conf->config($inc_file, $agentnum);
855     if (!@inc_src) {
856       my $converter = $convert_maps{$format}{$include};
857       if ( $converter ) {
858         # then attempt to convert LaTeX to the requested format
859         $inc_file = $conf->key_orbase($base.'latex'.$include, $template);
860         @inc_src = &$converter( $conf->config($inc_file, $agentnum) );
861         foreach (@inc_src) {
862           # this isn't included in the convert_maps
863           my ($open, $close) = @{ $delimiters{$format} };
864           s/\[\@--/$open/g;
865           s/--\@\]/$close/g;
866         }
867       }
868     } # else @inc_src is empty and that's fine
869
870     # make a Text::Template out of it
871
872     my $inc_tt = new Text::Template (
873       TYPE       => 'ARRAY',
874       SOURCE     => [ map "$_\n", @inc_src ],
875       DELIMITERS => $delimiters{$format},
876     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
877
878     unless ( $inc_tt->compile() ) {
879       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
880       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
881       die $error;
882     }
883
884     # fill in variables
885
886     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
887
888     $invoice_data{$include} =~ s/\n+$//
889       if ($format eq 'latex');
890   }
891
892   # let invoices use either of these as needed
893   $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
894     ? $cust_main->payinfo : '';
895   $invoice_data{'po_line'} = 
896     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
897       ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
898       : $nbsp;
899
900   my %money_chars = ( 'latex'    => '',
901                       'html'     => $conf->config('money_char') || '$',
902                       'template' => '',
903                     );
904   my $money_char = $money_chars{$format};
905
906   # extremely dubious
907   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
908                             'html'     => $conf->config('money_char') || '$',
909                             'template' => '',
910                           );
911   my $other_money_char = $other_money_chars{$format};
912   $invoice_data{'dollar'} = $other_money_char;
913
914   my %minus_signs = ( 'latex'    => '$-$',
915                       'html'     => '&minus;',
916                       'template' => '- ' );
917   my $minus = $minus_signs{$format};
918
919   my @detail_items = ();
920   my @total_items = ();
921   my @buf = ();
922   my @sections = ();
923
924   $invoice_data{'detail_items'} = \@detail_items;
925   $invoice_data{'total_items'} = \@total_items;
926   $invoice_data{'buf'} = \@buf;
927   $invoice_data{'sections'} = \@sections;
928
929   warn "$me generating sections\n"
930     if $DEBUG > 1;
931
932   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
933   my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
934                      $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
935   $invoice_data{'multisection'} = $multisection;
936   my $late_sections;
937   my $extra_sections = [];
938   my $extra_lines = ();
939
940   # default section ('Charges')
941   my $default_section = { 'description' => '',
942                           'subtotal'    => '', 
943                           'no_subtotal' => 1,
944                         };
945
946   # Previous Charges section
947   # subtotal is the first return value from $self->previous
948   my $previous_section;
949   # if the invoice has major sections, or if we're summarizing previous 
950   # charges with a single line, or if we've been specifically told to put them
951   # in a section, create a section for previous charges:
952   if ( $multisection or
953        $conf->exists('previous_balance-summary_only') or
954        $conf->exists('previous_balance-section') ) {
955     
956     $previous_section =  { 'description' => $self->mt('Previous Charges'),
957                            'subtotal'    => $other_money_char.
958                                             sprintf('%.2f', $pr_total),
959                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
960                          };
961     $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
962       join(' / ', map { $cust_main->balance_date_range(@$_) }
963                   $self->_prior_month30s
964           )
965       if $conf->exists('invoice_include_aging');
966
967   } else {
968     # otherwise put them in the main section
969     $previous_section = $default_section;
970   }
971
972   my $adjust_section = {
973     'description'    => $self->mt('Credits, Payments, and Adjustments'),
974     'adjust_section' => 1,
975     'subtotal'       => 0,   # adjusted below
976   };
977   my $adjust_weight = _pkg_category($adjust_section->{description})
978                         ? _pkg_category($adjust_section->{description})->weight
979                         : 0;
980   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
981   # Note: 'sort_weight' here is actually a flag telling whether there is an
982   # explicit package category for the adjust section. If so, certain behavior
983   # happens.
984   $adjust_section->{'sort_weight'} = $adjust_weight;
985
986
987   if ( $multisection ) {
988     ($extra_sections, $extra_lines) =
989       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
990       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
991       && $self->can('_items_extra_usage_sections');
992
993     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
994
995     push @detail_items, @$extra_lines if $extra_lines;
996
997     # the code is written so that both methods can be used together, but
998     # we haven't yet changed the template to take advantage of that, so for 
999     # now, treat them as mutually exclusive.
1000     my %section_method = ( by_category => 1 );
1001     if ( $conf->config($tc.'sections_method') eq 'location' ) {
1002       %section_method = ( by_location => 1 );
1003     }
1004     my ($early, $late) =
1005       $self->_items_sections( 'summary' => $summarypage,
1006                               'escape'  => $escape_function_nonbsp,
1007                               'extra_sections' => $extra_sections,
1008                               'format'  => $format,
1009                               %section_method
1010                             );
1011     push @sections, @$early;
1012     $late_sections = $late;
1013
1014     if (    $conf->exists('svc_phone_sections')
1015          && $self->can('_items_svc_phone_sections')
1016        )
1017     {
1018       my ($phone_sections, $phone_lines) =
1019         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
1020       push @{$late_sections}, @$phone_sections;
1021       push @detail_items, @$phone_lines;
1022     }
1023     if ( $conf->exists('voip-cust_accountcode_cdr')
1024          && $cust_main->accountcode_cdr
1025          && $self->can('_items_accountcode_cdr')
1026        )
1027     {
1028       my ($accountcode_section, $accountcode_lines) =
1029         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
1030       if ( scalar(@$accountcode_lines) ) {
1031           push @{$late_sections}, $accountcode_section;
1032           push @detail_items, @$accountcode_lines;
1033       }
1034     }
1035   } else {# not multisection
1036     # make a default section
1037     push @sections, $default_section;
1038     # and calculate the finance charge total, since it won't get done otherwise.
1039     # and the default section total
1040     # XXX possibly finance_pkgclass should not be used in this manner?
1041     my @finance_charges;
1042     my @charges;
1043     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1044       if ( $invoice_data{finance_section} and 
1045         grep { $_->section eq $invoice_data{finance_section} }
1046            $cust_bill_pkg->cust_bill_pkg_display ) {
1047         # I think these are always setup fees, but just to be sure...
1048         push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1049       } else {
1050         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1051       }
1052     }
1053     $invoice_data{finance_amount} = 
1054       sprintf('%.2f', sum( @finance_charges ) || 0);
1055     $default_section->{subtotal} = $other_money_char.
1056                                     sprintf('%.2f', sum( @charges ) || 0);
1057   }
1058
1059   # start setting up summary subtotals
1060   my @summary_subtotals;
1061   my $method = $conf->config('summary_subtotals_method');
1062   if ( $method and $method ne $conf->config($tc.'sections_method') ) {
1063     # then re-section them by the correct method
1064     my %section_method = ( by_category => 1 );
1065     if ( $conf->config('summary_subtotals_method') eq 'location' ) {
1066       %section_method = ( by_location => 1 );
1067     }
1068     my ($early, $late) =
1069       $self->_items_sections( 'summary' => $summarypage,
1070                               'escape'  => $escape_function_nonbsp,
1071                               'extra_sections' => $extra_sections,
1072                               'format'  => $format,
1073                               %section_method
1074                             );
1075     foreach ( @$early ) {
1076       next if $_->{subtotal} == 0;
1077       $_->{subtotal} = $other_money_char.sprintf('%.2f', $_->{subtotal});
1078       push @summary_subtotals, $_;
1079     }
1080   } else {
1081     # subtotal sectioning is the same as for the actual invoice sections
1082     @summary_subtotals = @sections;
1083   }
1084
1085   # Hereafter, push sections to both @sections and @summary_subtotals
1086   # if they belong in both places (e.g. tax section).  Late sections are
1087   # never in @summary_subtotals.
1088
1089   # previous invoice balances in the Previous Charges section if there
1090   # is one, otherwise in the main detail section
1091   # (except if summary_only is enabled, don't show them at all)
1092   if ( $self->can('_items_previous') &&
1093        $self->enable_previous &&
1094        ! $conf->exists('previous_balance-summary_only') ) {
1095
1096     warn "$me adding previous balances\n"
1097       if $DEBUG > 1;
1098
1099     foreach my $line_item ( $self->_items_previous ) {
1100
1101       my $detail = {
1102         ref             => $line_item->{'pkgnum'},
1103         pkgpart         => $line_item->{'pkgpart'},
1104         #quantity        => 1, # not really correct
1105         section         => $previous_section, # which might be $default_section
1106         description     => &$escape_function($line_item->{'description'}),
1107         ext_description => [ map { &$escape_function($_) } 
1108                              @{ $line_item->{'ext_description'} || [] }
1109                            ],
1110         amount          => $money_char . $line_item->{'amount'},
1111         product_code    => $line_item->{'pkgpart'} || 'N/A',
1112       };
1113
1114       push @detail_items, $detail;
1115       push @buf, [ $detail->{'description'},
1116                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1117                  ];
1118     }
1119
1120   }
1121
1122   if ( @pr_cust_bill && $self->enable_previous ) {
1123     push @buf, ['','-----------'];
1124     push @buf, [ $self->mt('Total Previous Balance'),
1125                  $money_char. sprintf("%10.2f", $pr_total) ];
1126     push @buf, ['',''];
1127   }
1128  
1129   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
1130       warn "$me adding DID summary\n"
1131         if $DEBUG > 1;
1132
1133       my ($didsummary,$minutes) = $self->_did_summary;
1134       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
1135       push @detail_items, 
1136        { 'description' => $didsummary_desc,
1137            'ext_description' => [ $didsummary, $minutes ],
1138        };
1139   }
1140
1141   foreach my $section (@sections, @$late_sections) {
1142
1143     # begin some normalization
1144     $section->{'subtotal'} = $section->{'amount'}
1145       if $multisection
1146          && !exists($section->{subtotal})
1147          && exists($section->{amount});
1148
1149     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1150       if ( $invoice_data{finance_section} &&
1151            $section->{'description'} eq $invoice_data{finance_section} );
1152
1153     $section->{'subtotal'} = $other_money_char.
1154                              sprintf('%.2f', $section->{'subtotal'})
1155       if $multisection;
1156
1157     # continue some normalization
1158     $section->{'amount'}   = $section->{'subtotal'}
1159       if $multisection;
1160
1161
1162     if ( $section->{'description'} ) {
1163       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1164                    [ '', '' ],
1165                  );
1166     }
1167
1168     warn "$me   setting options\n"
1169       if $DEBUG > 1;
1170
1171     my %options = ();
1172     $options{'section'} = $section if $multisection;
1173     $options{'format'} = $format;
1174     $options{'escape_function'} = $escape_function;
1175     $options{'no_usage'} = 1 unless $unsquelched;
1176     $options{'unsquelched'} = $unsquelched;
1177     $options{'summary_page'} = $summarypage;
1178     $options{'skip_usage'} =
1179       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1180     $options{'preref_callback'} = $params{'preref_callback'};
1181
1182     warn "$me   searching for line items\n"
1183       if $DEBUG > 1;
1184
1185     foreach my $line_item ( $self->_items_pkg(%options),
1186                             $self->_items_fee(%options) ) {
1187
1188       warn "$me     adding line item ".
1189            join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
1190         if $DEBUG > 1;
1191
1192       push @buf, ( [ $line_item->{'description'},
1193                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1194                    ],
1195                    map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1196                  );
1197
1198       $line_item->{'ref'} = $line_item->{'pkgnum'};
1199       $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1200       $line_item->{'section'} = $section;
1201       $line_item->{'description'} = &$escape_function($line_item->{'description'});
1202       $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1203
1204       if ( length($line_item->{'unit_amount'}) ) {
1205         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1206       }
1207       $line_item->{'ext_description'} ||= [];
1208  
1209       push @detail_items, $line_item;
1210     }
1211
1212     if ( $section->{'description'} ) {
1213       push @buf, ( ['','-----------'],
1214                    [ $section->{'description'}. ' sub-total',
1215                       $section->{'subtotal'} # already formatted this 
1216                    ],
1217                    [ '', '' ],
1218                    [ '', '' ],
1219                  );
1220     }
1221   
1222   }
1223
1224   $invoice_data{current_less_finance} =
1225     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1226
1227   # if there's anything in the Previous Charges section, prepend it to the list
1228   if ( $pr_total and $previous_section ne $default_section ) {
1229     unshift @sections, $previous_section;
1230     # but not @summary_subtotals
1231   }
1232
1233   warn "$me adding taxes\n"
1234     if $DEBUG > 1;
1235
1236   # create a tax section if we don't yet have one
1237   my $tax_description = 'Taxes, Surcharges, and Fees';
1238   my $tax_section =
1239     List::Util::first { $_->{description} eq $tax_description } @sections;
1240   if (!$tax_section) {
1241     $tax_section = { 'description' => $tax_description };
1242     push @sections, $tax_section if $multisection;
1243   }
1244   $tax_section->{tax_section} = 1; # mark this section as containing taxes
1245   # if this is an existing tax section, we're merging the tax items into it.
1246   # grab the taxtotal that's already there, strip the money symbol if any
1247   my $taxtotal = $tax_section->{'subtotal'} || 0;
1248   $taxtotal =~ s/^\Q$other_money_char\E//;
1249
1250   # this does nothing
1251   #my $tax_weight = _pkg_category($tax_section->{description})
1252   #                      ? _pkg_category($tax_section->{description})->weight
1253   #                      : 0;
1254   #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
1255   #$tax_section->{'sort_weight'} = $tax_weight;
1256
1257   my @items_tax = $self->_items_tax;
1258   foreach my $tax ( @items_tax ) {
1259
1260     $taxtotal += $tax->{'amount'};
1261
1262     my $description = &$escape_function( $tax->{'description'} );
1263     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
1264
1265     if ( $multisection ) {
1266
1267       push @detail_items, {
1268         ext_description => [],
1269         ref          => '',
1270         quantity     => '',
1271         description  => $description,
1272         amount       => $money_char. $amount,
1273         product_code => '',
1274         section      => $tax_section,
1275       };
1276
1277     } else {
1278
1279       push @total_items, {
1280         'total_item'   => $description,
1281         'total_amount' => $other_money_char. $amount,
1282       };
1283
1284     }
1285
1286     push @buf,[ $description,
1287                 $money_char. $amount,
1288               ];
1289
1290   }
1291  
1292   if ( @items_tax ) {
1293     my $total = {};
1294     $total->{'total_item'} = $self->mt('Sub-total');
1295     $total->{'total_amount'} =
1296       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1297
1298     if ( $multisection ) {
1299       if ( $taxtotal > 0 ) {
1300         # there are taxes, so prepare the section to be displayed.
1301         # $taxtotal already includes any line items that were already in the
1302         # section (fees, taxes that are charged as packages for some reason).
1303         # also set 'summarized' to false so that this isn't a summary-only
1304         # section.
1305         $tax_section->{'subtotal'} = $other_money_char.
1306                                      sprintf('%.2f', $taxtotal);
1307         $tax_section->{'pretotal'} = 'New charges sub-total '.
1308                                      $total->{'total_amount'};
1309         $tax_section->{'description'} = $self->mt($tax_description);
1310         $tax_section->{'summarized'} = '';
1311
1312         # append it if it's not already there
1313         if ( !grep $tax_section, @sections ) {
1314           push @sections, $tax_section;
1315           push @summary_subtotals, $tax_section;
1316         }
1317       }
1318
1319     } else {
1320       unshift @total_items, $total;
1321     }
1322   }
1323   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1324
1325   ###
1326   # Totals
1327   ###
1328
1329   my %embolden_functions = (
1330     'latex'    => sub { return '\textbf{'. shift(). '}' },
1331     'html'     => sub { return '<b>'. shift(). '</b>' },
1332     'template' => sub { shift },
1333   );
1334   my $embolden_function = $embolden_functions{$format};
1335
1336   if ( $multisection ) {
1337
1338     if ( $adjust_section->{'sort_weight'} ) {
1339       $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1340         $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
1341     } else{
1342       $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1343         $other_money_char.  sprintf('%.2f', $self->charged );
1344     }
1345
1346   }
1347   
1348   if ( $self->can('_items_total') ) { # should always be true now
1349
1350     # even for multisection, need plain text version
1351
1352     my @new_total_items = $self->_items_total;
1353
1354     push @buf,['','-----------'];
1355
1356     foreach ( @new_total_items ) {
1357       my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'});
1358       $_->{'total_item'}   = &$embolden_function( $item );
1359       $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
1360       # but if it's multisection, don't append to @total_items. the adjust
1361       # section has all this stuff
1362       push @total_items, $_ if !$multisection;
1363       push @buf, [ $item, $money_char.sprintf('%10.2f',$amount) ];
1364     }
1365
1366     push @buf, [ '', '' ];
1367
1368     # if we're showing previous invoices, also show previous
1369     # credits and payments 
1370     if ( $self->enable_previous 
1371           and $self->can('_items_credits')
1372           and $self->can('_items_payments') )
1373       {
1374     
1375       # credits
1376       my $credittotal = 0;
1377       foreach my $credit (
1378         $self->_items_credits( 'template' => $template, 'trim_len' => 40 )
1379       ) {
1380
1381         my $total;
1382         $total->{'total_item'} = &$escape_function($credit->{'description'});
1383         $credittotal += $credit->{'amount'};
1384         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1385         if ( $multisection ) {
1386           push @detail_items, {
1387             ext_description => [],
1388             ref          => '',
1389             quantity     => '',
1390             description  => &$escape_function($credit->{'description'}),
1391             amount       => $money_char . $credit->{'amount'},
1392             product_code => '',
1393             section      => $adjust_section,
1394           };
1395         } else {
1396           push @total_items, $total;
1397         }
1398
1399       }
1400       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1401
1402       #credits (again)
1403       foreach my $credit (
1404         $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1405       ) {
1406         push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1407       }
1408
1409       # payments
1410       my $paymenttotal = 0;
1411       foreach my $payment (
1412         $self->_items_payments( 'template' => $template )
1413       ) {
1414         my $total = {};
1415         $total->{'total_item'} = &$escape_function($payment->{'description'});
1416         $paymenttotal += $payment->{'amount'};
1417         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1418         if ( $multisection ) {
1419           push @detail_items, {
1420             ext_description => [],
1421             ref          => '',
1422             quantity     => '',
1423             description  => &$escape_function($payment->{'description'}),
1424             amount       => $money_char . $payment->{'amount'},
1425             product_code => '',
1426             section      => $adjust_section,
1427           };
1428         }else{
1429           push @total_items, $total;
1430         }
1431         push @buf, [ $payment->{'description'},
1432                      $money_char. sprintf("%10.2f", $payment->{'amount'}),
1433                    ];
1434       }
1435       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1436     
1437       if ( $multisection ) {
1438         $adjust_section->{'subtotal'} = $other_money_char.
1439                                         sprintf('%.2f', $credittotal + $paymenttotal);
1440
1441         #why this? because {sort_weight} forces the adjust_section to appear
1442         #in @extra_sections instead of @sections. obviously.
1443         push @sections, $adjust_section
1444           unless $adjust_section->{sort_weight};
1445         # do not summarize; adjustments there are shown according to 
1446         # different rules
1447       }
1448
1449       # create Balance Due message
1450       { 
1451         my $total;
1452         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1453         $total->{'total_amount'} =
1454           &$embolden_function(
1455             $other_money_char. sprintf('%.2f', #why? $summarypage 
1456                                                #  ? $self->charged +
1457                                                #    $self->billing_balance
1458                                                #  :
1459                                                    $self->owed + $pr_total
1460                                       )
1461           );
1462         if ( $multisection && !$adjust_section->{sort_weight} ) {
1463           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1464                                            $total->{'total_amount'};
1465         } else {
1466           push @total_items, $total;
1467         }
1468         push @buf,['','-----------'];
1469         push @buf,[$self->balance_due_msg, $money_char. 
1470           sprintf("%10.2f", $balance_due ) ];
1471       }
1472
1473       if ( $conf->exists('previous_balance-show_credit')
1474           and $cust_main->balance < 0 ) {
1475         my $credit_total = {
1476           'total_item'    => &$embolden_function($self->credit_balance_msg),
1477           'total_amount'  => &$embolden_function(
1478             $other_money_char. sprintf('%.2f', -$cust_main->balance)
1479           ),
1480         };
1481         if ( $multisection ) {
1482           $adjust_section->{'posttotal'} .= $newline_token .
1483             $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1484         }
1485         else {
1486           push @total_items, $credit_total;
1487         }
1488         push @buf,['','-----------'];
1489         push @buf,[$self->credit_balance_msg, $money_char. 
1490           sprintf("%10.2f", -$cust_main->balance ) ];
1491       }
1492     }
1493
1494   } #end of default total adding ! can('_items_total')
1495
1496   if ( $multisection ) {
1497     if (    $conf->exists('svc_phone_sections')
1498          && $self->can('_items_svc_phone_sections')
1499        )
1500     {
1501       my $total;
1502       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1503       $total->{'total_amount'} =
1504         &$embolden_function(
1505           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1506         );
1507       my $last_section = pop @sections;
1508       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1509                                      $total->{'total_amount'};
1510       push @sections, $last_section;
1511     }
1512     push @sections, @$late_sections
1513       if $unsquelched;
1514   }
1515
1516   # make a discounts-available section, even without multisection
1517   if ( $conf->exists('discount-show_available') 
1518        and my @discounts_avail = $self->_items_discounts_avail ) {
1519     my $discount_section = {
1520       'description' => $self->mt('Discounts Available'),
1521       'subtotal'    => '',
1522       'no_subtotal' => 1,
1523     };
1524
1525     push @sections, $discount_section; # do not summarize
1526     push @detail_items, map { +{
1527         'ref'         => '', #should this be something else?
1528         'section'     => $discount_section,
1529         'description' => &$escape_function( $_->{description} ),
1530         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1531         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1532     } } @discounts_avail;
1533   }
1534
1535   # not adding any more sections after this
1536   $invoice_data{summary_subtotals} = \@summary_subtotals;
1537
1538   # usage subtotals
1539   if ( $conf->exists('usage_class_summary')
1540        and $self->can('_items_usage_class_summary') ) {
1541     my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char);
1542     if ( @usage_subtotals ) {
1543       unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1544       unshift @detail_items, @usage_subtotals;
1545     }
1546   }
1547
1548   # invoice history "section" (not really a section)
1549   # not to be included in any subtotals, completely independent of 
1550   # everything...
1551   if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
1552     my %history;
1553     my %monthorder;
1554     foreach my $cust_bill ( $cust_main->cust_bill ) {
1555       # XXX hardcoded format, and currently only 'charged'; add other fields
1556       # if they become necessary
1557       my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1558       $history{$date} ||= 0;
1559       $history{$date} += $cust_bill->charged;
1560       # just so we have a numeric sort key
1561       $monthorder{$date} ||= $cust_bill->_date;
1562     }
1563     my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1564                         keys %history;
1565     my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1566     $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1567   }
1568
1569   # service locations: another option for template customization
1570   my %location_info;
1571   foreach my $item (@detail_items) {
1572     if ( $item->{locationnum} ) {
1573       $location_info{ $item->{locationnum} } ||= {
1574         FS::cust_location->by_key( $item->{locationnum} )->location_hash
1575       };
1576     }
1577   }
1578   $invoice_data{location_info} = \%location_info;
1579
1580   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1581   # the invoice variables
1582   return \%invoice_data if ( $params{'diag'} );
1583
1584   # All sections and items are built; now fill in templates.
1585   my @includelist = ();
1586   push @includelist, 'summary' if $summarypage;
1587   foreach my $include ( @includelist ) {
1588
1589     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1590     my @inc_src;
1591
1592     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1593
1594       @inc_src = $conf->config($inc_file, $agentnum);
1595
1596     } else {
1597
1598       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1599
1600       my $convert_map = $convert_maps{$format}{$include};
1601
1602       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1603                        s/--\@\]/$delimiters{$format}[1]/g;
1604                        $_;
1605                      } 
1606                  &$convert_map( $conf->config($inc_file, $agentnum) );
1607
1608     }
1609
1610     my $inc_tt = new Text::Template (
1611       TYPE       => 'ARRAY',
1612       SOURCE     => [ map "$_\n", @inc_src ],
1613       DELIMITERS => $delimiters{$format},
1614     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1615
1616     unless ( $inc_tt->compile() ) {
1617       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1618       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1619       die $error;
1620     }
1621
1622     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1623
1624     $invoice_data{$include} =~ s/\n+$//
1625       if ($format eq 'latex');
1626   }
1627
1628   $invoice_lines = 0;
1629   my $wasfunc = 0;
1630   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1631     /invoice_lines\((\d*)\)/;
1632     $invoice_lines += $1 || scalar(@buf);
1633     $wasfunc=1;
1634   }
1635   die "no invoice_lines() functions in template?"
1636     if ( $format eq 'template' && !$wasfunc );
1637
1638   if ($format eq 'template') {
1639
1640     if ( $invoice_lines ) {
1641       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1642       $invoice_data{'total_pages'}++
1643         if scalar(@buf) % $invoice_lines;
1644     }
1645
1646     #setup subroutine for the template
1647     $invoice_data{invoice_lines} = sub {
1648       my $lines = shift || scalar(@buf);
1649       map { 
1650         scalar(@buf)
1651           ? shift @buf
1652           : [ '', '' ];
1653       }
1654       ( 1 .. $lines );
1655     };
1656
1657     my $lines;
1658     my @collect;
1659     while (@buf) {
1660       push @collect, split("\n",
1661         $text_template->fill_in( HASH => \%invoice_data )
1662       );
1663       $invoice_data{'page'}++;
1664     }
1665     map "$_\n", @collect;
1666
1667   } else { # this is where we actually create the invoice
1668
1669     warn "filling in template for invoice ". $self->invnum. "\n"
1670       if $DEBUG;
1671     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1672       if $DEBUG > 1;
1673
1674     $text_template->fill_in(HASH => \%invoice_data);
1675   }
1676 }
1677
1678 sub notice_name { '('.shift->table.')'; }
1679
1680 sub template_conf { 'invoice_'; }
1681
1682 # helper routine for generating date ranges
1683 sub _prior_month30s {
1684   my $self = shift;
1685   my @ranges = (
1686    [ 1,       2592000 ], # 0-30 days ago
1687    [ 2592000, 5184000 ], # 30-60 days ago
1688    [ 5184000, 7776000 ], # 60-90 days ago
1689    [ 7776000, 0       ], # 90+   days ago
1690   );
1691
1692   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1693           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1694       ] }
1695   @ranges;
1696 }
1697
1698 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1699
1700 Returns an postscript invoice, as a scalar.
1701
1702 Options can be passed as a hashref (recommended) or as a list of time, template
1703 and then any key/value pairs for any other options.
1704
1705 I<time> an optional value used to control the printing of overdue messages.  The
1706 default is now.  It isn't the date of the invoice; that's the `_date' field.
1707 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1708 L<Time::Local> and L<Date::Parse> for conversion functions.
1709
1710 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1711
1712 =cut
1713
1714 sub print_ps {
1715   my $self = shift;
1716
1717   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1718   my $ps = generate_ps($file);
1719   unlink($logofile);
1720   unlink($barcodefile) if $barcodefile;
1721
1722   $ps;
1723 }
1724
1725 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1726
1727 Returns an PDF invoice, as a scalar.
1728
1729 Options can be passed as a hashref (recommended) or as a list of time, template
1730 and then any key/value pairs for any other options.
1731
1732 I<time> an optional value used to control the printing of overdue messages.  The
1733 default is now.  It isn't the date of the invoice; that's the `_date' field.
1734 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1735 L<Time::Local> and L<Date::Parse> for conversion functions.
1736
1737 I<template>, if specified, is the name of a suffix for alternate invoices.
1738
1739 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1740
1741 =cut
1742
1743 sub print_pdf {
1744   my $self = shift;
1745
1746   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1747   my $pdf = generate_pdf($file);
1748   unlink($logofile);
1749   unlink($barcodefile) if $barcodefile;
1750
1751   $pdf;
1752 }
1753
1754 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1755
1756 Returns an HTML invoice, as a scalar.
1757
1758 I<time> an optional value used to control the printing of overdue messages.  The
1759 default is now.  It isn't the date of the invoice; that's the `_date' field.
1760 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1761 L<Time::Local> and L<Date::Parse> for conversion functions.
1762
1763 I<template>, if specified, is the name of a suffix for alternate invoices.
1764
1765 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1766
1767 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1768 when emailing the invoice as part of a multipart/related MIME email.
1769
1770 =cut
1771
1772 sub print_html {
1773   my $self = shift;
1774   my %params;
1775   if ( ref($_[0]) ) {
1776     %params = %{ shift() }; 
1777   } else {
1778     %params = @_;
1779   }
1780   $params{'format'} = 'html';
1781   
1782   $self->print_generic( %params );
1783 }
1784
1785 # quick subroutine for print_latex
1786 #
1787 # There are ten characters that LaTeX treats as special characters, which
1788 # means that they do not simply typeset themselves: 
1789 #      # $ % & ~ _ ^ \ { }
1790 #
1791 # TeX ignores blanks following an escaped character; if you want a blank (as
1792 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1793
1794 sub _latex_escape {
1795   my $value = shift;
1796   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1797   $value =~ s/([<>])/\$$1\$/g;
1798   $value;
1799 }
1800
1801 sub _html_escape {
1802   my $value = shift;
1803   encode_entities($value);
1804   $value;
1805 }
1806
1807 sub _html_escape_nbsp {
1808   my $value = _html_escape(shift);
1809   $value =~ s/ +/&nbsp;/g;
1810   $value;
1811 }
1812
1813 #utility methods for print_*
1814
1815 sub _translate_old_latex_format {
1816   warn "_translate_old_latex_format called\n"
1817     if $DEBUG; 
1818
1819   my @template = ();
1820   while ( @_ ) {
1821     my $line = shift;
1822   
1823     if ( $line =~ /^%%Detail\s*$/ ) {
1824   
1825       push @template, q![@--!,
1826                       q!  foreach my $_tr_line (@detail_items) {!,
1827                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1828                       q!      $_tr_line->{'description'} .= !, 
1829                       q!        "\\tabularnewline\n~~".!,
1830                       q!        join( "\\tabularnewline\n~~",!,
1831                       q!          @{$_tr_line->{'ext_description'}}!,
1832                       q!        );!,
1833                       q!    }!;
1834
1835       while ( ( my $line_item_line = shift )
1836               !~ /^%%EndDetail\s*$/                            ) {
1837         $line_item_line =~ s/'/\\'/g;    # nice LTS
1838         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1839         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1840         push @template, "    \$OUT .= '$line_item_line';";
1841       }
1842
1843       push @template, '}',
1844                       '--@]';
1845       #' doh, gvim
1846     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1847
1848       push @template, '[@--',
1849                       '  foreach my $_tr_line (@total_items) {';
1850
1851       while ( ( my $total_item_line = shift )
1852               !~ /^%%EndTotalDetails\s*$/                      ) {
1853         $total_item_line =~ s/'/\\'/g;    # nice LTS
1854         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1855         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1856         push @template, "    \$OUT .= '$total_item_line';";
1857       }
1858
1859       push @template, '}',
1860                       '--@]';
1861
1862     } else {
1863       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1864       push @template, $line;  
1865     }
1866   
1867   }
1868
1869   if ($DEBUG) {
1870     warn "$_\n" foreach @template;
1871   }
1872
1873   (@template);
1874 }
1875
1876 =item terms
1877
1878 =cut
1879
1880 sub terms {
1881   my $self = shift;
1882   my $conf = $self->conf;
1883
1884   #check for an invoice-specific override
1885   return $self->invoice_terms if $self->invoice_terms;
1886   
1887   #check for a customer- specific override
1888   my $cust_main = $self->cust_main;
1889   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1890
1891   my $agentnum = '';
1892   if ( $cust_main ) {
1893     $agentnum = $cust_main->agentnum;
1894   } elsif ( my $prospect_main = $self->prospect_main ) {
1895     $agentnum = $prospect_main->agentnum;
1896   }
1897
1898   #use configured default
1899   $conf->config('invoice_default_terms', $agentnum) || '';
1900 }
1901
1902 =item due_date
1903
1904 =cut
1905
1906 sub due_date {
1907   my $self = shift;
1908   my $duedate = '';
1909   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1910     $duedate = $self->_date() + ( $1 * 86400 );
1911   }
1912   $duedate;
1913 }
1914
1915 =item due_date2str
1916
1917 =cut
1918
1919 sub due_date2str {
1920   my $self = shift;
1921   $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1922 }
1923
1924 =item balance_due_msg
1925
1926 =cut
1927
1928 sub balance_due_msg {
1929   my $self = shift;
1930   my $msg = $self->mt('Balance Due');
1931   return $msg unless $self->terms; # huh?
1932   if ( !$self->conf->exists('invoice_show_prior_due_date')
1933        or $self->conf->exists('invoice_sections') ) {
1934     # if enabled, the due date is shown with Total New Charges (see 
1935     # _items_total) and not here
1936     # (yes, or if invoice_sections is enabled; this is just for compatibility)
1937     if ( $self->due_date ) {
1938       $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1939         $self->due_date2str('short');
1940     } elsif ( $self->terms ) {
1941       $msg .= ' - '. $self->mt($self->terms);
1942     }
1943   }
1944   $msg;
1945 }
1946
1947 =item balance_due_date
1948
1949 =cut
1950
1951 sub balance_due_date {
1952   my $self = shift;
1953   my $conf = $self->conf;
1954   my $duedate = '';
1955   my $terms = $self->terms;
1956   if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1957     $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1958   }
1959   $duedate;
1960 }
1961
1962 sub credit_balance_msg { 
1963   my $self = shift;
1964   $self->mt('Credit Balance Remaining')
1965 }
1966
1967 =item _date_pretty
1968
1969 Returns a string with the date, for example: "3/20/2008", localized for the
1970 customer.  Use _date_pretty_unlocalized for non-end-customer display use.
1971
1972 =cut
1973
1974 sub _date_pretty {
1975   my $self = shift;
1976   $self->time2str_local('short', $self->_date);
1977 }
1978
1979 =item _date_pretty_unlocalized
1980
1981 Returns a string with the date, for example: "3/20/2008", in the format
1982 configured for the back-office.  Use _date_pretty for end-customer display use.
1983
1984 =cut
1985
1986 sub _date_pretty_unlocalized {
1987   my $self = shift;
1988   time2str($date_format, $self->_date);
1989 }
1990
1991 =item email HASHREF
1992
1993 Emails this template.
1994
1995 Options are passed as a hashref.  Available options:
1996
1997 =over 4
1998
1999 =item from
2000
2001 If specified, overrides the default From: address.
2002
2003 =item notice_name
2004
2005 If specified, overrides the name of the sent document ("Invoice" or "Quotation")
2006
2007 =item template
2008
2009 (Deprecated) If specified, is the name of a suffix for alternate template files.
2010
2011 =back
2012
2013 Options accepted by generate_email can also be used.
2014
2015 =cut
2016
2017 sub email {
2018   my $self = shift;
2019   my $opt = shift || {};
2020   if ($opt and !ref($opt)) {
2021     die ref($self). '->email called with positional parameters';
2022   }
2023
2024   return if $self->hide;
2025
2026   my $error = send_email(
2027     $self->generate_email(
2028       'subject'     => $self->email_subject($opt->{template}),
2029       %$opt, # template, etc.
2030     )
2031   );
2032
2033   die "can't email: $error\n" if $error;
2034 }
2035
2036 =item generate_email OPTION => VALUE ...
2037
2038 Options:
2039
2040 =over 4
2041
2042 =item from
2043
2044 sender address, required
2045
2046 =item template
2047
2048 alternate template name, optional
2049
2050 =item subject
2051
2052 email subject, optional
2053
2054 =item notice_name
2055
2056 notice name instead of "Invoice", optional
2057
2058 =back
2059
2060 Returns an argument list to be passed to L<FS::Misc::send_email>.
2061
2062 =cut
2063
2064 use MIME::Entity;
2065
2066 sub generate_email {
2067
2068   my $self = shift;
2069   my %args = @_;
2070   my $conf = $self->conf;
2071
2072   my $me = '[FS::Template_Mixin::generate_email]';
2073
2074   my %return = (
2075     'from'      => $args{'from'},
2076     'subject'   => ($args{'subject'} || $self->email_subject),
2077     'custnum'   => $self->custnum,
2078     'msgtype'   => 'invoice',
2079   );
2080
2081   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
2082
2083   my $cust_main = $self->cust_main;
2084
2085   if (ref($args{'to'}) eq 'ARRAY') {
2086     $return{'to'} = $args{'to'};
2087   } elsif ( $cust_main ) {
2088     $return{'to'} = [ $cust_main->invoicing_list_emailonly ];
2089   }
2090
2091   my $tc = $self->template_conf;
2092
2093   my @text; # array of lines
2094   my $html; # a big string
2095   my @related_parts; # will contain the text/HTML alternative, and images
2096   my $related; # will contain the multipart/related object
2097
2098   if ( $conf->exists($tc. 'email_pdf') ) {
2099     if ( my $msgnum = $conf->config($tc.'email_pdf_msgnum') ) {
2100
2101       warn "$me using '${tc}email_pdf_msgnum' in multipart message"
2102         if $DEBUG;
2103
2104       my $msg_template = FS::msg_template->by_key($msgnum)
2105         or die "${tc}email_pdf_msgnum $msgnum not found\n";
2106       my %prepared = $msg_template->prepare(
2107         cust_main => $self->cust_main,
2108         object    => $self
2109       );
2110
2111       @text = split(/(?=\n)/, $prepared{'text_body'});
2112       $html = $prepared{'html_body'};
2113
2114     } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) {
2115
2116       warn "$me using '${tc}email_pdf_note' in multipart message"
2117         if $DEBUG;
2118       @text = $conf->config($tc.'email_pdf_note');
2119       $html = join('<BR>', @text);
2120   
2121     } # else use the plain text invoice
2122   }
2123
2124   if (!@text) {
2125
2126     if ( $conf->config($tc.'template') ) {
2127
2128       warn "$me generating plain text invoice"
2129         if $DEBUG;
2130
2131       # 'print_text' argument is no longer used
2132       @text = $self->print_text(\%args);
2133
2134     } else {
2135
2136       warn "$me no plain text version exists; sending empty message body"
2137         if $DEBUG;
2138
2139     }
2140
2141   }
2142
2143   my $text_part = build MIME::Entity (
2144     'Type'        => 'text/plain',
2145     'Encoding'    => 'quoted-printable',
2146     'Charset'     => 'UTF-8',
2147     #'Encoding'    => '7bit',
2148     'Data'        => \@text,
2149     'Disposition' => 'inline',
2150   );
2151
2152   if (!$html) {
2153
2154     if ( $conf->exists($tc.'html') ) {
2155       warn "$me generating HTML invoice"
2156         if $DEBUG;
2157
2158       $args{'from'} =~ /\@([\w\.\-]+)/;
2159       my $from = $1 || 'example.com';
2160       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2161
2162       my $logo;
2163       my $agentnum = $cust_main ? $cust_main->agentnum
2164                                 : $self->prospect_main->agentnum;
2165       if ( defined($args{'template'}) && length($args{'template'})
2166            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
2167          )
2168       {
2169         $logo = 'logo_'. $args{'template'}. '.png';
2170       } else {
2171         $logo = "logo.png";
2172       }
2173       my $image_data = $conf->config_binary( $logo, $agentnum);
2174
2175       push @related_parts, build MIME::Entity
2176         'Type'       => 'image/png',
2177         'Encoding'   => 'base64',
2178         'Data'       => $image_data,
2179         'Filename'   => 'logo.png',
2180         'Content-ID' => "<$content_id>",
2181       ;
2182    
2183       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
2184         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2185         push @related_parts, build MIME::Entity
2186           'Type'       => 'image/png',
2187           'Encoding'   => 'base64',
2188           'Data'       => $self->invoice_barcode(0),
2189           'Filename'   => 'barcode.png',
2190           'Content-ID' => "<$barcode_content_id>",
2191         ;
2192         $args{'barcode_cid'} = $barcode_content_id;
2193       }
2194
2195       $html = $self->print_html({ 'cid'=>$content_id, %args });
2196     }
2197
2198   }
2199
2200   if ( $html ) {
2201
2202     warn "$me creating HTML/text multipart message"
2203       if $DEBUG;
2204
2205     $return{'nobody'} = 1;
2206
2207     my $alternative = build MIME::Entity
2208       'Type'        => 'multipart/alternative',
2209       #'Encoding'    => '7bit',
2210       'Disposition' => 'inline'
2211     ;
2212
2213     if ( @text ) {
2214       $alternative->add_part($text_part);
2215     }
2216
2217     $alternative->attach(
2218       'Type'        => 'text/html',
2219       'Encoding'    => 'quoted-printable',
2220       'Data'        => [ '<html>',
2221                          '  <head>',
2222                          '    <title>',
2223                          '      '. encode_entities($return{'subject'}), 
2224                          '    </title>',
2225                          '  </head>',
2226                          '  <body bgcolor="#e8e8e8">',
2227                          $html,
2228                          '  </body>',
2229                          '</html>',
2230                        ],
2231       'Disposition' => 'inline',
2232       #'Filename'    => 'invoice.pdf',
2233     );
2234
2235     unshift @related_parts, $alternative;
2236
2237     $related = build MIME::Entity 'Type'     => 'multipart/related',
2238                                   'Encoding' => '7bit';
2239
2240     #false laziness w/Misc::send_email
2241     $related->head->replace('Content-type',
2242       $related->mime_type.
2243       '; boundary="'. $related->head->multipart_boundary. '"'.
2244       '; type=multipart/alternative'
2245     );
2246
2247     $related->add_part($_) foreach @related_parts;
2248
2249   }
2250
2251   my @otherparts = ();
2252   if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
2253
2254     if ( $conf->config('voip-cdr_email_attach') eq 'zip' ) {
2255
2256       my $data = join('', map "$_\n",
2257                    $self->call_details(prepend_billed_number=>1)
2258                  );
2259
2260       my $zip = new Archive::Zip;
2261       my $file = $zip->addString( $data, 'usage-'.$self->invnum.'.csv' );
2262       $file->desiredCompressionMethod( COMPRESSION_DEFLATED );
2263
2264       my $zipdata = '';
2265       my $SH = IO::Scalar->new(\$zipdata);
2266       my $status = $zip->writeToFileHandle($SH);
2267       die "Error zipping CDR attachment: $!" unless $status == AZ_OK;
2268
2269       push @otherparts, build MIME::Entity
2270         'Type'        => 'application/zip',
2271         'Encoding'    => 'base64',
2272         'Data'        => $zipdata,
2273         'Disposition' => 'attachment',
2274         'Filename'    => 'usage-'. $self->invnum. '.zip',
2275       ;
2276
2277     } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
2278  
2279       push @otherparts, build MIME::Entity
2280         'Type'        => 'text/csv',
2281         'Encoding'    => '7bit',
2282         'Data'        => [ map { "$_\n" }
2283                              $self->call_details('prepend_billed_number' => 1)
2284                          ],
2285         'Disposition' => 'attachment',
2286         'Filename'    => 'usage-'. $self->invnum. '.csv',
2287       ;
2288
2289     }
2290
2291   }
2292
2293   if ( $conf->exists($tc.'email_pdf') ) {
2294
2295     #attaching pdf too:
2296     # multipart/mixed
2297     #   multipart/related
2298     #     multipart/alternative
2299     #       text/plain
2300     #       text/html
2301     #     image/png
2302     #   application/pdf
2303
2304     my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
2305     push @otherparts, $pdf;
2306   }
2307
2308   if (@otherparts) {
2309     $return{'content-type'} = 'multipart/mixed'; # of the outer container
2310     if ( $html ) {
2311       $return{'mimeparts'} = [ $related, @otherparts ];
2312       $return{'type'} = 'multipart/related'; # of the first part
2313     } else {
2314       $return{'mimeparts'} = [ $text_part, @otherparts ];
2315       $return{'type'} = 'text/plain';
2316     }
2317   } elsif ( $html ) { # no PDF or CSV, strip the outer container
2318     $return{'mimeparts'} = \@related_parts;
2319     $return{'content-type'} = 'multipart/related';
2320     $return{'type'} = 'multipart/alternative';
2321   } else { # no HTML either
2322     $return{'body'} = \@text;
2323     $return{'content-type'} = 'text/plain';
2324   }
2325
2326   %return;
2327
2328 }
2329
2330 =item mimebuild_pdf
2331
2332 Returns a list suitable for passing to MIME::Entity->build(), representing
2333 this invoice as PDF attachment.
2334
2335 =cut
2336
2337 sub mimebuild_pdf {
2338   my $self = shift;
2339   (
2340     'Type'        => 'application/pdf',
2341     'Encoding'    => 'base64',
2342     'Data'        => [ $self->print_pdf(@_) ],
2343     'Disposition' => 'attachment',
2344     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
2345   );
2346 }
2347
2348
2349
2350 use CAM::PDF;
2351 use LWP::UserAgent;
2352 use HTTP::Request::Common qw( POST );
2353 use JSON::XS;
2354 sub postal_mail_fsinc {
2355   my ( $self, %opt ) = @_;
2356
2357   my $url = 'https://ws.freeside.biz/print';
2358
2359   my $cust_main = $self->cust_main;
2360   my $agentnum = $cust_main->agentnum;
2361   my $bill_location = $cust_main->bill_location;
2362
2363   die "Extra charges for international mailing; contact support\@freeside.biz to enable\n"
2364     if $bill_location->country ne 'US';
2365
2366   my $conf = new FS::Conf;
2367
2368   my @company_address = $conf->config('company_address', $agentnum);
2369   my ( $company_address1, $company_address2, $company_city, $company_state, $company_zip );
2370   if ( $company_address[2] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2371     $company_address1 = $company_address[0];
2372     $company_address2 = $company_address[1];
2373     $company_city  = $1;
2374     $company_state = $2;
2375     $company_zip   = $3;
2376   } elsif ( $company_address[1] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2377     $company_address1 = $company_address[0];
2378     $company_address2 = '';
2379     $company_city  = $1;
2380     $company_state = $2;
2381     $company_zip   = $3;
2382   } else {
2383     die "Unparsable company_address; contact support\@freeside.biz\n";
2384   }
2385
2386   my $file = $self->print_pdf(%opt);
2387   my $pages = CAM::PDF->new($file)->numPages;
2388
2389   my $ua = LWP::UserAgent->new( 'ssl_opts' => { 'verify_hostname'=>0 });
2390   my $response = $ua->request( POST $url, [
2391     'support-key'      => scalar($conf->config('support-key')),
2392     'file'             => $file,
2393     'pages'            => $pages,
2394
2395     #from:
2396     'company_name'     => scalar( $conf->config('company_name', $agentnum) ),
2397     'company_address1' => $company_address1,
2398     'company_address2' => $company_address2,
2399     'company_city'     => $company_city,
2400     'company_state'    => $company_state,
2401     'company_zip'      => $company_zip,
2402     'company_country'  => 'US',
2403     'company_phonenum' => scalar($conf->config('company_phonenum', $agentnum)),
2404     'company_email'    => scalar($conf->config('invoice_from', $agentnum)),
2405
2406     #to:
2407     'name'             => ( $cust_main->payname
2408                               && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/
2409                                 ? $cust_main->payname
2410                                 : $cust_main->contact_firstlast
2411                           ),
2412     'company'          => $cust_main->company,
2413     'address1'         => $bill_location->address1,
2414     'address2'         => $bill_location->address2,
2415     'city'             => $bill_location->city,
2416     'state'            => $bill_location->state,
2417     'zip'              => $bill_location->zip,
2418     'country'          => $bill_location->country,
2419   ]);
2420
2421   die "Print connection error: ". $response->message. "\n"
2422     unless $response->is_success;
2423
2424   local $@;
2425   my $content = eval { decode_json($response->content) };
2426   die "Print JSON error : $@\n" if $@;
2427
2428   die $content->{error}."\n"
2429     if $content->{error};
2430 }
2431
2432 1;
2433     'address1'         => $bill_location->address1,
2434     'address2'         => $bill_location->address2,
2435     'city'             => $bill_location->city,
2436     'state'            => $bill_location->state,
2437     'zip'              => $bill_location->zip,
2438     'country'          => $bill_location->country,
2439   ]);
2440
2441   die "Print connection error: ". $response->message
2442     unless $response->is_success;
2443
2444   local $@;
2445   my $content = eval { decode_json($response->content) };
2446   die "Print JSON error : $@\n" if $@;
2447
2448   die $content->{error}."\n"
2449     if $content->{error};
2450
2451   #TODO: get some kind of letter ID back we can later retreive a status on
2452 }
2453
2454 =item _items_sections OPTIONS
2455
2456 Generate section information for all items appearing on this invoice.
2457 This will only be called for multi-section invoices.
2458
2459 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
2460 related display records (L<FS::cust_bill_pkg_display>) and organize 
2461 them into two groups ("early" and "late" according to whether they come 
2462 before or after the total), then into sections.  A subtotal is calculated 
2463 for each section.
2464
2465 Section descriptions are returned in sort weight order.  Each consists 
2466 of a hash containing:
2467
2468 description: the package category name, escaped
2469 subtotal: the total charges in that section
2470 tax_section: a flag indicating that the section contains only tax charges
2471 summarized: same as tax_section, for some reason
2472 sort_weight: the package category's sort weight
2473
2474 If 'condense' is set on the display record, it also contains everything 
2475 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
2476 coderefs to generate parts of the invoice.  This is not advised.
2477
2478 The method returns two arrayrefs, one of "early" sections and one of "late"
2479 sections.
2480
2481 OPTIONS may include:
2482
2483 by_location: a flag to divide the invoice into sections by location.  
2484 Each section hash will have a 'location' element containing a hashref of 
2485 the location fields (see L<FS::cust_location>).  The section description
2486 will be the location label, but the template can use any of the location 
2487 fields to create a suitable label.
2488
2489 by_category: a flag to divide the invoice into sections using display 
2490 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
2491 behavior.  Each section hash will have a 'category' element containing
2492 the section name from the display record (which probably equals the 
2493 category name of the package, but may not in some cases).
2494
2495 summary: a flag indicating that this is a summary-format invoice.
2496 Turning this on has the following effects:
2497 - Ignores display items with the 'summary' flag.
2498 - Places all sections in the "early" group even if they have post_total.
2499 - Creates sections for all non-disabled package categories, even if they 
2500 have no charges on this invoice, as well as a section with no name.
2501
2502 escape: an escape function to use for section titles.
2503
2504 extra_sections: an arrayref of additional sections to return after the 
2505 sorted list.  If there are any of these, section subtotals exclude 
2506 usage charges.
2507
2508 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
2509 passed through to C<_condense_section()>.
2510
2511 =cut
2512
2513 use vars qw(%pkg_category_cache);
2514 sub _items_sections {
2515   my $self = shift;
2516   my %opt = @_;
2517   
2518   my $escape = $opt{escape};
2519   my @extra_sections = @{ $opt{extra_sections} || [] };
2520
2521   # $subtotal{$locationnum}{$categoryname} = amount.
2522   # if we're not using by_location, $locationnum is undef.
2523   # if we're not using by_category, you guessed it, $categoryname is undef.
2524   # if we're not using either one, we shouldn't be here in the first place...
2525   my %subtotal = ();
2526   my %late_subtotal = ();
2527   my %not_tax = ();
2528
2529   # About tax items + multisection invoices:
2530   # If either invoice_*summary option is enabled, AND there is a 
2531   # package category with the name of the tax, then there will be 
2532   # a display record assigning the tax item to that category.
2533   #
2534   # However, the taxes are always placed in the "Taxes, Surcharges,
2535   # and Fees" section regardless of that.  The only effect of the 
2536   # display record is to create a subtotal for the summary page.
2537
2538   # cache these
2539   my $pkg_hash = $self->cust_pkg_hash;
2540
2541   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2542   {
2543
2544       my $usage = $cust_bill_pkg->usage;
2545
2546       my $locationnum;
2547       if ( $opt{by_location} ) {
2548         if ( $cust_bill_pkg->pkgnum ) {
2549           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2550         } else {
2551           $locationnum = '';
2552         }
2553       } else {
2554         $locationnum = undef;
2555       }
2556
2557       # as in _items_cust_pkg, if a line item has no display records,
2558       # cust_bill_pkg_display() returns a default record for it
2559
2560       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2561         next if ( $display->summary && $opt{summary} );
2562
2563         my $section = $display->section;
2564         my $type    = $display->type;
2565         # Set $section = undef if we're sectioning by location and this
2566         # line item _has_ a location (i.e. isn't a fee).
2567         $section = undef if $locationnum;
2568
2569         # set this flag if the section is not tax-only
2570         $not_tax{$locationnum}{$section} = 1
2571           if $cust_bill_pkg->pkgnum  or $cust_bill_pkg->feepart;
2572
2573         # there's actually a very important piece of logic buried in here:
2574         # incrementing $late_subtotal{$section} CREATES 
2575         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
2576         # to define the list of late sections, and likewise keys(%subtotal).
2577         # When _items_cust_bill_pkg is called to generate line items for 
2578         # real, it will be called with 'section' => $section for each 
2579         # of these.
2580         if ( $display->post_total && !$opt{summary} ) {
2581           if (! $type || $type eq 'S') {
2582             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2583               if $cust_bill_pkg->setup != 0
2584               || $cust_bill_pkg->setup_show_zero;
2585           }
2586
2587           if (! $type) {
2588             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2589               if $cust_bill_pkg->recur != 0
2590               || $cust_bill_pkg->recur_show_zero;
2591           }
2592
2593           if ($type && $type eq 'R') {
2594             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2595               if $cust_bill_pkg->recur != 0
2596               || $cust_bill_pkg->recur_show_zero;
2597           }
2598           
2599           if ($type && $type eq 'U') {
2600             $late_subtotal{$locationnum}{$section} += $usage
2601               unless scalar(@extra_sections);
2602           }
2603
2604         } else { # it's a pre-total (normal) section
2605
2606           # skip tax items unless they're explicitly included in a section
2607           next if $cust_bill_pkg->pkgnum == 0 and
2608                   ! $cust_bill_pkg->feepart   and
2609                   ! $section;
2610
2611           if ( $type eq 'S' ) {
2612             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2613               if $cust_bill_pkg->setup != 0
2614               || $cust_bill_pkg->setup_show_zero;
2615           } elsif ( $type eq 'R' ) {
2616             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2617               if $cust_bill_pkg->recur != 0
2618               || $cust_bill_pkg->recur_show_zero;
2619           } elsif ( $type eq 'U' ) {
2620             $subtotal{$locationnum}{$section} += $usage
2621               unless scalar(@extra_sections);
2622           } elsif ( !$type ) {
2623             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2624                                                + $cust_bill_pkg->recur;
2625           }
2626
2627         }
2628
2629       }
2630
2631   }
2632
2633   %pkg_category_cache = ();
2634
2635   # summary invoices need subtotals for all non-disabled package categories,
2636   # even if they're zero
2637   # but currently assume that there are no location sections, or at least
2638   # that the summary page doesn't care about them
2639   if ( $opt{summary} ) {
2640     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2641       $subtotal{''}{$category->categoryname} ||= 0;
2642     }
2643     $subtotal{''}{''} ||= 0;
2644   }
2645
2646   my @sections;
2647   foreach my $post_total (0,1) {
2648     my @these;
2649     my $s = $post_total ? \%late_subtotal : \%subtotal;
2650     foreach my $locationnum (keys %$s) {
2651       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2652         my $section = {
2653                         'subtotal'    => $s->{$locationnum}{$sectionname},
2654                         'sort_weight' => 0,
2655                       };
2656         if ( $locationnum ) {
2657           $section->{'locationnum'} = $locationnum;
2658           my $location = FS::cust_location->by_key($locationnum);
2659           $section->{'description'} = &{ $escape }($location->location_label);
2660           # Better ideas? This will roughly group them by proximity, 
2661           # which alpha sorting on any of the address fields won't.
2662           # Sorting by locationnum is meaningless.
2663           # We have to sort on _something_ or the order may change 
2664           # randomly from one invoice to the next, which will confuse
2665           # people.
2666           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2667                                       $locationnum;
2668           $section->{'location'} = {
2669             label_prefix => &{ $escape }($location->label_prefix),
2670             map { $_ => &{ $escape }($location->get($_)) }
2671               $location->fields
2672           };
2673         } else {
2674           $section->{'category'} = $sectionname;
2675           $section->{'description'} = &{ $escape }($sectionname);
2676           if ( _pkg_category($sectionname) ) {
2677             $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2678             if ( _pkg_category($sectionname)->condense ) {
2679               $section = { %$section, $self->_condense_section($opt{format}) };
2680             }
2681           }
2682         }
2683         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2684           # then it's a tax-only section
2685           $section->{'summarized'} = 'Y';
2686           $section->{'tax_section'} = 'Y';
2687         }
2688         push @these, $section;
2689       } # foreach $sectionname
2690     } #foreach $locationnum
2691     push @these, @extra_sections if $post_total == 0;
2692     # need an alpha sort for location sections, because postal codes can 
2693     # be non-numeric
2694     $sections[ $post_total ] = [ sort {
2695       $opt{'by_location'} ? 
2696         ($a->{sort_weight} cmp $b->{sort_weight}) :
2697         ($a->{sort_weight} <=> $b->{sort_weight})
2698       } @these ];
2699   } #foreach $post_total
2700
2701   return @sections; # early, late
2702 }
2703
2704 #helper subs for above
2705
2706 sub cust_pkg_hash {
2707   my $self = shift;
2708   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2709 }
2710
2711 sub _pkg_category {
2712   my $categoryname = shift;
2713   $pkg_category_cache{$categoryname} ||=
2714     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2715 }
2716
2717 my %condensed_format = (
2718   'label' => [ qw( Description Qty Amount ) ],
2719   'fields' => [
2720                 sub { shift->{description} },
2721                 sub { shift->{quantity} },
2722                 sub { my($href, %opt) = @_;
2723                       ($opt{dollar} || ''). $href->{amount};
2724                     },
2725               ],
2726   'align'  => [ qw( l r r ) ],
2727   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2728   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2729 );
2730
2731 sub _condense_section {
2732   my ( $self, $format ) = ( shift, shift );
2733   ( 'condensed' => 1,
2734     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2735       qw( description_generator
2736           header_generator
2737           total_generator
2738           total_line_generator
2739         )
2740   );
2741 }
2742
2743 sub _condensed_generator_defaults {
2744   my ( $self, $format ) = ( shift, shift );
2745   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2746 }
2747
2748 my %html_align = (
2749   'c' => 'center',
2750   'l' => 'left',
2751   'r' => 'right',
2752 );
2753
2754 sub _condensed_header_generator {
2755   my ( $self, $format ) = ( shift, shift );
2756
2757   my ( $f, $prefix, $suffix, $separator, $column ) =
2758     _condensed_generator_defaults($format);
2759
2760   if ($format eq 'latex') {
2761     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2762     $suffix = "\\\\\n\\hline";
2763     $separator = "&\n";
2764     $column =
2765       sub { my ($d,$a,$s,$w) = @_;
2766             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2767           };
2768   } elsif ( $format eq 'html' ) {
2769     $prefix = '<th></th>';
2770     $suffix = '';
2771     $separator = '';
2772     $column =
2773       sub { my ($d,$a,$s,$w) = @_;
2774             return qq!<th align="$html_align{$a}">$d</th>!;
2775       };
2776   }
2777
2778   sub {
2779     my @args = @_;
2780     my @result = ();
2781
2782     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2783       push @result,
2784         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2785     }
2786
2787     $prefix. join($separator, @result). $suffix;
2788   };
2789
2790 }
2791
2792 sub _condensed_description_generator {
2793   my ( $self, $format ) = ( shift, shift );
2794
2795   my ( $f, $prefix, $suffix, $separator, $column ) =
2796     _condensed_generator_defaults($format);
2797
2798   my $money_char = '$';
2799   if ($format eq 'latex') {
2800     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2801     $suffix = '\\\\';
2802     $separator = " & \n";
2803     $column =
2804       sub { my ($d,$a,$s,$w) = @_;
2805             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2806           };
2807     $money_char = '\\dollar';
2808   }elsif ( $format eq 'html' ) {
2809     $prefix = '"><td align="center"></td>';
2810     $suffix = '';
2811     $separator = '';
2812     $column =
2813       sub { my ($d,$a,$s,$w) = @_;
2814             return qq!<td align="$html_align{$a}">$d</td>!;
2815       };
2816     #$money_char = $conf->config('money_char') || '$';
2817     $money_char = '';  # this is madness
2818   }
2819
2820   sub {
2821     #my @args = @_;
2822     my $href = shift;
2823     my @result = ();
2824
2825     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2826       my $dollar = '';
2827       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2828       push @result,
2829         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2830                     map { $f->{$_}->[$i] } qw(align span width)
2831                   );
2832     }
2833
2834     $prefix. join( $separator, @result ). $suffix;
2835   };
2836
2837 }
2838
2839 sub _condensed_total_generator {
2840   my ( $self, $format ) = ( shift, shift );
2841
2842   my ( $f, $prefix, $suffix, $separator, $column ) =
2843     _condensed_generator_defaults($format);
2844   my $style = '';
2845
2846   if ($format eq 'latex') {
2847     $prefix = "& ";
2848     $suffix = "\\\\\n";
2849     $separator = " & \n";
2850     $column =
2851       sub { my ($d,$a,$s,$w) = @_;
2852             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2853           };
2854   }elsif ( $format eq 'html' ) {
2855     $prefix = '';
2856     $suffix = '';
2857     $separator = '';
2858     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2859     $column =
2860       sub { my ($d,$a,$s,$w) = @_;
2861             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2862       };
2863   }
2864
2865
2866   sub {
2867     my @args = @_;
2868     my @result = ();
2869
2870     #  my $r = &{$f->{fields}->[$i]}(@args);
2871     #  $r .= ' Total' unless $i;
2872
2873     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2874       push @result,
2875         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2876                     map { $f->{$_}->[$i] } qw(align span width)
2877                   );
2878     }
2879
2880     $prefix. join( $separator, @result ). $suffix;
2881   };
2882
2883 }
2884
2885 =item total_line_generator FORMAT
2886
2887 Returns a coderef used for generation of invoice total line items for this
2888 usage_class.  FORMAT is either html or latex
2889
2890 =cut
2891
2892 # should not be used: will have issues with hash element names (description vs
2893 # total_item and amount vs total_amount -- another array of functions?
2894
2895 sub _condensed_total_line_generator {
2896   my ( $self, $format ) = ( shift, shift );
2897
2898   my ( $f, $prefix, $suffix, $separator, $column ) =
2899     _condensed_generator_defaults($format);
2900   my $style = '';
2901
2902   if ($format eq 'latex') {
2903     $prefix = "& ";
2904     $suffix = "\\\\\n";
2905     $separator = " & \n";
2906     $column =
2907       sub { my ($d,$a,$s,$w) = @_;
2908             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2909           };
2910   }elsif ( $format eq 'html' ) {
2911     $prefix = '';
2912     $suffix = '';
2913     $separator = '';
2914     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2915     $column =
2916       sub { my ($d,$a,$s,$w) = @_;
2917             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2918       };
2919   }
2920
2921
2922   sub {
2923     my @args = @_;
2924     my @result = ();
2925
2926     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2927       push @result,
2928         &{$column}( &{$f->{fields}->[$i]}(@args),
2929                     map { $f->{$_}->[$i] } qw(align span width)
2930                   );
2931     }
2932
2933     $prefix. join( $separator, @result ). $suffix;
2934   };
2935
2936 }
2937
2938 =item _items_pkg [ OPTIONS ]
2939
2940 Return line item hashes for each package item on this invoice. Nearly 
2941 equivalent to 
2942
2943 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2944
2945 OPTIONS are passed through to _items_cust_bill_pkg, and should include
2946 'format' and 'escape_function' at minimum.
2947
2948 To produce items for a specific invoice section, OPTIONS should include
2949 'section', a hashref containing 'category' and/or 'locationnum' keys.
2950
2951 'section' may also contain a key named 'condensed'. If this is present
2952 and has a true value, _items_pkg will try to merge identical items into items
2953 with 'quantity' equal to the number of items (not the sum of their separate
2954 quantities, for some reason).
2955
2956 =cut
2957
2958 sub _items_nontax {
2959   my $self = shift;
2960   # The order of these is important.  Bundled line items will be merged into
2961   # the most recent non-hidden item, so it needs to be the one with:
2962   # - the same pkgnum
2963   # - the same start date
2964   # - no pkgpart_override
2965   #
2966   # So: sort by pkgnum,
2967   # then by sdate
2968   # then sort the base line item before any overrides
2969   # then sort hidden before non-hidden add-ons
2970   # then sort by override pkgpart (for consistency)
2971   sort { $a->pkgnum <=> $b->pkgnum        or
2972          $a->sdate  <=> $b->sdate         or
2973          ($a->pkgpart_override ? 0 : -1)  or
2974          ($b->pkgpart_override ? 0 : 1)   or
2975          $b->hidden cmp $a->hidden        or
2976          $a->pkgpart_override <=> $b->pkgpart_override
2977        }
2978   # and of course exclude taxes and fees
2979   grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2980 }
2981
2982 sub _items_fee {
2983   my $self = shift;
2984   my %options = @_;
2985   my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2986   my $escape_function = $options{escape_function};
2987
2988   my @items;
2989   foreach my $cust_bill_pkg (@cust_bill_pkg) {
2990     # cache this, so we don't look it up again in every section
2991     my $part_fee = $cust_bill_pkg->get('part_fee')
2992        || $cust_bill_pkg->part_fee;
2993     $cust_bill_pkg->set('part_fee', $part_fee);
2994     if (!$part_fee) {
2995       #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2996       warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2997       next;
2998     }
2999     if ( exists($options{section}) and exists($options{section}{category}) )
3000     {
3001       my $categoryname = $options{section}{category};
3002       # then filter for items that have that section
3003       if ( $part_fee->categoryname ne $categoryname ) {
3004         warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
3005         next;
3006       }
3007     } # otherwise include them all in the main section
3008     # XXX what to do when sectioning by location?
3009     
3010     my @ext_desc;
3011     my %base_invnums; # invnum => invoice date
3012     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
3013       if ($_->base_invnum) {
3014         # XXX what if base_bill has been voided?
3015         my $base_bill = FS::cust_bill->by_key($_->base_invnum);
3016         my $base_date = $self->time2str_local('short', $base_bill->_date)
3017           if $base_bill;
3018         $base_invnums{$_->base_invnum} = $base_date || '';
3019       }
3020     }
3021     foreach (sort keys(%base_invnums)) {
3022       next if $_ == $self->invnum;
3023       # per convention, we must escape ext_description lines
3024       push @ext_desc,
3025         &{$escape_function}(
3026           $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
3027         );
3028     }
3029     my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
3030     # but not escape the base description line
3031
3032     push @items,
3033       { feepart     => $cust_bill_pkg->feepart,
3034         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
3035         description => $desc,
3036         ext_description => \@ext_desc
3037         # sdate/edate?
3038       };
3039   }
3040   @items;
3041 }
3042
3043 sub _items_pkg {
3044   my $self = shift;
3045   my %options = @_;
3046
3047   warn "$me _items_pkg searching for all package line items\n"
3048     if $DEBUG > 1;
3049
3050   my @cust_bill_pkg = $self->_items_nontax;
3051
3052   warn "$me _items_pkg filtering line items\n"
3053     if $DEBUG > 1;
3054   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3055
3056   if ($options{section} && $options{section}->{condensed}) {
3057
3058     warn "$me _items_pkg condensing section\n"
3059       if $DEBUG > 1;
3060
3061     my %itemshash = ();
3062     local $Storable::canonical = 1;
3063     foreach ( @items ) {
3064       my $item = { %$_ };
3065       delete $item->{ref};
3066       delete $item->{ext_description};
3067       my $key = freeze($item);
3068       $itemshash{$key} ||= 0;
3069       $itemshash{$key} ++; # += $item->{quantity};
3070     }
3071     @items = sort { $a->{description} cmp $b->{description} }
3072              map { my $i = thaw($_);
3073                    $i->{quantity} = $itemshash{$_};
3074                    $i->{amount} =
3075                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3076                    $i;
3077                  }
3078              keys %itemshash;
3079   }
3080
3081   warn "$me _items_pkg returning ". scalar(@items). " items\n"
3082     if $DEBUG > 1;
3083
3084   @items;
3085 }
3086
3087 sub _taxsort {
3088   return 0 unless $a->itemdesc cmp $b->itemdesc;
3089   return -1 if $b->itemdesc eq 'Tax';
3090   return 1 if $a->itemdesc eq 'Tax';
3091   return -1 if $b->itemdesc eq 'Other surcharges';
3092   return 1 if $a->itemdesc eq 'Other surcharges';
3093   $a->itemdesc cmp $b->itemdesc;
3094 }
3095
3096 sub _items_tax {
3097   my $self = shift;
3098   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
3099     $self->cust_bill_pkg;
3100   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3101
3102   if ( $self->conf->exists('always_show_tax') ) {
3103     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
3104     if (0 == grep { $_->{description} eq $itemdesc } @items) {
3105       push @items,
3106         { 'description' => $itemdesc,
3107           'amount'      => 0.00 };
3108     }
3109   }
3110   @items;
3111 }
3112
3113 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
3114
3115 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
3116 list of hashrefs describing the line items they generate on the invoice.
3117
3118 OPTIONS may include:
3119
3120 format: the invoice format.
3121
3122 escape_function: the function used to escape strings.
3123
3124 DEPRECATED? (expensive, mostly unused?)
3125 format_function: the function used to format CDRs.
3126
3127 section: a hashref containing 'category' and/or 'locationnum'; if this 
3128 is present, only returns line items that belong to that category and/or
3129 location (whichever is defined).
3130
3131 multisection: a flag indicating that this is a multisection invoice,
3132 which does something complicated.
3133
3134 preref_callback: coderef run for each line item, code should return HTML to be
3135 displayed before that line item (quotations only)
3136
3137 Returns a list of hashrefs, each of which may contain:
3138
3139 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
3140 ext_description, which is an arrayref of detail lines to show below 
3141 the package line.
3142
3143 =cut
3144
3145 sub _items_cust_bill_pkg {
3146   my $self = shift;
3147   my $conf = $self->conf;
3148   my $cust_bill_pkgs = shift;
3149   my %opt = @_;
3150
3151   my $format = $opt{format} || '';
3152   my $escape_function = $opt{escape_function} || sub { shift };
3153   my $format_function = $opt{format_function} || '';
3154   my $no_usage = $opt{no_usage} || '';
3155   my $unsquelched = $opt{unsquelched} || ''; #unused
3156   my ($section, $locationnum, $category);
3157   if ( $opt{section} ) {
3158     $category = $opt{section}->{category};
3159     $locationnum = $opt{section}->{locationnum};
3160   }
3161   my $summary_page = $opt{summary_page} || ''; #unused
3162   my $multisection = defined($category) || defined($locationnum);
3163   # this variable is the value of the config setting, not whether it applies
3164   # to this particular line item.
3165   my $discount_show_always = $conf->exists('discount-show-always');
3166
3167   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
3168
3169   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
3170
3171   # for location labels: use default location on the invoice date
3172   my $default_locationnum;
3173   if ( $self->custnum ) {
3174     my $h_cust_main;
3175     my @h_search = FS::h_cust_main->sql_h_search($self->_date);
3176     $h_cust_main = qsearchs({
3177         'table'     => 'h_cust_main',
3178         'hashref'   => { custnum => $self->custnum },
3179         'extra_sql' => $h_search[1],
3180         'addl_from' => $h_search[3],
3181     }) || $cust_main;
3182     $default_locationnum = $h_cust_main->ship_locationnum;
3183   } elsif ( $self->prospectnum ) {
3184     my $cust_location = qsearchs('cust_location',
3185       { prospectnum => $self->prospectnum,
3186         disabled => '' });
3187     $default_locationnum = $cust_location->locationnum if $cust_location;
3188   }
3189
3190   my @b = (); # accumulator for the line item hashes that we'll return
3191   my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
3192             # the 'current' line item hashes for setup, recur, usage, discount
3193   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
3194   {
3195     # if the current line item is waiting to go out, and the one we're about
3196     # to start is not bundled, then push out the current one and start a new
3197     # one.
3198     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3199       if ( $_ && !$cust_bill_pkg->hidden ) {
3200         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
3201         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3202         if (exists($_->{unit_amount})) {
3203           $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3204         }
3205         push @b, { %$_ };
3206         # we already decided to create this display line; don't reconsider it
3207         # now.
3208         #  if $_->{amount} != 0
3209         #  || $discount_show_always
3210         #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3211         #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3212         ;
3213         $_ = undef;
3214       }
3215     }
3216
3217     if ( $locationnum ) {
3218       # this is a location section; skip packages that aren't at this
3219       # service location.
3220       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
3221       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
3222               != $locationnum;
3223     }
3224
3225     # Consider display records for this item to determine if it belongs
3226     # in this section.  Note that if there are no display records, there
3227     # will be a default pseudo-record that includes all charge types 
3228     # and has no section name.
3229     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
3230                                   ? $cust_bill_pkg->cust_bill_pkg_display
3231                                   : ( $cust_bill_pkg );
3232
3233     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
3234          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
3235       if $DEBUG > 1;
3236
3237     if ( defined($category) ) {
3238       # then this is a package category section; process all display records
3239       # that belong to this section.
3240       @cust_bill_pkg_display = grep { $_->section eq $category }
3241                                 @cust_bill_pkg_display;
3242     } else {
3243       # otherwise, process all display records that aren't usage summaries
3244       # (I don't think there should be usage summaries if you aren't using 
3245       # category sections, but this is the historical behavior)
3246       @cust_bill_pkg_display = grep { !$_->summary }
3247                                 @cust_bill_pkg_display;
3248     }
3249
3250     my $classname = ''; # package class name, will fill in later
3251
3252     foreach my $display (@cust_bill_pkg_display) {
3253
3254       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
3255            $display->billpkgdisplaynum. "\n"
3256         if $DEBUG > 1;
3257
3258       my $type = $display->type;
3259
3260       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
3261       $desc = substr($desc, 0, $maxlength). '...'
3262         if $format eq 'latex' && length($desc) > $maxlength;
3263
3264       my %details_opt = ( 'format'          => $format,
3265                           'escape_function' => $escape_function,
3266                           'format_function' => $format_function,
3267                           'no_usage'        => $opt{'no_usage'},
3268                         );
3269
3270       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
3271         # XXX this should be pulled out into quotation_pkg
3272
3273         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
3274           if $DEBUG > 1;
3275         # quotation_pkgs are never fees, so don't worry about the case where
3276         # part_pkg is undefined
3277
3278         my @details = $cust_bill_pkg->details;
3279
3280         # and I guess they're never bundled either?
3281         if ( $cust_bill_pkg->setup != 0 ) {
3282           my $description = $desc;
3283           $description .= ' Setup'
3284             if $cust_bill_pkg->recur != 0
3285             || $discount_show_always
3286             || $cust_bill_pkg->recur_show_zero;
3287           #push @b, {
3288           # keep it consistent, please
3289           $s = {
3290             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3291             'description' => $description,
3292             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3293             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
3294             'quantity'    => $cust_bill_pkg->quantity,
3295             'ext_description' => \@details,
3296             'preref_html' => ( $opt{preref_callback}
3297                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3298                                  : ''
3299                              ),
3300           };
3301         }
3302         if ( $cust_bill_pkg->recur != 0 ) {
3303           #push @b, {
3304           $r = {
3305             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3306             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
3307             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3308             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
3309             'quantity'    => $cust_bill_pkg->quantity,
3310             'ext_description' => \@details,
3311            'preref_html'  => ( $opt{preref_callback}
3312                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3313                                  : ''
3314                              ),
3315           };
3316         }
3317
3318       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
3319         # a "normal" package line item (not a quotation, not a fee, not a tax)
3320
3321         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
3322           if $DEBUG > 1;
3323  
3324         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3325         my $part_pkg = $cust_pkg->part_pkg;
3326
3327         # which pkgpart to show for display purposes?
3328         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
3329
3330         # start/end dates for invoice formats that do nonstandard 
3331         # things with them
3332         my %item_dates = ();
3333         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
3334           unless $part_pkg->option('disable_line_item_date_ranges',1);
3335
3336         # not normally used, but pass this to the template anyway
3337         $classname = $part_pkg->classname;
3338
3339         if (    (!$type || $type eq 'S')
3340              && (    $cust_bill_pkg->setup != 0
3341                   || $cust_bill_pkg->setup_show_zero
3342                   || ($discount_show_always and $cust_bill_pkg->unitsetup > 0)
3343                 )
3344            )
3345          {
3346
3347           warn "$me _items_cust_bill_pkg adding setup\n"
3348             if $DEBUG > 1;
3349
3350           # append the word 'Setup' to the setup line if there's going to be
3351           # a recur line for the same package (i.e. not a one-time charge) 
3352           my $description = $desc;
3353           $description .= ' Setup'
3354             if $cust_bill_pkg->recur != 0
3355             || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3356             || $cust_bill_pkg->recur_show_zero;
3357
3358           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3359                                                               $self->agentnum )
3360             if $part_pkg->is_prepaid #for prepaid, "display the validity period
3361                                      # triggered by the recurring charge freq
3362                                      # (RT#26274)
3363             && $cust_bill_pkg->recur == 0
3364             && ! $cust_bill_pkg->recur_show_zero;
3365
3366           my @d = ();
3367           my $svc_label;
3368
3369           # always pass the svc_label through to the template, even if 
3370           # not displaying it as an ext_description
3371           my @svc_labels = map &{$escape_function}($_),
3372                       $cust_pkg->h_labels_short($self->_date, undef, 'I');
3373
3374           $svc_label = $svc_labels[0];
3375
3376           unless ( $cust_pkg->part_pkg->hide_svc_detail
3377                 || $cust_bill_pkg->hidden )
3378           {
3379
3380             push @d, @svc_labels
3381               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3382             # show the location label if it's not the customer's default
3383             # location, and we're not grouping items by location already
3384             if ( $cust_pkg->locationnum != $default_locationnum
3385                   and !defined($locationnum) ) {
3386               my $loc = $cust_pkg->location_label;
3387               $loc = substr($loc, 0, $maxlength). '...'
3388                 if $format eq 'latex' && length($loc) > $maxlength;
3389               push @d, &{$escape_function}($loc);
3390             }
3391
3392           } #unless hiding service details
3393
3394           push @d, $cust_bill_pkg->details(%details_opt)
3395             if $cust_bill_pkg->recur == 0;
3396
3397           if ( $cust_bill_pkg->hidden ) {
3398             $s->{amount}      += $cust_bill_pkg->setup;
3399             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3400             push @{ $s->{ext_description} }, @d;
3401           } else {
3402             $s = {
3403               _is_setup       => 1,
3404               description     => $description,
3405               pkgpart         => $pkgpart,
3406               pkgnum          => $cust_bill_pkg->pkgnum,
3407               amount          => $cust_bill_pkg->setup,
3408               setup_show_zero => $cust_bill_pkg->setup_show_zero,
3409               unit_amount     => $cust_bill_pkg->unitsetup,
3410               quantity        => $cust_bill_pkg->quantity,
3411               ext_description => \@d,
3412               svc_label       => ($svc_label || ''),
3413               locationnum     => $cust_pkg->locationnum, # sure, why not?
3414             };
3415           };
3416
3417         }
3418
3419         # should we show a recur line?
3420         # if type eq 'S', then NO, because we've been told not to.
3421         # otherwise, show the recur line if:
3422         # - there's a recurring charge
3423         # - or recur_show_zero is on
3424         # - or there's a positive unitrecur (so it's been discounted to zero)
3425         #   and discount-show-always is on
3426         if (    ( !$type || $type eq 'R' || $type eq 'U' )
3427              && (
3428                      $cust_bill_pkg->recur != 0
3429                   || !defined($s)
3430                   || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3431                   || $cust_bill_pkg->recur_show_zero
3432                 )
3433            )
3434         {
3435
3436           warn "$me _items_cust_bill_pkg adding recur/usage\n"
3437             if $DEBUG > 1;
3438
3439           my $is_summary = $display->summary;
3440           my $description = $desc;
3441           if ( $type eq 'U' and defined($r) ) {
3442             # don't just show the same description as the recur line
3443             $description = $self->mt('Usage charges');
3444           }
3445
3446           my $part_pkg = $cust_pkg->part_pkg;
3447
3448           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3449                                                               $self->agentnum );
3450
3451           my @d = ();
3452           my @seconds = (); # for display of usage info
3453           my $svc_label = '';
3454
3455           #at least until cust_bill_pkg has "past" ranges in addition to
3456           #the "future" sdate/edate ones... see #3032
3457           my @dates = ( $self->_date );
3458           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3459           push @dates, $prev->sdate if $prev;
3460           push @dates, undef if !$prev;
3461
3462           my @svc_labels = map &{$escape_function}($_),
3463                       $cust_pkg->h_labels_short(@dates, 'I');
3464           $svc_label = $svc_labels[0];
3465
3466           # show service labels, unless...
3467                     # the package is set not to display them
3468           unless ( $part_pkg->hide_svc_detail
3469                     # or this is a tax-like line item
3470                 || $cust_bill_pkg->itemdesc
3471                     # or this is a hidden (bundled) line item
3472                 || $cust_bill_pkg->hidden
3473                     # or this is a usage summary line
3474                 || $is_summary && $type && $type eq 'U'
3475                     # or this is a usage line and there's a recurring line
3476                     # for the package in the same section (which will 
3477                     # have service labels already)
3478                 || ($type eq 'U' and defined($r))
3479               )
3480           {
3481
3482             warn "$me _items_cust_bill_pkg adding service details\n"
3483               if $DEBUG > 1;
3484
3485             push @d, @svc_labels
3486               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3487             warn "$me _items_cust_bill_pkg done adding service details\n"
3488               if $DEBUG > 1;
3489
3490             # show the location label if it's not the customer's default
3491             # location, and we're not grouping items by location already
3492             if ( $cust_pkg->locationnum != $default_locationnum
3493                   and !defined($locationnum) ) {
3494               my $loc = $cust_pkg->location_label;
3495               $loc = substr($loc, 0, $maxlength). '...'
3496                 if $format eq 'latex' && length($loc) > $maxlength;
3497               push @d, &{$escape_function}($loc);
3498             }
3499
3500             # Display of seconds_since_sqlradacct:
3501             # On the invoice, when processing @detail_items, look for a field
3502             # named 'seconds'.  This will contain total seconds for each 
3503             # service, in the same order as @ext_description.  For services 
3504             # that don't support this it will show undef.
3505             if ( $conf->exists('svc_acct-usage_seconds') 
3506                  and ! $cust_bill_pkg->pkgpart_override ) {
3507               foreach my $cust_svc ( 
3508                   $cust_pkg->h_cust_svc(@dates, 'I') 
3509                 ) {
3510
3511                 # eval because not having any part_export_usage exports 
3512                 # is a fatal error, last_bill/_date because that's how 
3513                 # sqlradius_hour billing does it
3514                 my $sec = eval {
3515                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
3516                 };
3517                 push @seconds, $sec;
3518               }
3519             } #if svc_acct-usage_seconds
3520
3521           } # if we are showing service labels
3522
3523           unless ( $is_summary ) {
3524             warn "$me _items_cust_bill_pkg adding details\n"
3525               if $DEBUG > 1;
3526
3527             #instead of omitting details entirely in this case (unwanted side
3528             # effects), just omit CDRs
3529             $details_opt{'no_usage'} = 1
3530               if $type && $type eq 'R';
3531
3532             push @d, $cust_bill_pkg->details(%details_opt);
3533           }
3534
3535           warn "$me _items_cust_bill_pkg calculating amount\n"
3536             if $DEBUG > 1;
3537   
3538           my $amount = 0;
3539           if (!$type) {
3540             $amount = $cust_bill_pkg->recur;
3541           } elsif ($type eq 'R') {
3542             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3543           } elsif ($type eq 'U') {
3544             $amount = $cust_bill_pkg->usage;
3545           }
3546   
3547           if ( !$type || $type eq 'R' ) {
3548
3549             warn "$me _items_cust_bill_pkg adding recur\n"
3550               if $DEBUG > 1;
3551
3552             my $unit_amount =
3553               ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
3554                                                 : $amount;
3555
3556             if ( $cust_bill_pkg->hidden ) {
3557               $r->{amount}      += $amount;
3558               $r->{unit_amount} += $unit_amount;
3559               push @{ $r->{ext_description} }, @d;
3560             } else {
3561               $r = {
3562                 description     => $description,
3563                 pkgpart         => $pkgpart,
3564                 pkgnum          => $cust_bill_pkg->pkgnum,
3565                 amount          => $amount,
3566                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3567                 unit_amount     => $unit_amount,
3568                 quantity        => $cust_bill_pkg->quantity,
3569                 %item_dates,
3570                 ext_description => \@d,
3571                 svc_label       => ($svc_label || ''),
3572                 locationnum     => $cust_pkg->locationnum,
3573               };
3574               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3575             }
3576
3577           } else {  # $type eq 'U'
3578
3579             warn "$me _items_cust_bill_pkg adding usage\n"
3580               if $DEBUG > 1;
3581
3582             if ( $cust_bill_pkg->hidden and defined($u) ) {
3583               # if this is a hidden package and there's already a usage
3584               # line for the bundle, add this package's total amount and
3585               # usage details to it
3586               $u->{amount}      += $amount;
3587               push @{ $u->{ext_description} }, @d;
3588             } elsif ( $amount ) {
3589               # create a new usage line
3590               $u = {
3591                 description     => $description,
3592                 pkgpart         => $pkgpart,
3593                 pkgnum          => $cust_bill_pkg->pkgnum,
3594                 amount          => $amount,
3595                 usage_item      => 1,
3596                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3597                 %item_dates,
3598                 ext_description => \@d,
3599                 locationnum     => $cust_pkg->locationnum,
3600               };
3601             } # else this has no usage, so don't create a usage section
3602           }
3603
3604         } # recurring or usage with recurring charge
3605
3606       } else { # taxes and fees
3607
3608         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3609           if $DEBUG > 1;
3610
3611         # items of this kind should normally not have sdate/edate.
3612         push @b, {
3613           'description' => $desc,
3614           'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
3615                                            + $cust_bill_pkg->recur)
3616         };
3617
3618       } # if quotation / package line item / other line item
3619
3620       # decide whether to show active discounts here
3621       if (
3622           # case 1: we are showing a single line for the package
3623           ( !$type )
3624           # case 2: we are showing a setup line for a package that has
3625           # no base recurring fee
3626           or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3627           # case 3: we are showing a recur line for a package that has 
3628           # a base recurring fee
3629           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3630       ) {
3631
3632         my $item_discount = $cust_bill_pkg->_item_discount;
3633         if ( $item_discount ) {
3634           # $item_discount->{amount} is negative
3635
3636           if ( $d and $cust_bill_pkg->hidden ) {
3637             $d->{amount}      += $item_discount->{amount};
3638           } else {
3639             $d = $item_discount;
3640             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
3641           }
3642
3643           # update the active line (before the discount) to show the 
3644           # original price (whether this is a hidden line or not)
3645           #
3646           # quotation discounts keep track of setup and recur; invoice 
3647           # discounts currently don't
3648           if ( exists $item_discount->{setup_amount} ) {
3649
3650             $s->{amount} -= $item_discount->{setup_amount} if $s;
3651             $r->{amount} -= $item_discount->{recur_amount} if $r;
3652
3653           } else {
3654
3655             # $active_line is the line item hashref for the line that will
3656             # show the original price
3657             # (use the recur or single line for the package, unless we're 
3658             # showing a setup line for a package with no recurring fee)
3659             my $active_line = $r;
3660             if ( $type eq 'S' ) {
3661               $active_line = $s;
3662             }
3663             $active_line->{amount} -= $item_discount->{amount};
3664
3665           }
3666
3667         } # if there are any discounts
3668       } # if this is an appropriate place to show discounts
3669
3670     } # foreach $display
3671
3672   }
3673
3674   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3675     if ( $_  ) {
3676       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3677         if exists($_->{amount});
3678       $_->{amount}      =~ s/^\-0\.00$/0.00/;
3679       if (exists($_->{unit_amount})) {
3680         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3681       }
3682
3683       push @b, { %$_ };
3684       #if $_->{amount} != 0
3685       #  || $discount_show_always
3686       #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3687       #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3688     }
3689   }
3690
3691   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3692     if $DEBUG > 1;
3693
3694   @b;
3695
3696 }
3697
3698 =item _items_discounts_avail
3699
3700 Returns an array of line item hashrefs representing available term discounts
3701 for this invoice.  This makes the same assumptions that apply to term 
3702 discounts in general: that the package is billed monthly, at a flat rate, 
3703 with no usage charges.  A prorated first month will be handled, as will 
3704 a setup fee if the discount is allowed to apply to setup fees.
3705
3706 =cut
3707
3708 sub _items_discounts_avail {
3709   my $self = shift;
3710
3711   #maybe move this method from cust_bill when quotations support discount_plans 
3712   return () unless $self->can('discount_plans');
3713   my %plans = $self->discount_plans;
3714
3715   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3716   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3717
3718   map {
3719     my $months = $_;
3720     my $plan = $plans{$months};
3721
3722     my $term_total = sprintf('%.2f', $plan->discounted_total);
3723     my $percent = sprintf('%.0f', 
3724                           100 * (1 - $term_total / $plan->base_total) );
3725     my $permonth = sprintf('%.2f', $term_total / $months);
3726     my $detail = $self->mt('discount on item'). ' '.
3727                  join(', ', map { "#$_" } $plan->pkgnums)
3728       if $list_pkgnums;
3729
3730     # discounts for non-integer months don't work anyway
3731     $months = sprintf("%d", $months);
3732
3733     +{
3734       description => $self->mt('Save [_1]% by paying for [_2] months',
3735                                 $percent, $months),
3736       amount      => $self->mt('[_1] ([_2] per month)', 
3737                                 $term_total, $money_char.$permonth),
3738       ext_description => ($detail || ''),
3739     }
3740   } #map
3741   sort { $b <=> $a } keys %plans;
3742
3743 }
3744
3745 1;