End of Month invoice terms, RT#74050
[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       $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1957               $self->due_date2str('short')
1958        unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum);
1959     } elsif ( $self->terms ) {
1960       $msg .= ' - '. $self->mt($self->terms);
1961     }
1962   }
1963   $msg;
1964 }
1965
1966 =item balance_due_date
1967
1968 =cut
1969
1970 sub balance_due_date {
1971   my $self = shift;
1972   my $conf = $self->conf;
1973   my $duedate = '';
1974   my $terms = $self->terms;
1975   if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1976     $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1977   }
1978   $duedate;
1979 }
1980
1981 sub credit_balance_msg { 
1982   my $self = shift;
1983   $self->mt('Credit Balance Remaining')
1984 }
1985
1986 =item _date_pretty
1987
1988 Returns a string with the date, for example: "3/20/2008", localized for the
1989 customer.  Use _date_pretty_unlocalized for non-end-customer display use.
1990
1991 =cut
1992
1993 sub _date_pretty {
1994   my $self = shift;
1995   $self->time2str_local('short', $self->_date);
1996 }
1997
1998 =item _date_pretty_unlocalized
1999
2000 Returns a string with the date, for example: "3/20/2008", in the format
2001 configured for the back-office.  Use _date_pretty for end-customer display use.
2002
2003 =cut
2004
2005 sub _date_pretty_unlocalized {
2006   my $self = shift;
2007   time2str($date_format, $self->_date);
2008 }
2009
2010 =item email HASHREF
2011
2012 Emails this template.
2013
2014 Options are passed as a hashref.  Available options:
2015
2016 =over 4
2017
2018 =item from
2019
2020 If specified, overrides the default From: address.
2021
2022 =item notice_name
2023
2024 If specified, overrides the name of the sent document ("Invoice" or "Quotation")
2025
2026 =item template
2027
2028 (Deprecated) If specified, is the name of a suffix for alternate template files.
2029
2030 =back
2031
2032 Options accepted by generate_email can also be used.
2033
2034 =cut
2035
2036 sub email {
2037   my $self = shift;
2038   my $opt = shift || {};
2039   if ($opt and !ref($opt)) {
2040     die ref($self). '->email called with positional parameters';
2041   }
2042
2043   return if $self->hide;
2044
2045   my $error = send_email(
2046     $self->generate_email(
2047       'subject'     => $self->email_subject($opt->{template}),
2048       %$opt, # template, etc.
2049     )
2050   );
2051
2052   die "can't email: $error\n" if $error;
2053 }
2054
2055 =item generate_email OPTION => VALUE ...
2056
2057 Options:
2058
2059 =over 4
2060
2061 =item from
2062
2063 sender address, required
2064
2065 =item template
2066
2067 alternate template name, optional
2068
2069 =item subject
2070
2071 email subject, optional
2072
2073 =item notice_name
2074
2075 notice name instead of "Invoice", optional
2076
2077 =back
2078
2079 Returns an argument list to be passed to L<FS::Misc::send_email>.
2080
2081 =cut
2082
2083 use MIME::Entity;
2084
2085 sub generate_email {
2086
2087   my $self = shift;
2088   my %args = @_;
2089   my $conf = $self->conf;
2090
2091   my $me = '[FS::Template_Mixin::generate_email]';
2092
2093   my %return = (
2094     'from'      => $args{'from'},
2095     'subject'   => ($args{'subject'} || $self->email_subject),
2096     'custnum'   => $self->custnum,
2097     'msgtype'   => 'invoice',
2098   );
2099
2100   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
2101
2102   my $cust_main = $self->cust_main;
2103
2104   if (ref($args{'to'}) eq 'ARRAY') {
2105     $return{'to'} = $args{'to'};
2106   } elsif ( $cust_main ) {
2107     $return{'to'} = [ $cust_main->invoicing_list_emailonly ];
2108   }
2109
2110   my $tc = $self->template_conf;
2111
2112   my @text; # array of lines
2113   my $html; # a big string
2114   my @related_parts; # will contain the text/HTML alternative, and images
2115   my $related; # will contain the multipart/related object
2116
2117   if ( $conf->exists($tc. 'email_pdf') ) {
2118     if ( my $msgnum = $conf->config($tc.'email_pdf_msgnum') ) {
2119
2120       warn "$me using '${tc}email_pdf_msgnum' in multipart message"
2121         if $DEBUG;
2122
2123       my $msg_template = FS::msg_template->by_key($msgnum)
2124         or die "${tc}email_pdf_msgnum $msgnum not found\n";
2125       my %prepared = $msg_template->prepare(
2126         cust_main => $self->cust_main,
2127         object    => $self
2128       );
2129
2130       @text = split(/(?=\n)/, $prepared{'text_body'});
2131       $html = $prepared{'html_body'};
2132
2133     } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) {
2134
2135       warn "$me using '${tc}email_pdf_note' in multipart message"
2136         if $DEBUG;
2137       @text = $conf->config($tc.'email_pdf_note');
2138       $html = join('<BR>', @text);
2139   
2140     } # else use the plain text invoice
2141   }
2142
2143   if (!@text) {
2144
2145     if ( $conf->config($tc.'template') ) {
2146
2147       warn "$me generating plain text invoice"
2148         if $DEBUG;
2149
2150       # 'print_text' argument is no longer used
2151       @text = $self->print_text(\%args);
2152
2153     } else {
2154
2155       warn "$me no plain text version exists; sending empty message body"
2156         if $DEBUG;
2157
2158     }
2159
2160   }
2161
2162   my $text_part = build MIME::Entity (
2163     'Type'        => 'text/plain',
2164     'Encoding'    => 'quoted-printable',
2165     'Charset'     => 'UTF-8',
2166     #'Encoding'    => '7bit',
2167     'Data'        => \@text,
2168     'Disposition' => 'inline',
2169   );
2170
2171   if (!$html) {
2172
2173     if ( $conf->exists($tc.'html') ) {
2174       warn "$me generating HTML invoice"
2175         if $DEBUG;
2176
2177       $args{'from'} =~ /\@([\w\.\-]+)/;
2178       my $from = $1 || 'example.com';
2179       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2180
2181       my $logo;
2182       my $agentnum = $cust_main ? $cust_main->agentnum
2183                                 : $self->prospect_main->agentnum;
2184       if ( defined($args{'template'}) && length($args{'template'})
2185            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
2186          )
2187       {
2188         $logo = 'logo_'. $args{'template'}. '.png';
2189       } else {
2190         $logo = "logo.png";
2191       }
2192       my $image_data = $conf->config_binary( $logo, $agentnum);
2193
2194       push @related_parts, build MIME::Entity
2195         'Type'       => 'image/png',
2196         'Encoding'   => 'base64',
2197         'Data'       => $image_data,
2198         'Filename'   => 'logo.png',
2199         'Content-ID' => "<$content_id>",
2200       ;
2201    
2202       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
2203         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2204         push @related_parts, build MIME::Entity
2205           'Type'       => 'image/png',
2206           'Encoding'   => 'base64',
2207           'Data'       => $self->invoice_barcode(0),
2208           'Filename'   => 'barcode.png',
2209           'Content-ID' => "<$barcode_content_id>",
2210         ;
2211         $args{'barcode_cid'} = $barcode_content_id;
2212       }
2213
2214       $html = $self->print_html({ 'cid'=>$content_id, %args });
2215     }
2216
2217   }
2218
2219   if ( $html ) {
2220
2221     warn "$me creating HTML/text multipart message"
2222       if $DEBUG;
2223
2224     $return{'nobody'} = 1;
2225
2226     my $alternative = build MIME::Entity
2227       'Type'        => 'multipart/alternative',
2228       #'Encoding'    => '7bit',
2229       'Disposition' => 'inline'
2230     ;
2231
2232     if ( @text ) {
2233       $alternative->add_part($text_part);
2234     }
2235
2236     $alternative->attach(
2237       'Type'        => 'text/html',
2238       'Encoding'    => 'quoted-printable',
2239       'Data'        => [ '<html>',
2240                          '  <head>',
2241                          '    <title>',
2242                          '      '. encode_entities($return{'subject'}), 
2243                          '    </title>',
2244                          '  </head>',
2245                          '  <body bgcolor="#e8e8e8">',
2246                          $html,
2247                          '  </body>',
2248                          '</html>',
2249                        ],
2250       'Disposition' => 'inline',
2251       #'Filename'    => 'invoice.pdf',
2252     );
2253
2254     unshift @related_parts, $alternative;
2255
2256     $related = build MIME::Entity 'Type'     => 'multipart/related',
2257                                   'Encoding' => '7bit';
2258
2259     #false laziness w/Misc::send_email
2260     $related->head->replace('Content-type',
2261       $related->mime_type.
2262       '; boundary="'. $related->head->multipart_boundary. '"'.
2263       '; type=multipart/alternative'
2264     );
2265
2266     $related->add_part($_) foreach @related_parts;
2267
2268   }
2269
2270   my @otherparts = ();
2271   if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
2272
2273     if ( $conf->config('voip-cdr_email_attach') eq 'zip' ) {
2274
2275       my $data = join('', map "$_\n",
2276                    $self->call_details(prepend_billed_number=>1)
2277                  );
2278
2279       my $zip = new Archive::Zip;
2280       my $file = $zip->addString( $data, 'usage-'.$self->invnum.'.csv' );
2281       $file->desiredCompressionMethod( COMPRESSION_DEFLATED );
2282
2283       my $zipdata = '';
2284       my $SH = IO::Scalar->new(\$zipdata);
2285       my $status = $zip->writeToFileHandle($SH);
2286       die "Error zipping CDR attachment: $!" unless $status == AZ_OK;
2287
2288       push @otherparts, build MIME::Entity
2289         'Type'        => 'application/zip',
2290         'Encoding'    => 'base64',
2291         'Data'        => $zipdata,
2292         'Disposition' => 'attachment',
2293         'Filename'    => 'usage-'. $self->invnum. '.zip',
2294       ;
2295
2296     } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
2297  
2298       push @otherparts, build MIME::Entity
2299         'Type'        => 'text/csv',
2300         'Encoding'    => '7bit',
2301         'Data'        => [ map { "$_\n" }
2302                              $self->call_details('prepend_billed_number' => 1)
2303                          ],
2304         'Disposition' => 'attachment',
2305         'Filename'    => 'usage-'. $self->invnum. '.csv',
2306       ;
2307
2308     }
2309
2310   }
2311
2312   if ( $conf->exists($tc.'email_pdf') ) {
2313
2314     #attaching pdf too:
2315     # multipart/mixed
2316     #   multipart/related
2317     #     multipart/alternative
2318     #       text/plain
2319     #       text/html
2320     #     image/png
2321     #   application/pdf
2322
2323     my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
2324     push @otherparts, $pdf;
2325   }
2326
2327   if (@otherparts) {
2328     $return{'content-type'} = 'multipart/mixed'; # of the outer container
2329     if ( $html ) {
2330       $return{'mimeparts'} = [ $related, @otherparts ];
2331       $return{'type'} = 'multipart/related'; # of the first part
2332     } else {
2333       $return{'mimeparts'} = [ $text_part, @otherparts ];
2334       $return{'type'} = 'text/plain';
2335     }
2336   } elsif ( $html ) { # no PDF or CSV, strip the outer container
2337     $return{'mimeparts'} = \@related_parts;
2338     $return{'content-type'} = 'multipart/related';
2339     $return{'type'} = 'multipart/alternative';
2340   } else { # no HTML either
2341     $return{'body'} = \@text;
2342     $return{'content-type'} = 'text/plain';
2343   }
2344
2345   %return;
2346
2347 }
2348
2349 =item mimebuild_pdf
2350
2351 Returns a list suitable for passing to MIME::Entity->build(), representing
2352 this invoice as PDF attachment.
2353
2354 =cut
2355
2356 sub mimebuild_pdf {
2357   my $self = shift;
2358   (
2359     'Type'        => 'application/pdf',
2360     'Encoding'    => 'base64',
2361     'Data'        => [ $self->print_pdf(@_) ],
2362     'Disposition' => 'attachment',
2363     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
2364   );
2365 }
2366
2367 =item postal_mail_fsinc
2368
2369 Sends this invoice to the Freeside Internet Services, Inc. print and mail
2370 service.
2371
2372 =cut
2373
2374 use CAM::PDF;
2375 use IO::Socket::SSL;
2376 use LWP::UserAgent;
2377 use HTTP::Request::Common qw( POST );
2378 use JSON::XS;
2379 use MIME::Base64;
2380 sub postal_mail_fsinc {
2381   my ( $self, %opt ) = @_;
2382
2383   my $url = 'https://ws.freeside.biz/print';
2384
2385   my $cust_main = $self->cust_main;
2386   my $agentnum = $cust_main->agentnum;
2387   my $bill_location = $cust_main->bill_location;
2388
2389   die "Extra charges for international mailing; contact support\@freeside.biz to enable\n"
2390     if $bill_location->country ne 'US';
2391
2392   my $conf = new FS::Conf;
2393
2394   my @company_address = $conf->config('company_address', $agentnum);
2395   my ( $company_address1, $company_address2, $company_city, $company_state, $company_zip );
2396   if ( $company_address[2] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2397     $company_address1 = $company_address[0];
2398     $company_address2 = $company_address[1];
2399     $company_city  = $1;
2400     $company_state = $2;
2401     $company_zip   = $3;
2402   } elsif ( $company_address[1] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2403     $company_address1 = $company_address[0];
2404     $company_address2 = '';
2405     $company_city  = $1;
2406     $company_state = $2;
2407     $company_zip   = $3;
2408   } else {
2409     die "Unparsable company_address; contact support\@freeside.biz\n";
2410   }
2411   $company_city =~ s/,$//;
2412
2413   my $file = $self->print_pdf(%opt, 'no_addresses' => 1);
2414   my $pages = CAM::PDF->new($file)->numPages;
2415
2416   my $ua = LWP::UserAgent->new(
2417     'ssl_opts' => { 
2418       verify_hostname => 0,
2419       SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
2420     }
2421   );
2422   my $response = $ua->request( POST $url, [
2423     'support-key'      => scalar($conf->config('support-key')),
2424     'file'             => encode_base64($file),
2425     'pages'            => $pages,
2426
2427     #from:
2428     'company_name'     => scalar( $conf->config('company_name', $agentnum) ),
2429     'company_address1' => $company_address1,
2430     'company_address2' => $company_address2,
2431     'company_city'     => $company_city,
2432     'company_state'    => $company_state,
2433     'company_zip'      => $company_zip,
2434     'company_country'  => 'US',
2435     'company_phonenum' => scalar($conf->config('company_phonenum', $agentnum)),
2436     'company_email'    => scalar($conf->config('invoice_from', $agentnum)),
2437
2438     #to:
2439     'name'             => ( $cust_main->payname
2440                               && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/
2441                                 ? $cust_main->payname
2442                                 : $cust_main->contact_firstlast
2443                           ),
2444     'company'          => $cust_main->company,
2445     'address1'         => $bill_location->address1,
2446     'address2'         => $bill_location->address2,
2447     'city'             => $bill_location->city,
2448     'state'            => $bill_location->state,
2449     'zip'              => $bill_location->zip,
2450     'country'          => $bill_location->country,
2451   ]);
2452
2453   die "Print connection error: ". $response->message. "\n"
2454     unless $response->is_success;
2455
2456   local $@;
2457   my $content = eval { decode_json($response->content) };
2458   die "Print JSON error : $@\n" if $@;
2459
2460   die $content->{error}."\n"
2461     if $content->{error};
2462
2463   #TODO: store this so we can query for a status later
2464   warn "Invoice printed, ID ". $content->{id}. "\n";
2465
2466   $content->{id};
2467 }
2468
2469 =item _items_sections OPTIONS
2470
2471 Generate section information for all items appearing on this invoice.
2472 This will only be called for multi-section invoices.
2473
2474 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
2475 related display records (L<FS::cust_bill_pkg_display>) and organize 
2476 them into two groups ("early" and "late" according to whether they come 
2477 before or after the total), then into sections.  A subtotal is calculated 
2478 for each section.
2479
2480 Section descriptions are returned in sort weight order.  Each consists 
2481 of a hash containing:
2482
2483 description: the package category name, escaped
2484 subtotal: the total charges in that section
2485 tax_section: a flag indicating that the section contains only tax charges
2486 summarized: same as tax_section, for some reason
2487 sort_weight: the package category's sort weight
2488
2489 If 'condense' is set on the display record, it also contains everything 
2490 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
2491 coderefs to generate parts of the invoice.  This is not advised.
2492
2493 The method returns two arrayrefs, one of "early" sections and one of "late"
2494 sections.
2495
2496 OPTIONS may include:
2497
2498 by_location: a flag to divide the invoice into sections by location.  
2499 Each section hash will have a 'location' element containing a hashref of 
2500 the location fields (see L<FS::cust_location>).  The section description
2501 will be the location label, but the template can use any of the location 
2502 fields to create a suitable label.
2503
2504 by_category: a flag to divide the invoice into sections using display 
2505 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
2506 behavior.  Each section hash will have a 'category' element containing
2507 the section name from the display record (which probably equals the 
2508 category name of the package, but may not in some cases).
2509
2510 summary: a flag indicating that this is a summary-format invoice.
2511 Turning this on has the following effects:
2512 - Ignores display items with the 'summary' flag.
2513 - Places all sections in the "early" group even if they have post_total.
2514 - Creates sections for all non-disabled package categories, even if they 
2515 have no charges on this invoice, as well as a section with no name.
2516
2517 escape: an escape function to use for section titles.
2518
2519 extra_sections: an arrayref of additional sections to return after the 
2520 sorted list.  If there are any of these, section subtotals exclude 
2521 usage charges.
2522
2523 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
2524 passed through to C<_condense_section()>.
2525
2526 =cut
2527
2528 use vars qw(%pkg_category_cache);
2529 sub _items_sections {
2530   my $self = shift;
2531   my %opt = @_;
2532   
2533   my $escape = $opt{escape};
2534   my @extra_sections = @{ $opt{extra_sections} || [] };
2535
2536   # $subtotal{$locationnum}{$categoryname} = amount.
2537   # if we're not using by_location, $locationnum is undef.
2538   # if we're not using by_category, you guessed it, $categoryname is undef.
2539   # if we're not using either one, we shouldn't be here in the first place...
2540   my %subtotal = ();
2541   my %late_subtotal = ();
2542   my %not_tax = ();
2543
2544   # About tax items + multisection invoices:
2545   # If either invoice_*summary option is enabled, AND there is a 
2546   # package category with the name of the tax, then there will be 
2547   # a display record assigning the tax item to that category.
2548   #
2549   # However, the taxes are always placed in the "Taxes, Surcharges,
2550   # and Fees" section regardless of that.  The only effect of the 
2551   # display record is to create a subtotal for the summary page.
2552
2553   # cache these
2554   my $pkg_hash = $self->cust_pkg_hash;
2555
2556   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2557   {
2558
2559       my $usage = $cust_bill_pkg->usage;
2560
2561       my $locationnum;
2562       if ( $opt{by_location} ) {
2563         if ( $cust_bill_pkg->pkgnum ) {
2564           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2565         } else {
2566           $locationnum = '';
2567         }
2568       } else {
2569         $locationnum = undef;
2570       }
2571
2572       # as in _items_cust_pkg, if a line item has no display records,
2573       # cust_bill_pkg_display() returns a default record for it
2574
2575       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2576         next if ( $display->summary && $opt{summary} );
2577
2578         my $section = $display->section;
2579         my $type    = $display->type;
2580         # Set $section = undef if we're sectioning by location and this
2581         # line item _has_ a location (i.e. isn't a fee).
2582         $section = undef if $locationnum;
2583
2584         # set this flag if the section is not tax-only
2585         $not_tax{$locationnum}{$section} = 1
2586           if $cust_bill_pkg->pkgnum  or $cust_bill_pkg->feepart;
2587
2588         # there's actually a very important piece of logic buried in here:
2589         # incrementing $late_subtotal{$section} CREATES 
2590         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
2591         # to define the list of late sections, and likewise keys(%subtotal).
2592         # When _items_cust_bill_pkg is called to generate line items for 
2593         # real, it will be called with 'section' => $section for each 
2594         # of these.
2595         if ( $display->post_total && !$opt{summary} ) {
2596           if (! $type || $type eq 'S') {
2597             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2598               if $cust_bill_pkg->setup != 0
2599               || $cust_bill_pkg->setup_show_zero;
2600           }
2601
2602           if (! $type) {
2603             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2604               if $cust_bill_pkg->recur != 0
2605               || $cust_bill_pkg->recur_show_zero;
2606           }
2607
2608           if ($type && $type eq 'R') {
2609             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2610               if $cust_bill_pkg->recur != 0
2611               || $cust_bill_pkg->recur_show_zero;
2612           }
2613           
2614           if ($type && $type eq 'U') {
2615             $late_subtotal{$locationnum}{$section} += $usage
2616               unless scalar(@extra_sections);
2617           }
2618
2619         } else { # it's a pre-total (normal) section
2620
2621           # skip tax items unless they're explicitly included in a section
2622           next if $cust_bill_pkg->pkgnum == 0 and
2623                   ! $cust_bill_pkg->feepart   and
2624                   ! $section;
2625
2626           if ( $type eq 'S' ) {
2627             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2628               if $cust_bill_pkg->setup != 0
2629               || $cust_bill_pkg->setup_show_zero;
2630           } elsif ( $type eq 'R' ) {
2631             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2632               if $cust_bill_pkg->recur != 0
2633               || $cust_bill_pkg->recur_show_zero;
2634           } elsif ( $type eq 'U' ) {
2635             $subtotal{$locationnum}{$section} += $usage
2636               unless scalar(@extra_sections);
2637           } elsif ( !$type ) {
2638             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2639                                                + $cust_bill_pkg->recur;
2640           }
2641
2642         }
2643
2644       }
2645
2646   }
2647
2648   %pkg_category_cache = ();
2649
2650   # summary invoices need subtotals for all non-disabled package categories,
2651   # even if they're zero
2652   # but currently assume that there are no location sections, or at least
2653   # that the summary page doesn't care about them
2654   if ( $opt{summary} ) {
2655     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2656       $subtotal{''}{$category->categoryname} ||= 0;
2657     }
2658     $subtotal{''}{''} ||= 0;
2659   }
2660
2661   my @sections;
2662   foreach my $post_total (0,1) {
2663     my @these;
2664     my $s = $post_total ? \%late_subtotal : \%subtotal;
2665     foreach my $locationnum (keys %$s) {
2666       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2667         my $section = {
2668                         'subtotal'    => $s->{$locationnum}{$sectionname},
2669                         'sort_weight' => 0,
2670                       };
2671         if ( $locationnum ) {
2672           $section->{'locationnum'} = $locationnum;
2673           my $location = FS::cust_location->by_key($locationnum);
2674           $section->{'description'} = &{ $escape }($location->location_label);
2675           # Better ideas? This will roughly group them by proximity, 
2676           # which alpha sorting on any of the address fields won't.
2677           # Sorting by locationnum is meaningless.
2678           # We have to sort on _something_ or the order may change 
2679           # randomly from one invoice to the next, which will confuse
2680           # people.
2681           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2682                                       $locationnum;
2683           $section->{'location'} = {
2684             label_prefix => &{ $escape }($location->label_prefix),
2685             map { $_ => &{ $escape }($location->get($_)) }
2686               $location->fields
2687           };
2688         } else {
2689           $section->{'category'} = $sectionname;
2690           $section->{'description'} = &{ $escape }($sectionname);
2691           if ( _pkg_category($sectionname) ) {
2692             $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2693             if ( _pkg_category($sectionname)->condense ) {
2694               $section = { %$section, $self->_condense_section($opt{format}) };
2695             }
2696           }
2697         }
2698         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2699           # then it's a tax-only section
2700           $section->{'summarized'} = 'Y';
2701           $section->{'tax_section'} = 'Y';
2702         }
2703         push @these, $section;
2704       } # foreach $sectionname
2705     } #foreach $locationnum
2706     push @these, @extra_sections if $post_total == 0;
2707     # need an alpha sort for location sections, because postal codes can 
2708     # be non-numeric
2709     $sections[ $post_total ] = [ sort {
2710       $opt{'by_location'} ? 
2711         ($a->{sort_weight} cmp $b->{sort_weight}) :
2712         ($a->{sort_weight} <=> $b->{sort_weight})
2713       } @these ];
2714   } #foreach $post_total
2715
2716   return @sections; # early, late
2717 }
2718
2719 #helper subs for above
2720
2721 sub cust_pkg_hash {
2722   my $self = shift;
2723   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2724 }
2725
2726 sub _pkg_category {
2727   my $categoryname = shift;
2728   $pkg_category_cache{$categoryname} ||=
2729     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2730 }
2731
2732 my %condensed_format = (
2733   'label' => [ qw( Description Qty Amount ) ],
2734   'fields' => [
2735                 sub { shift->{description} },
2736                 sub { shift->{quantity} },
2737                 sub { my($href, %opt) = @_;
2738                       ($opt{dollar} || ''). $href->{amount};
2739                     },
2740               ],
2741   'align'  => [ qw( l r r ) ],
2742   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2743   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2744 );
2745
2746 sub _condense_section {
2747   my ( $self, $format ) = ( shift, shift );
2748   ( 'condensed' => 1,
2749     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2750       qw( description_generator
2751           header_generator
2752           total_generator
2753           total_line_generator
2754         )
2755   );
2756 }
2757
2758 sub _condensed_generator_defaults {
2759   my ( $self, $format ) = ( shift, shift );
2760   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2761 }
2762
2763 my %html_align = (
2764   'c' => 'center',
2765   'l' => 'left',
2766   'r' => 'right',
2767 );
2768
2769 sub _condensed_header_generator {
2770   my ( $self, $format ) = ( shift, shift );
2771
2772   my ( $f, $prefix, $suffix, $separator, $column ) =
2773     _condensed_generator_defaults($format);
2774
2775   if ($format eq 'latex') {
2776     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2777     $suffix = "\\\\\n\\hline";
2778     $separator = "&\n";
2779     $column =
2780       sub { my ($d,$a,$s,$w) = @_;
2781             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2782           };
2783   } elsif ( $format eq 'html' ) {
2784     $prefix = '<th></th>';
2785     $suffix = '';
2786     $separator = '';
2787     $column =
2788       sub { my ($d,$a,$s,$w) = @_;
2789             return qq!<th align="$html_align{$a}">$d</th>!;
2790       };
2791   }
2792
2793   sub {
2794     my @args = @_;
2795     my @result = ();
2796
2797     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2798       push @result,
2799         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2800     }
2801
2802     $prefix. join($separator, @result). $suffix;
2803   };
2804
2805 }
2806
2807 sub _condensed_description_generator {
2808   my ( $self, $format ) = ( shift, shift );
2809
2810   my ( $f, $prefix, $suffix, $separator, $column ) =
2811     _condensed_generator_defaults($format);
2812
2813   my $money_char = '$';
2814   if ($format eq 'latex') {
2815     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2816     $suffix = '\\\\';
2817     $separator = " & \n";
2818     $column =
2819       sub { my ($d,$a,$s,$w) = @_;
2820             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2821           };
2822     $money_char = '\\dollar';
2823   }elsif ( $format eq 'html' ) {
2824     $prefix = '"><td align="center"></td>';
2825     $suffix = '';
2826     $separator = '';
2827     $column =
2828       sub { my ($d,$a,$s,$w) = @_;
2829             return qq!<td align="$html_align{$a}">$d</td>!;
2830       };
2831     #$money_char = $conf->config('money_char') || '$';
2832     $money_char = '';  # this is madness
2833   }
2834
2835   sub {
2836     #my @args = @_;
2837     my $href = shift;
2838     my @result = ();
2839
2840     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2841       my $dollar = '';
2842       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2843       push @result,
2844         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2845                     map { $f->{$_}->[$i] } qw(align span width)
2846                   );
2847     }
2848
2849     $prefix. join( $separator, @result ). $suffix;
2850   };
2851
2852 }
2853
2854 sub _condensed_total_generator {
2855   my ( $self, $format ) = ( shift, shift );
2856
2857   my ( $f, $prefix, $suffix, $separator, $column ) =
2858     _condensed_generator_defaults($format);
2859   my $style = '';
2860
2861   if ($format eq 'latex') {
2862     $prefix = "& ";
2863     $suffix = "\\\\\n";
2864     $separator = " & \n";
2865     $column =
2866       sub { my ($d,$a,$s,$w) = @_;
2867             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2868           };
2869   }elsif ( $format eq 'html' ) {
2870     $prefix = '';
2871     $suffix = '';
2872     $separator = '';
2873     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2874     $column =
2875       sub { my ($d,$a,$s,$w) = @_;
2876             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2877       };
2878   }
2879
2880
2881   sub {
2882     my @args = @_;
2883     my @result = ();
2884
2885     #  my $r = &{$f->{fields}->[$i]}(@args);
2886     #  $r .= ' Total' unless $i;
2887
2888     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2889       push @result,
2890         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2891                     map { $f->{$_}->[$i] } qw(align span width)
2892                   );
2893     }
2894
2895     $prefix. join( $separator, @result ). $suffix;
2896   };
2897
2898 }
2899
2900 =item total_line_generator FORMAT
2901
2902 Returns a coderef used for generation of invoice total line items for this
2903 usage_class.  FORMAT is either html or latex
2904
2905 =cut
2906
2907 # should not be used: will have issues with hash element names (description vs
2908 # total_item and amount vs total_amount -- another array of functions?
2909
2910 sub _condensed_total_line_generator {
2911   my ( $self, $format ) = ( shift, shift );
2912
2913   my ( $f, $prefix, $suffix, $separator, $column ) =
2914     _condensed_generator_defaults($format);
2915   my $style = '';
2916
2917   if ($format eq 'latex') {
2918     $prefix = "& ";
2919     $suffix = "\\\\\n";
2920     $separator = " & \n";
2921     $column =
2922       sub { my ($d,$a,$s,$w) = @_;
2923             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2924           };
2925   }elsif ( $format eq 'html' ) {
2926     $prefix = '';
2927     $suffix = '';
2928     $separator = '';
2929     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2930     $column =
2931       sub { my ($d,$a,$s,$w) = @_;
2932             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2933       };
2934   }
2935
2936
2937   sub {
2938     my @args = @_;
2939     my @result = ();
2940
2941     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2942       push @result,
2943         &{$column}( &{$f->{fields}->[$i]}(@args),
2944                     map { $f->{$_}->[$i] } qw(align span width)
2945                   );
2946     }
2947
2948     $prefix. join( $separator, @result ). $suffix;
2949   };
2950
2951 }
2952
2953 =item _items_pkg [ OPTIONS ]
2954
2955 Return line item hashes for each package item on this invoice. Nearly 
2956 equivalent to 
2957
2958 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2959
2960 OPTIONS are passed through to _items_cust_bill_pkg, and should include
2961 'format' and 'escape_function' at minimum.
2962
2963 To produce items for a specific invoice section, OPTIONS should include
2964 'section', a hashref containing 'category' and/or 'locationnum' keys.
2965
2966 'section' may also contain a key named 'condensed'. If this is present
2967 and has a true value, _items_pkg will try to merge identical items into items
2968 with 'quantity' equal to the number of items (not the sum of their separate
2969 quantities, for some reason).
2970
2971 =cut
2972
2973 sub _items_nontax {
2974   my $self = shift;
2975   # The order of these is important.  Bundled line items will be merged into
2976   # the most recent non-hidden item, so it needs to be the one with:
2977   # - the same pkgnum
2978   # - the same start date
2979   # - no pkgpart_override
2980   #
2981   # So: sort by pkgnum,
2982   # then by sdate
2983   # then sort the base line item before any overrides
2984   # then sort hidden before non-hidden add-ons
2985   # then sort by override pkgpart (for consistency)
2986   sort { $a->pkgnum <=> $b->pkgnum        or
2987          $a->sdate  <=> $b->sdate         or
2988          ($a->pkgpart_override ? 0 : -1)  or
2989          ($b->pkgpart_override ? 0 : 1)   or
2990          $b->hidden cmp $a->hidden        or
2991          $a->pkgpart_override <=> $b->pkgpart_override
2992        }
2993   # and of course exclude taxes and fees
2994   grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2995 }
2996
2997 sub _items_fee {
2998   my $self = shift;
2999   my %options = @_;
3000   my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
3001   my $escape_function = $options{escape_function};
3002
3003   my @items;
3004   foreach my $cust_bill_pkg (@cust_bill_pkg) {
3005     # cache this, so we don't look it up again in every section
3006     my $part_fee = $cust_bill_pkg->get('part_fee')
3007        || $cust_bill_pkg->part_fee;
3008     $cust_bill_pkg->set('part_fee', $part_fee);
3009     if (!$part_fee) {
3010       #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
3011       warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
3012       next;
3013     }
3014     if ( exists($options{section}) and exists($options{section}{category}) )
3015     {
3016       my $categoryname = $options{section}{category};
3017       # then filter for items that have that section
3018       if ( $part_fee->categoryname ne $categoryname ) {
3019         warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
3020         next;
3021       }
3022     } # otherwise include them all in the main section
3023     # XXX what to do when sectioning by location?
3024     
3025     my @ext_desc;
3026     my %base_invnums; # invnum => invoice date
3027     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
3028       if ($_->base_invnum) {
3029         # XXX what if base_bill has been voided?
3030         my $base_bill = FS::cust_bill->by_key($_->base_invnum);
3031         my $base_date = $self->time2str_local('short', $base_bill->_date)
3032           if $base_bill;
3033         $base_invnums{$_->base_invnum} = $base_date || '';
3034       }
3035     }
3036     foreach (sort keys(%base_invnums)) {
3037       next if $_ == $self->invnum;
3038       # per convention, we must escape ext_description lines
3039       push @ext_desc,
3040         &{$escape_function}(
3041           $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
3042         );
3043     }
3044     my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
3045     # but not escape the base description line
3046
3047     push @items,
3048       { feepart     => $cust_bill_pkg->feepart,
3049         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
3050         description => $desc,
3051         ext_description => \@ext_desc
3052         # sdate/edate?
3053       };
3054   }
3055   @items;
3056 }
3057
3058 sub _items_pkg {
3059   my $self = shift;
3060   my %options = @_;
3061
3062   warn "$me _items_pkg searching for all package line items\n"
3063     if $DEBUG > 1;
3064
3065   my @cust_bill_pkg = $self->_items_nontax;
3066
3067   warn "$me _items_pkg filtering line items\n"
3068     if $DEBUG > 1;
3069   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3070
3071   if ($options{section} && $options{section}->{condensed}) {
3072
3073     warn "$me _items_pkg condensing section\n"
3074       if $DEBUG > 1;
3075
3076     my %itemshash = ();
3077     local $Storable::canonical = 1;
3078     foreach ( @items ) {
3079       my $item = { %$_ };
3080       delete $item->{ref};
3081       delete $item->{ext_description};
3082       my $key = freeze($item);
3083       $itemshash{$key} ||= 0;
3084       $itemshash{$key} ++; # += $item->{quantity};
3085     }
3086     @items = sort { $a->{description} cmp $b->{description} }
3087              map { my $i = thaw($_);
3088                    $i->{quantity} = $itemshash{$_};
3089                    $i->{amount} =
3090                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3091                    $i;
3092                  }
3093              keys %itemshash;
3094   }
3095
3096   warn "$me _items_pkg returning ". scalar(@items). " items\n"
3097     if $DEBUG > 1;
3098
3099   @items;
3100 }
3101
3102 sub _taxsort {
3103   return 0 unless $a->itemdesc cmp $b->itemdesc;
3104   return -1 if $b->itemdesc eq 'Tax';
3105   return 1 if $a->itemdesc eq 'Tax';
3106   return -1 if $b->itemdesc eq 'Other surcharges';
3107   return 1 if $a->itemdesc eq 'Other surcharges';
3108   $a->itemdesc cmp $b->itemdesc;
3109 }
3110
3111 sub _items_tax {
3112   my $self = shift;
3113   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
3114     $self->cust_bill_pkg;
3115   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3116
3117   if ( $self->conf->exists('always_show_tax') ) {
3118     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
3119     if (0 == grep { $_->{description} eq $itemdesc } @items) {
3120       push @items,
3121         { 'description' => $itemdesc,
3122           'amount'      => 0.00 };
3123     }
3124   }
3125   @items;
3126 }
3127
3128 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
3129
3130 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
3131 list of hashrefs describing the line items they generate on the invoice.
3132
3133 OPTIONS may include:
3134
3135 format: the invoice format.
3136
3137 escape_function: the function used to escape strings.
3138
3139 DEPRECATED? (expensive, mostly unused?)
3140 format_function: the function used to format CDRs.
3141
3142 section: a hashref containing 'category' and/or 'locationnum'; if this 
3143 is present, only returns line items that belong to that category and/or
3144 location (whichever is defined).
3145
3146 multisection: a flag indicating that this is a multisection invoice,
3147 which does something complicated.
3148
3149 preref_callback: coderef run for each line item, code should return HTML to be
3150 displayed before that line item (quotations only)
3151
3152 Returns a list of hashrefs, each of which may contain:
3153
3154 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
3155 ext_description, which is an arrayref of detail lines to show below 
3156 the package line.
3157
3158 =cut
3159
3160 sub _items_cust_bill_pkg {
3161   my $self = shift;
3162   my $conf = $self->conf;
3163   my $cust_bill_pkgs = shift;
3164   my %opt = @_;
3165
3166   my $format = $opt{format} || '';
3167   my $escape_function = $opt{escape_function} || sub { shift };
3168   my $format_function = $opt{format_function} || '';
3169   my $no_usage = $opt{no_usage} || '';
3170   my $unsquelched = $opt{unsquelched} || ''; #unused
3171   my ($section, $locationnum, $category);
3172   if ( $opt{section} ) {
3173     $category = $opt{section}->{category};
3174     $locationnum = $opt{section}->{locationnum};
3175   }
3176   my $summary_page = $opt{summary_page} || ''; #unused
3177   my $multisection = defined($category) || defined($locationnum);
3178   # this variable is the value of the config setting, not whether it applies
3179   # to this particular line item.
3180   my $discount_show_always = $conf->exists('discount-show-always');
3181
3182   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
3183
3184   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
3185
3186   # for location labels: use default location on the invoice date
3187   my $default_locationnum;
3188   if ( $conf->exists('invoice-all_pkg_addresses') ) {
3189     $default_locationnum = 0; # treat them all as non-default
3190   } elsif ( $self->custnum ) {
3191     my $h_cust_main;
3192     my @h_search = FS::h_cust_main->sql_h_search($self->_date);
3193     $h_cust_main = qsearchs({
3194         'table'     => 'h_cust_main',
3195         'hashref'   => { custnum => $self->custnum },
3196         'extra_sql' => $h_search[1],
3197         'addl_from' => $h_search[3],
3198     }) || $cust_main;
3199     $default_locationnum = $h_cust_main->ship_locationnum;
3200   } elsif ( $self->prospectnum ) {
3201     my $cust_location = qsearchs('cust_location',
3202       { prospectnum => $self->prospectnum,
3203         disabled => '' });
3204     $default_locationnum = $cust_location->locationnum if $cust_location;
3205   }
3206
3207   my @b = (); # accumulator for the line item hashes that we'll return
3208   my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
3209             # the 'current' line item hashes for setup, recur, usage, discount
3210   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
3211   {
3212     # if the current line item is waiting to go out, and the one we're about
3213     # to start is not bundled, then push out the current one and start a new
3214     # one.
3215     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3216       if ( $_ && !$cust_bill_pkg->hidden ) {
3217         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
3218         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3219         if (exists($_->{unit_amount})) {
3220           $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3221         }
3222         push @b, { %$_ };
3223         # we already decided to create this display line; don't reconsider it
3224         # now.
3225         #  if $_->{amount} != 0
3226         #  || $discount_show_always
3227         #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3228         #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3229         ;
3230         $_ = undef;
3231       }
3232     }
3233
3234     if ( $locationnum ) {
3235       # this is a location section; skip packages that aren't at this
3236       # service location.
3237       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
3238       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
3239               != $locationnum;
3240     }
3241
3242     # Consider display records for this item to determine if it belongs
3243     # in this section.  Note that if there are no display records, there
3244     # will be a default pseudo-record that includes all charge types 
3245     # and has no section name.
3246     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
3247                                   ? $cust_bill_pkg->cust_bill_pkg_display
3248                                   : ( $cust_bill_pkg );
3249
3250     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
3251          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
3252       if $DEBUG > 1;
3253
3254     if ( defined($category) ) {
3255       # then this is a package category section; process all display records
3256       # that belong to this section.
3257       @cust_bill_pkg_display = grep { $_->section eq $category }
3258                                 @cust_bill_pkg_display;
3259     } else {
3260       # otherwise, process all display records that aren't usage summaries
3261       # (I don't think there should be usage summaries if you aren't using 
3262       # category sections, but this is the historical behavior)
3263       @cust_bill_pkg_display = grep { !$_->summary }
3264                                 @cust_bill_pkg_display;
3265     }
3266
3267     my $classname = ''; # package class name, will fill in later
3268
3269     foreach my $display (@cust_bill_pkg_display) {
3270
3271       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
3272            $display->billpkgdisplaynum. "\n"
3273         if $DEBUG > 1;
3274
3275       my $type = $display->type;
3276
3277       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
3278       $desc = substr($desc, 0, $maxlength). '...'
3279         if $format eq 'latex' && length($desc) > $maxlength;
3280
3281       my %details_opt = ( 'format'          => $format,
3282                           'escape_function' => $escape_function,
3283                           'format_function' => $format_function,
3284                           'no_usage'        => $opt{'no_usage'},
3285                         );
3286
3287       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
3288         # XXX this should be pulled out into quotation_pkg
3289
3290         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
3291           if $DEBUG > 1;
3292         # quotation_pkgs are never fees, so don't worry about the case where
3293         # part_pkg is undefined
3294
3295         my @details = $cust_bill_pkg->details;
3296
3297         # and I guess they're never bundled either?
3298         if (( $cust_bill_pkg->setup != 0 ) || ( $cust_bill_pkg->setup_show_zero )) {
3299           my $description = $desc;
3300           $description .= ' Setup'
3301             if $cust_bill_pkg->recur != 0
3302             || $discount_show_always
3303             || $cust_bill_pkg->recur_show_zero;
3304           #push @b, {
3305           # keep it consistent, please
3306           $s = {
3307             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3308             'description' => $description,
3309             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3310             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
3311             'quantity'    => $cust_bill_pkg->quantity,
3312             'ext_description' => \@details,
3313             'preref_html' => ( $opt{preref_callback}
3314                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3315                                  : ''
3316                              ),
3317           };
3318         }
3319         if (( $cust_bill_pkg->recur != 0 ) || ( $cust_bill_pkg->recur_show_zero )) {
3320           #push @b, {
3321           $r = {
3322             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3323             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
3324             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3325             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
3326             'quantity'    => $cust_bill_pkg->quantity,
3327             'ext_description' => \@details,
3328            'preref_html'  => ( $opt{preref_callback}
3329                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3330                                  : ''
3331                              ),
3332           };
3333         }
3334
3335       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
3336         # a "normal" package line item (not a quotation, not a fee, not a tax)
3337
3338         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
3339           if $DEBUG > 1;
3340  
3341         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3342         my $part_pkg = $cust_pkg->part_pkg;
3343
3344         # which pkgpart to show for display purposes?
3345         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
3346
3347         # start/end dates for invoice formats that do nonstandard 
3348         # things with them
3349         my %item_dates = ();
3350         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
3351           unless $part_pkg->option('disable_line_item_date_ranges',1);
3352
3353         # not normally used, but pass this to the template anyway
3354         $classname = $part_pkg->classname;
3355
3356         if (    (!$type || $type eq 'S')
3357              && (    $cust_bill_pkg->setup != 0
3358                   || $cust_bill_pkg->setup_show_zero
3359                   || ($discount_show_always and $cust_bill_pkg->unitsetup > 0)
3360                 )
3361            )
3362          {
3363
3364           warn "$me _items_cust_bill_pkg adding setup\n"
3365             if $DEBUG > 1;
3366
3367           # append the word 'Setup' to the setup line if there's going to be
3368           # a recur line for the same package (i.e. not a one-time charge) 
3369           # XXX localization
3370           my $description = $desc;
3371           $description .= ' Setup'
3372             if $cust_bill_pkg->recur != 0
3373             || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3374             || $cust_bill_pkg->recur_show_zero;
3375
3376           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3377                                                               $self->agentnum )
3378             if $part_pkg->is_prepaid #for prepaid, "display the validity period
3379                                      # triggered by the recurring charge freq
3380                                      # (RT#26274)
3381             && $cust_bill_pkg->recur == 0
3382             && ! $cust_bill_pkg->recur_show_zero;
3383
3384           my @d = ();
3385           my $svc_label;
3386
3387           # always pass the svc_label through to the template, even if 
3388           # not displaying it as an ext_description
3389           my @svc_labels = map &{$escape_function}($_),
3390             $cust_pkg->h_labels_short($self->_date,
3391                                       undef,
3392                                       'I',
3393                                       $self->conf->{locale},
3394                                      );
3395           $svc_label = $svc_labels[0];
3396
3397           unless ( $cust_pkg->part_pkg->hide_svc_detail
3398                 || $cust_bill_pkg->hidden )
3399           {
3400
3401             push @d, @svc_labels
3402               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3403             # show the location label if it's not the customer's default
3404             # location, and we're not grouping items by location already
3405             if ( $cust_pkg->locationnum != $default_locationnum
3406                   and !defined($locationnum) ) {
3407               my $loc = $cust_pkg->location_label;
3408               $loc = substr($loc, 0, $maxlength). '...'
3409                 if $format eq 'latex' && length($loc) > $maxlength;
3410               push @d, &{$escape_function}($loc);
3411             }
3412
3413           } #unless hiding service details
3414
3415           push @d, $cust_bill_pkg->details(%details_opt)
3416             if $cust_bill_pkg->recur == 0;
3417
3418           if ( $cust_bill_pkg->hidden ) {
3419             $s->{amount}      += $cust_bill_pkg->setup;
3420             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3421             push @{ $s->{ext_description} }, @d;
3422           } else {
3423             $s = {
3424               _is_setup       => 1,
3425               description     => $description,
3426               pkgpart         => $pkgpart,
3427               pkgnum          => $cust_bill_pkg->pkgnum,
3428               amount          => $cust_bill_pkg->setup,
3429               setup_show_zero => $cust_bill_pkg->setup_show_zero,
3430               unit_amount     => $cust_bill_pkg->unitsetup,
3431               quantity        => $cust_bill_pkg->quantity,
3432               ext_description => \@d,
3433               svc_label       => ($svc_label || ''),
3434               locationnum     => $cust_pkg->locationnum, # sure, why not?
3435             };
3436           };
3437
3438         }
3439
3440         # should we show a recur line?
3441         # if type eq 'S', then NO, because we've been told not to.
3442         # otherwise, show the recur line if:
3443         # - there's a recurring charge
3444         # - or recur_show_zero is on
3445         # - or there's a positive unitrecur (so it's been discounted to zero)
3446         #   and discount-show-always is on
3447         if (    ( !$type || $type eq 'R' || $type eq 'U' )
3448              && (
3449                      $cust_bill_pkg->recur != 0
3450                   || !defined($s)
3451                   || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3452                   || $cust_bill_pkg->recur_show_zero
3453                 )
3454            )
3455         {
3456
3457           warn "$me _items_cust_bill_pkg adding recur/usage\n"
3458             if $DEBUG > 1;
3459
3460           my $is_summary = $display->summary;
3461           my $description = $desc;
3462           if ( $type eq 'U' and defined($r) ) {
3463             # don't just show the same description as the recur line
3464             $description = $self->mt('Usage charges');
3465           }
3466
3467           my $part_pkg = $cust_pkg->part_pkg;
3468
3469           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3470                                                               $self->agentnum );
3471
3472           my @d = ();
3473           my @seconds = (); # for display of usage info
3474           my $svc_label = '';
3475
3476           #at least until cust_bill_pkg has "past" ranges in addition to
3477           #the "future" sdate/edate ones... see #3032
3478           my @dates = ( $self->_date );
3479           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3480           push @dates, $prev->sdate if $prev;
3481           push @dates, undef if !$prev;
3482
3483           my @svc_labels = map &{$escape_function}($_),
3484             $cust_pkg->h_labels_short(@dates,
3485                                       'I',
3486                                       $self->conf->{locale});
3487           $svc_label = $svc_labels[0];
3488
3489           # show service labels, unless...
3490                     # the package is set not to display them
3491           unless ( $part_pkg->hide_svc_detail
3492                     # or this is a tax-like line item
3493                 || $cust_bill_pkg->itemdesc
3494                     # or this is a hidden (bundled) line item
3495                 || $cust_bill_pkg->hidden
3496                     # or this is a usage summary line
3497                 || $is_summary && $type && $type eq 'U'
3498                     # or this is a usage line and there's a recurring line
3499                     # for the package in the same section (which will 
3500                     # have service labels already)
3501                 || ($type eq 'U' and defined($r))
3502               )
3503           {
3504
3505             warn "$me _items_cust_bill_pkg adding service details\n"
3506               if $DEBUG > 1;
3507
3508             push @d, @svc_labels
3509               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3510             warn "$me _items_cust_bill_pkg done adding service details\n"
3511               if $DEBUG > 1;
3512
3513             # show the location label if it's not the customer's default
3514             # location, and we're not grouping items by location already
3515             if ( $cust_pkg->locationnum != $default_locationnum
3516                   and !defined($locationnum) ) {
3517               my $loc = $cust_pkg->location_label;
3518               $loc = substr($loc, 0, $maxlength). '...'
3519                 if $format eq 'latex' && length($loc) > $maxlength;
3520               push @d, &{$escape_function}($loc);
3521             }
3522
3523             # Display of seconds_since_sqlradacct:
3524             # On the invoice, when processing @detail_items, look for a field
3525             # named 'seconds'.  This will contain total seconds for each 
3526             # service, in the same order as @ext_description.  For services 
3527             # that don't support this it will show undef.
3528             if ( $conf->exists('svc_acct-usage_seconds') 
3529                  and ! $cust_bill_pkg->pkgpart_override ) {
3530               foreach my $cust_svc ( 
3531                   $cust_pkg->h_cust_svc(@dates, 'I') 
3532                 ) {
3533
3534                 # eval because not having any part_export_usage exports 
3535                 # is a fatal error, last_bill/_date because that's how 
3536                 # sqlradius_hour billing does it
3537                 my $sec = eval {
3538                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
3539                 };
3540                 push @seconds, $sec;
3541               }
3542             } #if svc_acct-usage_seconds
3543
3544           } # if we are showing service labels
3545
3546           unless ( $is_summary ) {
3547             warn "$me _items_cust_bill_pkg adding details\n"
3548               if $DEBUG > 1;
3549
3550             #instead of omitting details entirely in this case (unwanted side
3551             # effects), just omit CDRs
3552             $details_opt{'no_usage'} = 1
3553               if $type && $type eq 'R';
3554
3555             push @d, $cust_bill_pkg->details(%details_opt);
3556           }
3557
3558           warn "$me _items_cust_bill_pkg calculating amount\n"
3559             if $DEBUG > 1;
3560   
3561           my $amount = 0;
3562           if (!$type) {
3563             $amount = $cust_bill_pkg->recur;
3564           } elsif ($type eq 'R') {
3565             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3566           } elsif ($type eq 'U') {
3567             $amount = $cust_bill_pkg->usage;
3568           }
3569   
3570           if ( !$type || $type eq 'R' ) {
3571
3572             warn "$me _items_cust_bill_pkg adding recur\n"
3573               if $DEBUG > 1;
3574
3575             my $unit_amount =
3576               ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
3577                                                 : $amount;
3578
3579             if ( $cust_bill_pkg->hidden ) {
3580               $r->{amount}      += $amount;
3581               $r->{unit_amount} += $unit_amount;
3582               push @{ $r->{ext_description} }, @d;
3583             } else {
3584               $r = {
3585                 description     => $description,
3586                 pkgpart         => $pkgpart,
3587                 pkgnum          => $cust_bill_pkg->pkgnum,
3588                 amount          => $amount,
3589                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3590                 unit_amount     => $unit_amount,
3591                 quantity        => $cust_bill_pkg->quantity,
3592                 %item_dates,
3593                 ext_description => \@d,
3594                 svc_label       => ($svc_label || ''),
3595                 locationnum     => $cust_pkg->locationnum,
3596               };
3597               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3598             }
3599
3600           } else {  # $type eq 'U'
3601
3602             warn "$me _items_cust_bill_pkg adding usage\n"
3603               if $DEBUG > 1;
3604
3605             if ( $cust_bill_pkg->hidden and defined($u) ) {
3606               # if this is a hidden package and there's already a usage
3607               # line for the bundle, add this package's total amount and
3608               # usage details to it
3609               $u->{amount}      += $amount;
3610               push @{ $u->{ext_description} }, @d;
3611             } elsif ( $amount ) {
3612               # create a new usage line
3613               $u = {
3614                 description     => $description,
3615                 pkgpart         => $pkgpart,
3616                 pkgnum          => $cust_bill_pkg->pkgnum,
3617                 amount          => $amount,
3618                 usage_item      => 1,
3619                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3620                 %item_dates,
3621                 ext_description => \@d,
3622                 locationnum     => $cust_pkg->locationnum,
3623               };
3624             } # else this has no usage, so don't create a usage section
3625           }
3626
3627         } # recurring or usage with recurring charge
3628
3629       } else { # taxes and fees
3630
3631         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3632           if $DEBUG > 1;
3633
3634         # items of this kind should normally not have sdate/edate.
3635         push @b, {
3636           'description' => $desc,
3637           'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
3638                                            + $cust_bill_pkg->recur)
3639         };
3640
3641       } # if quotation / package line item / other line item
3642
3643       # decide whether to show active discounts here
3644       if (
3645           # case 1: we are showing a single line for the package
3646           ( !$type )
3647           # case 2: we are showing a setup line for a package that has
3648           # no base recurring fee
3649           or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3650           # case 3: we are showing a recur line for a package that has 
3651           # a base recurring fee
3652           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3653       ) {
3654
3655         my $item_discount = $cust_bill_pkg->_item_discount;
3656         if ( $item_discount ) {
3657           # $item_discount->{amount} is negative
3658
3659           if ( $d and $cust_bill_pkg->hidden ) {
3660             $d->{amount}      += $item_discount->{amount};
3661           } else {
3662             $d = $item_discount;
3663             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
3664           }
3665
3666           # update the active line (before the discount) to show the 
3667           # original price (whether this is a hidden line or not)
3668           #
3669           # quotation discounts keep track of setup and recur; invoice 
3670           # discounts currently don't
3671           if ( exists $item_discount->{setup_amount} ) {
3672
3673             $s->{amount} -= $item_discount->{setup_amount} if $s;
3674             $r->{amount} -= $item_discount->{recur_amount} if $r;
3675
3676           } else {
3677
3678             # $active_line is the line item hashref for the line that will
3679             # show the original price
3680             # (use the recur or single line for the package, unless we're 
3681             # showing a setup line for a package with no recurring fee)
3682             my $active_line = $r;
3683             if ( $type eq 'S' ) {
3684               $active_line = $s;
3685             }
3686             $active_line->{amount} -= $item_discount->{amount};
3687
3688           }
3689
3690         } # if there are any discounts
3691       } # if this is an appropriate place to show discounts
3692
3693     } # foreach $display
3694
3695   }
3696
3697   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3698     if ( $_  ) {
3699       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3700         if exists($_->{amount});
3701       $_->{amount}      =~ s/^\-0\.00$/0.00/;
3702       if (exists($_->{unit_amount})) {
3703         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3704       }
3705
3706       push @b, { %$_ };
3707       #if $_->{amount} != 0
3708       #  || $discount_show_always
3709       #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3710       #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3711     }
3712   }
3713
3714   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3715     if $DEBUG > 1;
3716
3717   @b;
3718
3719 }
3720
3721 =item _items_discounts_avail
3722
3723 Returns an array of line item hashrefs representing available term discounts
3724 for this invoice.  This makes the same assumptions that apply to term 
3725 discounts in general: that the package is billed monthly, at a flat rate, 
3726 with no usage charges.  A prorated first month will be handled, as will 
3727 a setup fee if the discount is allowed to apply to setup fees.
3728
3729 =cut
3730
3731 sub _items_discounts_avail {
3732   my $self = shift;
3733
3734   #maybe move this method from cust_bill when quotations support discount_plans 
3735   return () unless $self->can('discount_plans');
3736   my %plans = $self->discount_plans;
3737
3738   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3739   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3740
3741   map {
3742     my $months = $_;
3743     my $plan = $plans{$months};
3744
3745     my $term_total = sprintf('%.2f', $plan->discounted_total);
3746     my $percent = sprintf('%.0f', 
3747                           100 * (1 - $term_total / $plan->base_total) );
3748     my $permonth = sprintf('%.2f', $term_total / $months);
3749     my $detail = $self->mt('discount on item'). ' '.
3750                  join(', ', map { "#$_" } $plan->pkgnums)
3751       if $list_pkgnums;
3752
3753     # discounts for non-integer months don't work anyway
3754     $months = sprintf("%d", $months);
3755
3756     +{
3757       description => $self->mt('Save [_1]% by paying for [_2] months',
3758                                 $percent, $months),
3759       amount      => $self->mt('[_1] ([_2] per month)', 
3760                                 $term_total, $money_char.$permonth),
3761       ext_description => ($detail || ''),
3762     }
3763   } #map
3764   sort { $b <=> $a } keys %plans;
3765
3766 }
3767
3768 1;