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