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