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