show a total range for prorate quotations
[freeside.git] / FS / FS / Template_Mixin.pm
1 package FS::Template_Mixin;
2
3 use strict;
4 use vars qw( $DEBUG $me
5              $money_char
6              $date_format
7            );
8              # but NOT $conf
9 use vars qw( $invoice_lines @buf ); #yuck
10 use List::Util qw(sum); #can't import first, it conflicts with cust_main.first
11 use Date::Format;
12 use Date::Language;
13 use Time::Local qw( timelocal );
14 use Text::Template 1.20;
15 use File::Temp 0.14;
16 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
17 use IO::Scalar;
18 use HTML::Entities;
19 use Cwd;
20 use FS::UID;
21 use FS::Misc qw( send_email );
22 use FS::Record qw( qsearch qsearchs );
23 use FS::Conf;
24 use FS::Misc qw( generate_ps generate_pdf );
25 use FS::pkg_category;
26 use FS::pkg_class;
27 use FS::invoice_mode;
28 use FS::L10N;
29
30 $DEBUG = 0;
31 $me = '[FS::Template_Mixin]';
32 FS::UID->install_callback( sub { 
33   my $conf = new FS::Conf; #global
34   $money_char  = $conf->config('money_char')  || '$';  
35   $date_format = $conf->config('date_format') || '%x'; #/YY
36 } );
37
38 =item conf [ MODE ]
39
40 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
41
42 If the "mode" pseudo-field is set on the object, the configuration handle
43 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
44 locale).
45
46 =cut
47
48 sub conf {
49   my $self = shift;
50   my $mode = $self->get('mode');
51   if ($self->{_conf} and !defined($mode)) {
52     return $self->{_conf};
53   }
54
55   my $cust_main = $self->cust_main;
56   my $locale = $cust_main ? $cust_main->locale : '';
57   my $conf;
58   if ( $mode ) {
59     if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
60       $mode = $mode->modenum;
61     } elsif ( $mode =~ /\D/ ) {
62       die "invalid invoice mode $mode";
63     }
64     $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
65     if (!$conf) {
66       $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
67       # it doesn't have a locale, but system conf still might
68       $conf->set('locale' => $locale) if $conf;
69     }
70   }
71   # if $mode is unspecified, or if there is no invoice_conf matching this mode
72   # and locale, then use the system config only (but with the locale)
73   $conf ||= FS::Conf->new({ 'locale' => $locale });
74   # cache it
75   return $self->{_conf} = $conf;
76 }
77
78 =item print_text OPTIONS
79
80 Returns an text invoice, as a list of lines.
81
82 Options can be passed as a hash.
83
84 I<time>, if specified, is used to control the printing of overdue messages.  The
85 default is now.  It isn't the date of the invoice; that's the `_date' field.
86 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
87 L<Time::Local> and L<Date::Parse> for conversion functions.
88
89 I<template>, if specified, is the name of a suffix for alternate invoices.
90
91 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
92
93 =cut
94
95 sub print_text {
96   my $self = shift;
97   my %params;
98   if ( ref($_[0]) ) {
99     %params = %{ shift() };
100   } else {
101     %params = @_;
102   }
103
104   $params{'format'} = 'template'; # for some reason
105
106   $self->print_generic( %params );
107 }
108
109 =item print_latex HASHREF
110
111 Internal method - returns a filename of a filled-in LaTeX template for this
112 invoice (Note: add ".tex" to get the actual filename), and a filename of
113 an associated logo (with the .eps extension included).
114
115 See print_ps and print_pdf for methods that return PostScript and PDF output.
116
117 Options can be passed as a hash.
118
119 I<time>, if specified, is used to control the printing of overdue messages.  The
120 default is now.  It isn't the date of the invoice; that's the `_date' field.
121 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
122 L<Time::Local> and L<Date::Parse> for conversion functions.
123
124 I<template>, if specified, is the name of a suffix for alternate invoices.  
125 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
126 customize invoice templates for different purposes.
127
128 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
129
130 =cut
131
132 sub print_latex {
133   my $self = shift;
134   my %params;
135
136   if ( ref($_[0]) ) {
137     %params = %{ shift() };
138   } else {
139     %params = @_;
140   }
141
142   $params{'format'} = 'latex';
143   my $conf = $self->conf;
144
145   # this needs to go away
146   my $template = $params{'template'};
147   # and this especially
148   $template ||= $self->_agent_template
149     if $self->can('_agent_template');
150
151   #the new way
152   $self->set('mode', $params{mode})
153     if $params{mode};
154
155   my $pkey = $self->primary_key;
156   my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
157
158   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
159   my $lh = new File::Temp(
160     TEMPLATE => $tmp_template,
161     DIR      => $dir,
162     SUFFIX   => '.eps',
163     UNLINK   => 0,
164   ) or die "can't open temp file: $!\n";
165
166   my $agentnum = $self->agentnum;
167
168   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
169     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
170       or die "can't write temp file: $!\n";
171   } else {
172     print $lh $conf->config_binary('logo.eps', $agentnum)
173       or die "can't write temp file: $!\n";
174   }
175   close $lh;
176   $params{'logo_file'} = $lh->filename;
177
178   if( $conf->exists('invoice-barcode') 
179         && $self->can('invoice_barcode')
180         && $self->invnum ) { # don't try to barcode statements
181       my $png_file = $self->invoice_barcode($dir);
182       my $eps_file = $png_file;
183       $eps_file =~ s/\.png$/.eps/g;
184       $png_file =~ /(barcode.*png)/;
185       $png_file = $1;
186       $eps_file =~ /(barcode.*eps)/;
187       $eps_file = $1;
188
189       my $curr_dir = cwd();
190       chdir($dir); 
191       # after painfuly long experimentation, it was determined that sam2p won't
192       # accept : and other chars in the path, no matter how hard I tried to
193       # escape them, hence the chdir (and chdir back, just to be safe)
194       system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
195         or die "sam2p failed: $!\n";
196       unlink($png_file);
197       chdir($curr_dir);
198
199       $params{'barcode_file'} = $eps_file;
200   }
201
202   my @filled_in = $self->print_generic( %params );
203   
204   my $fh = new File::Temp( TEMPLATE => $tmp_template,
205                            DIR      => $dir,
206                            SUFFIX   => '.tex',
207                            UNLINK   => 0,
208                          ) or die "can't open temp file: $!\n";
209   binmode($fh, ':utf8'); # language support
210   print $fh join('', @filled_in );
211   close $fh;
212
213   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
214   return ($1, $params{'logo_file'}, $params{'barcode_file'});
215
216 }
217
218 sub agentnum {
219   my $self = shift;
220   my $cust_main = $self->cust_main;
221   $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
222 }
223
224 =item print_generic OPTION => VALUE ...
225
226 Internal method - returns a filled-in template for this invoice as a scalar.
227
228 See print_ps and print_pdf for methods that return PostScript and PDF output.
229
230 Required options
231
232 =over 4
233
234 =item format
235
236 The B<format> option is required and should be set to html, latex (print and PDF) or template (plaintext).
237
238 =back
239
240 Additional options
241
242 =over 4
243
244 =item notice_name
245
246 Overrides "Invoice" as the name of the sent document.
247
248 =item today
249
250 Used to control the printing of overdue messages.  The
251 default is now.  It isn't the date of the invoice; that's the `_date' field.
252 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
253 L<Time::Local> and L<Date::Parse> for conversion functions.
254
255 =item logo_file
256
257 Logo file (path to temporary EPS file on the local filesystem)
258
259 =item cid
260
261 CID for inline (emailed) images (logo)
262
263 =item locale
264
265 Override customer's locale
266
267 =item unsquelch_cdr
268
269 Overrides any per customer cdr squelching when true
270
271 =item no_number
272
273 Supress the (invoice, quotation, statement, etc.) number
274
275 =item no_date
276
277 Supress the date
278
279 =item no_coupon
280
281 Supress the payment coupon
282
283 =item barcode_file
284
285 Barcode file (path to temporary EPS file on the local filesystem)
286
287 =item barcode_img
288
289 Flag indicating the barcode image should be a link (normal HTML dipaly)
290
291 =item barcode_cid
292
293 Barcode CID for inline (emailed) images
294
295 =item preref_callback
296
297 Coderef run for each line item, code should return HTML to be displayed
298 before that line item (quotations only)
299
300 =item template
301
302 Dprecated.  Used as a suffix for a configuration template.  Please 
303 don't use this, it deprecated in favor of more flexible alternatives.
304
305 =back
306
307 =cut
308
309 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
310 # (alignment in text invoice?) problems to change them all to '%.2f' ?
311 # yes: fixed width/plain text printing will be borked
312 sub print_generic {
313   my( $self, %params ) = @_;
314   my $conf = $self->conf;
315
316   my $today = $params{today} ? $params{today} : time;
317   warn "$me print_generic called on $self with suffix $params{template}\n"
318     if $DEBUG;
319
320   my $format = $params{format};
321   die "Unknown format: $format"
322     unless $format =~ /^(latex|html|template)$/;
323
324   my $cust_main = $self->cust_main || $self->prospect_main;
325   $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
1379       if ( ref($amount) ) {
1380         $_->{'total_amount'} = &$embolden_function(
1381                                  $other_money_char.$amount->[0]. ' to '.
1382                                  $other_money_char.$amount->[1]
1383                                );
1384       } else {
1385       $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
1386       }
1387
1388       # but if it's multisection, don't append to @total_items. the adjust
1389       # section has all this stuff
1390       push @total_items, $_ if !$multisection;
1391       push @buf, [ $item, $money_char.sprintf('%10.2f',$amount) ];
1392     }
1393
1394     push @buf, [ '', '' ];
1395
1396     # if we're showing previous invoices, also show previous
1397     # credits and payments 
1398     if ( $self->enable_previous 
1399           and $self->can('_items_credits')
1400           and $self->can('_items_payments') )
1401       {
1402     
1403       # credits
1404       my $credittotal = 0;
1405       foreach my $credit (
1406         $self->_items_credits( 'template' => $template, 'trim_len' => 40 )
1407       ) {
1408
1409         my $total;
1410         $total->{'total_item'} = &$escape_function($credit->{'description'});
1411         $credittotal += $credit->{'amount'};
1412         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1413         if ( $multisection ) {
1414           push @detail_items, {
1415             ext_description => [],
1416             ref          => '',
1417             quantity     => '',
1418             description  => &$escape_function($credit->{'description'}),
1419             amount       => $money_char . $credit->{'amount'},
1420             product_code => '',
1421             section      => $adjust_section,
1422           };
1423         } else {
1424           push @total_items, $total;
1425         }
1426
1427       }
1428       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1429
1430       #credits (again)
1431       foreach my $credit (
1432         $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1433       ) {
1434         push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1435       }
1436
1437       # payments
1438       my $paymenttotal = 0;
1439       foreach my $payment (
1440         $self->_items_payments( 'template' => $template )
1441       ) {
1442         my $total = {};
1443         $total->{'total_item'} = &$escape_function($payment->{'description'});
1444         $paymenttotal += $payment->{'amount'};
1445         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1446         if ( $multisection ) {
1447           push @detail_items, {
1448             ext_description => [],
1449             ref          => '',
1450             quantity     => '',
1451             description  => &$escape_function($payment->{'description'}),
1452             amount       => $money_char . $payment->{'amount'},
1453             product_code => '',
1454             section      => $adjust_section,
1455           };
1456         }else{
1457           push @total_items, $total;
1458         }
1459         push @buf, [ $payment->{'description'},
1460                      $money_char. sprintf("%10.2f", $payment->{'amount'}),
1461                    ];
1462       }
1463       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1464     
1465       if ( $multisection ) {
1466         $adjust_section->{'subtotal'} = $other_money_char.
1467                                         sprintf('%.2f', $credittotal + $paymenttotal);
1468
1469         #why this? because {sort_weight} forces the adjust_section to appear
1470         #in @extra_sections instead of @sections. obviously.
1471         push @sections, $adjust_section
1472           unless $adjust_section->{sort_weight};
1473         # do not summarize; adjustments there are shown according to 
1474         # different rules
1475       }
1476
1477       # create Balance Due message
1478       { 
1479         my $total;
1480         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1481         $total->{'total_amount'} =
1482           &$embolden_function(
1483             $other_money_char. sprintf('%.2f', #why? $summarypage 
1484                                                #  ? $self->charged +
1485                                                #    $self->billing_balance
1486                                                #  :
1487                                                    $self->owed + $pr_total
1488                                       )
1489           );
1490         if ( $multisection && !$adjust_section->{sort_weight} ) {
1491           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1492                                            $total->{'total_amount'};
1493         } else {
1494           push @total_items, $total;
1495         }
1496         push @buf,['','-----------'];
1497         push @buf,[$self->balance_due_msg, $money_char. 
1498           sprintf("%10.2f", $balance_due ) ];
1499       }
1500
1501       if ( $conf->exists('previous_balance-show_credit')
1502           and $cust_main->balance < 0 ) {
1503         my $credit_total = {
1504           'total_item'    => &$embolden_function($self->credit_balance_msg),
1505           'total_amount'  => &$embolden_function(
1506             $other_money_char. sprintf('%.2f', -$cust_main->balance)
1507           ),
1508         };
1509         if ( $multisection ) {
1510           $adjust_section->{'posttotal'} .= $newline_token .
1511             $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1512         }
1513         else {
1514           push @total_items, $credit_total;
1515         }
1516         push @buf,['','-----------'];
1517         push @buf,[$self->credit_balance_msg, $money_char. 
1518           sprintf("%10.2f", -$cust_main->balance ) ];
1519       }
1520     }
1521
1522   } #end of default total adding ! can('_items_total')
1523
1524   if ( $multisection ) {
1525     if (    $conf->exists('svc_phone_sections')
1526          && $self->can('_items_svc_phone_sections')
1527        )
1528     {
1529       my $total;
1530       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1531       $total->{'total_amount'} =
1532         &$embolden_function(
1533           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1534         );
1535       my $last_section = pop @sections;
1536       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1537                                      $total->{'total_amount'};
1538       push @sections, $last_section;
1539     }
1540     push @sections, @$late_sections
1541       if $unsquelched;
1542   }
1543
1544   # make a discounts-available section, even without multisection
1545   if ( $conf->exists('discount-show_available') 
1546        and my @discounts_avail = $self->_items_discounts_avail ) {
1547     my $discount_section = {
1548       'description' => $self->mt('Discounts Available'),
1549       'subtotal'    => '',
1550       'no_subtotal' => 1,
1551     };
1552
1553     push @sections, $discount_section; # do not summarize
1554     push @detail_items, map { +{
1555         'ref'         => '', #should this be something else?
1556         'section'     => $discount_section,
1557         'description' => &$escape_function( $_->{description} ),
1558         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1559         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1560     } } @discounts_avail;
1561   }
1562
1563   # not adding any more sections after this
1564   $invoice_data{summary_subtotals} = \@summary_subtotals;
1565
1566   # usage subtotals
1567   if ( $conf->exists('usage_class_summary')
1568        and $self->can('_items_usage_class_summary') ) {
1569     my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char);
1570     if ( @usage_subtotals ) {
1571       unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1572       unshift @detail_items, @usage_subtotals;
1573     }
1574   }
1575
1576   # invoice history "section" (not really a section)
1577   # not to be included in any subtotals, completely independent of 
1578   # everything...
1579   if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
1580     my %history;
1581     my %monthorder;
1582     foreach my $cust_bill ( $cust_main->cust_bill ) {
1583       # XXX hardcoded format, and currently only 'charged'; add other fields
1584       # if they become necessary
1585       my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1586       $history{$date} ||= 0;
1587       $history{$date} += $cust_bill->charged;
1588       # just so we have a numeric sort key
1589       $monthorder{$date} ||= $cust_bill->_date;
1590     }
1591     my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1592                         keys %history;
1593     my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1594     $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1595   }
1596
1597   # service locations: another option for template customization
1598   my %location_info;
1599   foreach my $item (@detail_items) {
1600     if ( $item->{locationnum} ) {
1601       $location_info{ $item->{locationnum} } ||= {
1602         FS::cust_location->by_key( $item->{locationnum} )->location_hash
1603       };
1604     }
1605   }
1606   $invoice_data{location_info} = \%location_info;
1607
1608   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1609   # the invoice variables
1610   return \%invoice_data if ( $params{'diag'} );
1611
1612   # All sections and items are built; now fill in templates.
1613   my @includelist = ();
1614   push @includelist, 'summary' if $summarypage;
1615   foreach my $include ( @includelist ) {
1616
1617     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1618     my @inc_src;
1619
1620     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1621
1622       @inc_src = $conf->config($inc_file, $agentnum);
1623
1624     } else {
1625
1626       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1627
1628       my $convert_map = $convert_maps{$format}{$include};
1629
1630       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1631                        s/--\@\]/$delimiters{$format}[1]/g;
1632                        $_;
1633                      } 
1634                  &$convert_map( $conf->config($inc_file, $agentnum) );
1635
1636     }
1637
1638     my $inc_tt = new Text::Template (
1639       TYPE       => 'ARRAY',
1640       SOURCE     => [ map "$_\n", @inc_src ],
1641       DELIMITERS => $delimiters{$format},
1642     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1643
1644     unless ( $inc_tt->compile() ) {
1645       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1646       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1647       die $error;
1648     }
1649
1650     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1651
1652     $invoice_data{$include} =~ s/\n+$//
1653       if ($format eq 'latex');
1654   }
1655
1656   $invoice_lines = 0;
1657   my $wasfunc = 0;
1658   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1659     /invoice_lines\((\d*)\)/;
1660     $invoice_lines += $1 || scalar(@buf);
1661     $wasfunc=1;
1662   }
1663   die "no invoice_lines() functions in template?"
1664     if ( $format eq 'template' && !$wasfunc );
1665
1666   if ( $invoice_lines ) {
1667     $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1668     $invoice_data{'total_pages'}++
1669       if scalar(@buf) % $invoice_lines;
1670   }
1671
1672   #setup subroutine for the template
1673   $invoice_data{invoice_lines} = sub {
1674     my $lines = shift || scalar(@buf);
1675     map { 
1676       scalar(@buf)
1677         ? shift @buf
1678         : [ '', '' ];
1679     }
1680     ( 1 .. $lines );
1681   };
1682
1683   if ($format eq 'template') {
1684
1685     my $lines;
1686     my @collect;
1687     while (@buf) {
1688       push @collect, split("\n",
1689         $text_template->fill_in( HASH => \%invoice_data )
1690       );
1691       $invoice_data{'page'}++;
1692     }
1693     map "$_\n", @collect;
1694
1695   } else { # this is where we actually create the invoice
1696
1697     if ( $params{no_addresses} ) {
1698       delete $invoice_data{$_} foreach qw(
1699         payname company address1 address2 city state zip country
1700       );
1701       $invoice_data{returnaddress} = '~';
1702     }
1703
1704     warn "filling in template for invoice ". $self->invnum. "\n"
1705       if $DEBUG;
1706     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1707       if $DEBUG > 1;
1708
1709     $text_template->fill_in(HASH => \%invoice_data);
1710   }
1711 }
1712
1713 sub notice_name { '('.shift->table.')'; }
1714
1715 sub template_conf { 'invoice_'; }
1716
1717 # helper routine for generating date ranges
1718 sub _prior_month30s {
1719   my $self = shift;
1720   my @ranges = (
1721    [ 1,       2592000 ], # 0-30 days ago
1722    [ 2592000, 5184000 ], # 30-60 days ago
1723    [ 5184000, 7776000 ], # 60-90 days ago
1724    [ 7776000, 0       ], # 90+   days ago
1725   );
1726
1727   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1728           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1729       ] }
1730   @ranges;
1731 }
1732
1733 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1734
1735 Returns an postscript invoice, as a scalar.
1736
1737 Options can be passed as a hashref (recommended) or as a list of time, template
1738 and then any key/value pairs for any other options.
1739
1740 I<time> an optional value used to control the printing of overdue messages.  The
1741 default is now.  It isn't the date of the invoice; that's the `_date' field.
1742 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1743 L<Time::Local> and L<Date::Parse> for conversion functions.
1744
1745 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1746
1747 =cut
1748
1749 sub print_ps {
1750   my $self = shift;
1751
1752   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1753   my $ps = generate_ps($file);
1754   unlink($logofile);
1755   unlink($barcodefile) if $barcodefile;
1756
1757   $ps;
1758 }
1759
1760 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1761
1762 Returns an PDF invoice, as a scalar.
1763
1764 Options can be passed as a hashref (recommended) or as a list of time, template
1765 and then any key/value pairs for any other options.
1766
1767 I<time> an optional value used to control the printing of overdue messages.  The
1768 default is now.  It isn't the date of the invoice; that's the `_date' field.
1769 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1770 L<Time::Local> and L<Date::Parse> for conversion functions.
1771
1772 I<template>, if specified, is the name of a suffix for alternate invoices.
1773
1774 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1775
1776 =cut
1777
1778 sub print_pdf {
1779   my $self = shift;
1780
1781   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1782   my $pdf = generate_pdf($file);
1783   unlink($logofile);
1784   unlink($barcodefile) if $barcodefile;
1785
1786   $pdf;
1787 }
1788
1789 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1790
1791 Returns an HTML invoice, as a scalar.
1792
1793 I<time> an optional value used to control the printing of overdue messages.  The
1794 default is now.  It isn't the date of the invoice; that's the `_date' field.
1795 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1796 L<Time::Local> and L<Date::Parse> for conversion functions.
1797
1798 I<template>, if specified, is the name of a suffix for alternate invoices.
1799
1800 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1801
1802 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1803 when emailing the invoice as part of a multipart/related MIME email.
1804
1805 =cut
1806
1807 sub print_html {
1808   my $self = shift;
1809   my %params;
1810   if ( ref($_[0]) ) {
1811     %params = %{ shift() }; 
1812   } else {
1813     %params = @_;
1814   }
1815   $params{'format'} = 'html';
1816   
1817   $self->print_generic( %params );
1818 }
1819
1820 # quick subroutine for print_latex
1821 #
1822 # There are ten characters that LaTeX treats as special characters, which
1823 # means that they do not simply typeset themselves: 
1824 #      # $ % & ~ _ ^ \ { }
1825 #
1826 # TeX ignores blanks following an escaped character; if you want a blank (as
1827 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1828
1829 sub _latex_escape {
1830   my $value = shift;
1831   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1832   $value =~ s/([<>])/\$$1\$/g;
1833   $value;
1834 }
1835
1836 sub _html_escape {
1837   my $value = shift;
1838   encode_entities($value);
1839   $value;
1840 }
1841
1842 sub _html_escape_nbsp {
1843   my $value = _html_escape(shift);
1844   $value =~ s/ +/&nbsp;/g;
1845   $value;
1846 }
1847
1848 #utility methods for print_*
1849
1850 sub _translate_old_latex_format {
1851   warn "_translate_old_latex_format called\n"
1852     if $DEBUG; 
1853
1854   my @template = ();
1855   while ( @_ ) {
1856     my $line = shift;
1857   
1858     if ( $line =~ /^%%Detail\s*$/ ) {
1859   
1860       push @template, q![@--!,
1861                       q!  foreach my $_tr_line (@detail_items) {!,
1862                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1863                       q!      $_tr_line->{'description'} .= !, 
1864                       q!        "\\tabularnewline\n~~".!,
1865                       q!        join( "\\tabularnewline\n~~",!,
1866                       q!          @{$_tr_line->{'ext_description'}}!,
1867                       q!        );!,
1868                       q!    }!;
1869
1870       while ( ( my $line_item_line = shift )
1871               !~ /^%%EndDetail\s*$/                            ) {
1872         $line_item_line =~ s/'/\\'/g;    # nice LTS
1873         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1874         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1875         push @template, "    \$OUT .= '$line_item_line';";
1876       }
1877
1878       push @template, '}',
1879                       '--@]';
1880       #' doh, gvim
1881     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1882
1883       push @template, '[@--',
1884                       '  foreach my $_tr_line (@total_items) {';
1885
1886       while ( ( my $total_item_line = shift )
1887               !~ /^%%EndTotalDetails\s*$/                      ) {
1888         $total_item_line =~ s/'/\\'/g;    # nice LTS
1889         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1890         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1891         push @template, "    \$OUT .= '$total_item_line';";
1892       }
1893
1894       push @template, '}',
1895                       '--@]';
1896
1897     } else {
1898       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1899       push @template, $line;  
1900     }
1901   
1902   }
1903
1904   if ($DEBUG) {
1905     warn "$_\n" foreach @template;
1906   }
1907
1908   (@template);
1909 }
1910
1911 =item terms
1912
1913 =cut
1914
1915 sub terms {
1916   my $self = shift;
1917   my $conf = $self->conf;
1918
1919   #check for an invoice-specific override
1920   return $self->invoice_terms if $self->invoice_terms;
1921   
1922   #check for a customer- specific override
1923   my $cust_main = $self->cust_main;
1924   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1925
1926   my $agentnum = '';
1927   if ( $cust_main ) {
1928     $agentnum = $cust_main->agentnum;
1929   } elsif ( my $prospect_main = $self->prospect_main ) {
1930     $agentnum = $prospect_main->agentnum;
1931   }
1932
1933   #use configured default
1934   $conf->config('invoice_default_terms', $agentnum) || '';
1935 }
1936
1937 =item due_date
1938
1939 =cut
1940
1941 sub due_date {
1942   my $self = shift;
1943   my $duedate = '';
1944   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1945     $duedate = $self->_date() + ( $1 * 86400 );
1946   } elsif ( $self->terms =~ /^End of Month$/ ) {
1947     my ($mon,$year) = (localtime($self->_date) )[4,5];
1948     $mon++;
1949     until ( $mon < 12 ) { $mon -= 12; $year++; }
1950     my $nextmonth_first = timelocal(0,0,0,1,$mon,$year);
1951     $duedate = $nextmonth_first - 86400;
1952   }
1953   $duedate;
1954 }
1955
1956 =item due_date2str
1957
1958 =cut
1959
1960 sub due_date2str {
1961   my $self = shift;
1962   $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1963 }
1964
1965 =item balance_due_msg
1966
1967 =cut
1968
1969 sub balance_due_msg {
1970   my $self = shift;
1971   my $msg = $self->mt('Balance Due');
1972   return $msg unless $self->terms; # huh?
1973   if ( !$self->conf->exists('invoice_show_prior_due_date')
1974        or $self->conf->exists('invoice_sections') ) {
1975     # if enabled, the due date is shown with Total New Charges (see 
1976     # _items_total) and not here
1977     # (yes, or if invoice_sections is enabled; this is just for compatibility)
1978     if ( $self->due_date ) {
1979       my $please_pay_by =
1980         $self->conf->config('invoice_pay_by_msg', $self->agentnum)
1981         || 'Please pay by [_1]';
1982       $msg .= ' - ' . $self->mt($please_pay_by, $self->due_date2str('short')).
1983               ' '
1984        unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum);
1985     } elsif ( $self->terms ) {
1986       $msg .= ' - '. $self->mt($self->terms);
1987     }
1988   }
1989   $msg;
1990 }
1991
1992 =item balance_due_date
1993
1994 =cut
1995
1996 sub balance_due_date {
1997   my $self = shift;
1998   my $conf = $self->conf;
1999   my $duedate = '';
2000   my $terms = $self->terms;
2001   if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2002     $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
2003   }
2004   $duedate;
2005 }
2006
2007 sub credit_balance_msg { 
2008   my $self = shift;
2009   $self->mt('Credit Balance Remaining')
2010 }
2011
2012 =item _date_pretty
2013
2014 Returns a string with the date, for example: "3/20/2008", localized for the
2015 customer.  Use _date_pretty_unlocalized for non-end-customer display use.
2016
2017 =cut
2018
2019 sub _date_pretty {
2020   my $self = shift;
2021   $self->time2str_local('short', $self->_date);
2022 }
2023
2024 =item _date_pretty_unlocalized
2025
2026 Returns a string with the date, for example: "3/20/2008", in the format
2027 configured for the back-office.  Use _date_pretty for end-customer display use.
2028
2029 =cut
2030
2031 sub _date_pretty_unlocalized {
2032   my $self = shift;
2033   time2str($date_format, $self->_date);
2034 }
2035
2036 =item email HASHREF
2037
2038 Emails this template.
2039
2040 Options are passed as a hashref.  Available options:
2041
2042 =over 4
2043
2044 =item from
2045
2046 If specified, overrides the default From: address.
2047
2048 =item notice_name
2049
2050 If specified, overrides the name of the sent document ("Invoice" or "Quotation")
2051
2052 =item template
2053
2054 (Deprecated) If specified, is the name of a suffix for alternate template files.
2055
2056 =back
2057
2058 Options accepted by generate_email can also be used.
2059
2060 =cut
2061
2062 sub email {
2063   my $self = shift;
2064   my $opt = shift || {};
2065   if ($opt and !ref($opt)) {
2066     die ref($self). '->email called with positional parameters';
2067   }
2068
2069   return if $self->hide;
2070
2071   my $error = send_email(
2072     $self->generate_email(
2073       'subject'     => $self->email_subject($opt->{template}),
2074       %$opt, # template, etc.
2075     )
2076   );
2077
2078   die "can't email: $error\n" if $error;
2079 }
2080
2081 =item generate_email OPTION => VALUE ...
2082
2083 Options:
2084
2085 =over 4
2086
2087 =item from
2088
2089 sender address, required
2090
2091 =item template
2092
2093 alternate template name, optional
2094
2095 =item subject
2096
2097 email subject, optional
2098
2099 =item notice_name
2100
2101 notice name instead of "Invoice", optional
2102
2103 =back
2104
2105 Returns an argument list to be passed to L<FS::Misc::send_email>.
2106
2107 =cut
2108
2109 use MIME::Entity;
2110
2111 sub generate_email {
2112
2113   my $self = shift;
2114   my %args = @_;
2115   my $conf = $self->conf;
2116
2117   my $me = '[FS::Template_Mixin::generate_email]';
2118
2119   my %return = (
2120     'from'      => $args{'from'},
2121     'subject'   => ($args{'subject'} || $self->email_subject),
2122     'custnum'   => $self->custnum,
2123     'msgtype'   => 'invoice',
2124   );
2125
2126   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
2127
2128   my $cust_main = $self->cust_main;
2129
2130   if (ref($args{'to'}) eq 'ARRAY') {
2131     $return{'to'} = $args{'to'};
2132   } elsif ( $cust_main ) {
2133     $return{'to'} = [ $cust_main->invoicing_list_emailonly ];
2134   }
2135
2136   my $tc = $self->template_conf;
2137
2138   my @text; # array of lines
2139   my $html; # a big string
2140   my @related_parts; # will contain the text/HTML alternative, and images
2141   my $related; # will contain the multipart/related object
2142
2143   if ( $conf->exists($tc. 'email_pdf') ) {
2144     if ( my $msgnum = $conf->config($tc.'email_pdf_msgnum') ) {
2145
2146       warn "$me using '${tc}email_pdf_msgnum' in multipart message"
2147         if $DEBUG;
2148
2149       my $msg_template = FS::msg_template->by_key($msgnum)
2150         or die "${tc}email_pdf_msgnum $msgnum not found\n";
2151       my %prepared = $msg_template->prepare(
2152         cust_main => $self->cust_main,
2153         object    => $self
2154       );
2155
2156       @text = split(/(?=\n)/, $prepared{'text_body'});
2157       $html = $prepared{'html_body'};
2158
2159     } elsif ( my @note = $conf->config($tc.'email_pdf_note') ) {
2160
2161       warn "$me using '${tc}email_pdf_note' in multipart message"
2162         if $DEBUG;
2163       @text = $conf->config($tc.'email_pdf_note');
2164       $html = join('<BR>', @text);
2165   
2166     } # else use the plain text invoice
2167   }
2168
2169   if (!@text) {
2170
2171     if ( $conf->config($tc.'template') ) {
2172
2173       warn "$me generating plain text invoice"
2174         if $DEBUG;
2175
2176       # 'print_text' argument is no longer used
2177       @text = $self->print_text(\%args);
2178
2179     } else {
2180
2181       warn "$me no plain text version exists; sending empty message body"
2182         if $DEBUG;
2183
2184     }
2185
2186   }
2187
2188   my $text_part = build MIME::Entity (
2189     'Type'        => 'text/plain',
2190     'Encoding'    => 'quoted-printable',
2191     'Charset'     => 'UTF-8',
2192     #'Encoding'    => '7bit',
2193     'Data'        => \@text,
2194     'Disposition' => 'inline',
2195   );
2196
2197   if (!$html) {
2198
2199     if ( $conf->exists($tc.'html') ) {
2200       warn "$me generating HTML invoice"
2201         if $DEBUG;
2202
2203       $args{'from'} =~ /\@([\w\.\-]+)/;
2204       my $from = $1 || 'example.com';
2205       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2206
2207       my $logo;
2208       my $agentnum = $cust_main ? $cust_main->agentnum
2209                                 : $self->prospect_main->agentnum;
2210       if ( defined($args{'template'}) && length($args{'template'})
2211            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
2212          )
2213       {
2214         $logo = 'logo_'. $args{'template'}. '.png';
2215       } else {
2216         $logo = "logo.png";
2217       }
2218       my $image_data = $conf->config_binary( $logo, $agentnum);
2219
2220       push @related_parts, build MIME::Entity
2221         'Type'       => 'image/png',
2222         'Encoding'   => 'base64',
2223         'Data'       => $image_data,
2224         'Filename'   => 'logo.png',
2225         'Content-ID' => "<$content_id>",
2226       ;
2227    
2228       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
2229         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
2230         push @related_parts, build MIME::Entity
2231           'Type'       => 'image/png',
2232           'Encoding'   => 'base64',
2233           'Data'       => $self->invoice_barcode(0),
2234           'Filename'   => 'barcode.png',
2235           'Content-ID' => "<$barcode_content_id>",
2236         ;
2237         $args{'barcode_cid'} = $barcode_content_id;
2238       }
2239
2240       $html = $self->print_html({ 'cid'=>$content_id, %args });
2241     }
2242
2243   }
2244
2245   if ( $html ) {
2246
2247     warn "$me creating HTML/text multipart message"
2248       if $DEBUG;
2249
2250     $return{'nobody'} = 1;
2251
2252     my $alternative = build MIME::Entity
2253       'Type'        => 'multipart/alternative',
2254       #'Encoding'    => '7bit',
2255       'Disposition' => 'inline'
2256     ;
2257
2258     if ( @text ) {
2259       $alternative->add_part($text_part);
2260     }
2261
2262     $alternative->attach(
2263       'Type'        => 'text/html',
2264       'Encoding'    => 'quoted-printable',
2265       'Data'        => [ '<html>',
2266                          '  <head>',
2267                          '    <title>',
2268                          '      '. encode_entities($return{'subject'}), 
2269                          '    </title>',
2270                          '  </head>',
2271                          '  <body bgcolor="#e8e8e8">',
2272                          $html,
2273                          '  </body>',
2274                          '</html>',
2275                        ],
2276       'Disposition' => 'inline',
2277       #'Filename'    => 'invoice.pdf',
2278     );
2279
2280     unshift @related_parts, $alternative;
2281
2282     $related = build MIME::Entity 'Type'     => 'multipart/related',
2283                                   'Encoding' => '7bit';
2284
2285     #false laziness w/Misc::send_email
2286     $related->head->replace('Content-type',
2287       $related->mime_type.
2288       '; boundary="'. $related->head->multipart_boundary. '"'.
2289       '; type=multipart/alternative'
2290     );
2291
2292     $related->add_part($_) foreach @related_parts;
2293
2294   }
2295
2296   my @otherparts = ();
2297   if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
2298
2299     if ( $conf->config('voip-cdr_email_attach') eq 'zip' ) {
2300
2301       my $data = join('', map "$_\n",
2302                    $self->call_details(prepend_billed_number=>1)
2303                  );
2304
2305       my $zip = new Archive::Zip;
2306       my $file = $zip->addString( $data, 'usage-'.$self->invnum.'.csv' );
2307       $file->desiredCompressionMethod( COMPRESSION_DEFLATED );
2308
2309       my $zipdata = '';
2310       my $SH = IO::Scalar->new(\$zipdata);
2311       my $status = $zip->writeToFileHandle($SH);
2312       die "Error zipping CDR attachment: $!" unless $status == AZ_OK;
2313
2314       push @otherparts, build MIME::Entity
2315         'Type'        => 'application/zip',
2316         'Encoding'    => 'base64',
2317         'Data'        => $zipdata,
2318         'Disposition' => 'attachment',
2319         'Filename'    => 'usage-'. $self->invnum. '.zip',
2320       ;
2321
2322     } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
2323  
2324       push @otherparts, build MIME::Entity
2325         'Type'        => 'text/csv',
2326         'Encoding'    => '7bit',
2327         'Data'        => [ map { "$_\n" }
2328                              $self->call_details('prepend_billed_number' => 1)
2329                          ],
2330         'Disposition' => 'attachment',
2331         'Filename'    => 'usage-'. $self->invnum. '.csv',
2332       ;
2333
2334     }
2335
2336   }
2337
2338   if ( $conf->exists($tc.'email_pdf') ) {
2339
2340     #attaching pdf too:
2341     # multipart/mixed
2342     #   multipart/related
2343     #     multipart/alternative
2344     #       text/plain
2345     #       text/html
2346     #     image/png
2347     #   application/pdf
2348
2349     my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
2350     push @otherparts, $pdf;
2351   }
2352
2353   if (@otherparts) {
2354     $return{'content-type'} = 'multipart/mixed'; # of the outer container
2355     if ( $html ) {
2356       $return{'mimeparts'} = [ $related, @otherparts ];
2357       $return{'type'} = 'multipart/related'; # of the first part
2358     } else {
2359       $return{'mimeparts'} = [ $text_part, @otherparts ];
2360       $return{'type'} = 'text/plain';
2361     }
2362   } elsif ( $html ) { # no PDF or CSV, strip the outer container
2363     $return{'mimeparts'} = \@related_parts;
2364     $return{'content-type'} = 'multipart/related';
2365     $return{'type'} = 'multipart/alternative';
2366   } else { # no HTML either
2367     $return{'body'} = \@text;
2368     $return{'content-type'} = 'text/plain';
2369   }
2370
2371   %return;
2372
2373 }
2374
2375 =item mimebuild_pdf
2376
2377 Returns a list suitable for passing to MIME::Entity->build(), representing
2378 this invoice as PDF attachment.
2379
2380 =cut
2381
2382 sub mimebuild_pdf {
2383   my $self = shift;
2384   (
2385     'Type'        => 'application/pdf',
2386     'Encoding'    => 'base64',
2387     'Data'        => [ $self->print_pdf(@_) ],
2388     'Disposition' => 'attachment',
2389     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
2390   );
2391 }
2392
2393 =item postal_mail_fsinc
2394
2395 Sends this invoice to the Freeside Internet Services, Inc. print and mail
2396 service.
2397
2398 =cut
2399
2400 use CAM::PDF;
2401 use IO::Socket::SSL;
2402 use LWP::UserAgent;
2403 use HTTP::Request::Common qw( POST );
2404 use JSON::XS;
2405 use MIME::Base64;
2406 sub postal_mail_fsinc {
2407   my ( $self, %opt ) = @_;
2408
2409   my $url = 'https://ws.freeside.biz/print';
2410
2411   my $cust_main = $self->cust_main;
2412   my $agentnum = $cust_main->agentnum;
2413   my $bill_location = $cust_main->bill_location;
2414
2415   die "Extra charges for international mailing; contact support\@freeside.biz to enable\n"
2416     if $bill_location->country ne 'US';
2417
2418   my $conf = new FS::Conf;
2419
2420   my @company_address = $conf->config('company_address', $agentnum);
2421   my ( $company_address1, $company_address2, $company_city, $company_state, $company_zip );
2422   if ( $company_address[2] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2423     $company_address1 = $company_address[0];
2424     $company_address2 = $company_address[1];
2425     $company_city  = $1;
2426     $company_state = $2;
2427     $company_zip   = $3;
2428   } elsif ( $company_address[1] =~ /^\s*(\S.*\S)\s*[\s,](\w\w),?\s*(\d{5}(-\d{4})?)\s*$/ ) {
2429     $company_address1 = $company_address[0];
2430     $company_address2 = '';
2431     $company_city  = $1;
2432     $company_state = $2;
2433     $company_zip   = $3;
2434   } else {
2435     die "Unparsable company_address; contact support\@freeside.biz\n";
2436   }
2437   $company_city =~ s/,$//;
2438
2439   my $file = $self->print_pdf(%opt, 'no_addresses' => 1);
2440   my $pages = CAM::PDF->new($file)->numPages;
2441
2442   my $ua = LWP::UserAgent->new(
2443     'ssl_opts' => { 
2444       verify_hostname => 0,
2445       SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
2446       SSL_version     => 'SSLv3',
2447     }
2448   );
2449   my $response = $ua->request( POST $url, [
2450     'support-key'      => scalar($conf->config('support-key')),
2451     'file'             => encode_base64($file),
2452     'pages'            => $pages,
2453
2454     #from:
2455     'company_name'     => scalar( $conf->config('company_name', $agentnum) ),
2456     'company_address1' => $company_address1,
2457     'company_address2' => $company_address2,
2458     'company_city'     => $company_city,
2459     'company_state'    => $company_state,
2460     'company_zip'      => $company_zip,
2461     'company_country'  => 'US',
2462     'company_phonenum' => scalar($conf->config('company_phonenum', $agentnum)),
2463     'company_email'    => scalar($conf->config('invoice_from', $agentnum)),
2464
2465     #to:
2466     'name'             => ( $cust_main->payname
2467                               && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/
2468                                 ? $cust_main->payname
2469                                 : $cust_main->contact_firstlast
2470                           ),
2471     'company'          => $cust_main->company,
2472     'address1'         => $bill_location->address1,
2473     'address2'         => $bill_location->address2,
2474     'city'             => $bill_location->city,
2475     'state'            => $bill_location->state,
2476     'zip'              => $bill_location->zip,
2477     'country'          => $bill_location->country,
2478   ]);
2479
2480   die "Print connection error: ". $response->message.
2481       ' ('. $response->as_string. ")\n"
2482     unless $response->is_success;
2483
2484   local $@;
2485   my $content = eval { decode_json($response->content) };
2486   die "Print JSON error : $@\n" if $@;
2487
2488   die $content->{error}."\n"
2489     if $content->{error};
2490
2491   #TODO: store this so we can query for a status later
2492   warn "Invoice printed, ID ". $content->{id}. "\n";
2493
2494   $content->{id};
2495 }
2496
2497 =item _items_sections OPTIONS
2498
2499 Generate section information for all items appearing on this invoice.
2500 This will only be called for multi-section invoices.
2501
2502 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
2503 related display records (L<FS::cust_bill_pkg_display>) and organize 
2504 them into two groups ("early" and "late" according to whether they come 
2505 before or after the total), then into sections.  A subtotal is calculated 
2506 for each section.
2507
2508 Section descriptions are returned in sort weight order.  Each consists 
2509 of a hash containing:
2510
2511 description: the package category name, escaped
2512 subtotal: the total charges in that section
2513 tax_section: a flag indicating that the section contains only tax charges
2514 summarized: same as tax_section, for some reason
2515 sort_weight: the package category's sort weight
2516
2517 If 'condense' is set on the display record, it also contains everything 
2518 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
2519 coderefs to generate parts of the invoice.  This is not advised.
2520
2521 The method returns two arrayrefs, one of "early" sections and one of "late"
2522 sections.
2523
2524 OPTIONS may include:
2525
2526 by_location: a flag to divide the invoice into sections by location.  
2527 Each section hash will have a 'location' element containing a hashref of 
2528 the location fields (see L<FS::cust_location>).  The section description
2529 will be the location label, but the template can use any of the location 
2530 fields to create a suitable label.
2531
2532 by_category: a flag to divide the invoice into sections using display 
2533 records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
2534 behavior.  Each section hash will have a 'category' element containing
2535 the section name from the display record (which probably equals the 
2536 category name of the package, but may not in some cases).
2537
2538 summary: a flag indicating that this is a summary-format invoice.
2539 Turning this on has the following effects:
2540 - Ignores display items with the 'summary' flag.
2541 - Places all sections in the "early" group even if they have post_total.
2542 - Creates sections for all non-disabled package categories, even if they 
2543 have no charges on this invoice, as well as a section with no name.
2544
2545 escape: an escape function to use for section titles.
2546
2547 extra_sections: an arrayref of additional sections to return after the 
2548 sorted list.  If there are any of these, section subtotals exclude 
2549 usage charges.
2550
2551 format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
2552 passed through to C<_condense_section()>.
2553
2554 =cut
2555
2556 use vars qw(%pkg_category_cache);
2557 sub _items_sections {
2558   my $self = shift;
2559   my %opt = @_;
2560   
2561   my $escape = $opt{escape};
2562   my @extra_sections = @{ $opt{extra_sections} || [] };
2563
2564   # $subtotal{$locationnum}{$categoryname} = amount.
2565   # if we're not using by_location, $locationnum is undef.
2566   # if we're not using by_category, you guessed it, $categoryname is undef.
2567   # if we're not using either one, we shouldn't be here in the first place...
2568   my %subtotal = ();
2569   my %late_subtotal = ();
2570   my %not_tax = ();
2571
2572   # About tax items + multisection invoices:
2573   # If either invoice_*summary option is enabled, AND there is a 
2574   # package category with the name of the tax, then there will be 
2575   # a display record assigning the tax item to that category.
2576   #
2577   # However, the taxes are always placed in the "Taxes, Surcharges,
2578   # and Fees" section regardless of that.  The only effect of the 
2579   # display record is to create a subtotal for the summary page.
2580
2581   # cache these
2582   my $pkg_hash = $self->cust_pkg_hash;
2583
2584   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2585   {
2586
2587       my $usage = $cust_bill_pkg->usage;
2588
2589       my $locationnum;
2590       if ( $opt{by_location} ) {
2591         if ( $cust_bill_pkg->pkgnum ) {
2592           $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2593         } else {
2594           $locationnum = '';
2595         }
2596       } else {
2597         $locationnum = undef;
2598       }
2599
2600       # as in _items_cust_pkg, if a line item has no display records,
2601       # cust_bill_pkg_display() returns a default record for it
2602
2603       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2604         next if ( $display->summary && $opt{summary} );
2605
2606         my $section = $display->section;
2607         my $type    = $display->type;
2608         # Set $section = undef if we're sectioning by location and this
2609         # line item _has_ a location (i.e. isn't a fee).
2610         $section = undef if $locationnum;
2611
2612         # set this flag if the section is not tax-only
2613         $not_tax{$locationnum}{$section} = 1
2614           if $cust_bill_pkg->pkgnum  or $cust_bill_pkg->feepart;
2615
2616         # there's actually a very important piece of logic buried in here:
2617         # incrementing $late_subtotal{$section} CREATES 
2618         # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
2619         # to define the list of late sections, and likewise keys(%subtotal).
2620         # When _items_cust_bill_pkg is called to generate line items for 
2621         # real, it will be called with 'section' => $section for each 
2622         # of these.
2623         if ( $display->post_total && !$opt{summary} ) {
2624           if (! $type || $type eq 'S') {
2625             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2626               if $cust_bill_pkg->setup != 0
2627               || $cust_bill_pkg->setup_show_zero;
2628           }
2629
2630           if (! $type) {
2631             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2632               if $cust_bill_pkg->recur != 0
2633               || $cust_bill_pkg->recur_show_zero;
2634           }
2635
2636           if ($type && $type eq 'R') {
2637             $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2638               if $cust_bill_pkg->recur != 0
2639               || $cust_bill_pkg->recur_show_zero;
2640           }
2641           
2642           if ($type && $type eq 'U') {
2643             $late_subtotal{$locationnum}{$section} += $usage
2644               unless scalar(@extra_sections);
2645           }
2646
2647         } else { # it's a pre-total (normal) section
2648
2649           # skip tax items unless they're explicitly included in a section
2650           next if $cust_bill_pkg->pkgnum == 0 and
2651                   ! $cust_bill_pkg->feepart   and
2652                   ! $section;
2653
2654           if ( $type eq 'S' ) {
2655             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2656               if $cust_bill_pkg->setup != 0
2657               || $cust_bill_pkg->setup_show_zero;
2658           } elsif ( $type eq 'R' ) {
2659             $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2660               if $cust_bill_pkg->recur != 0
2661               || $cust_bill_pkg->recur_show_zero;
2662           } elsif ( $type eq 'U' ) {
2663             $subtotal{$locationnum}{$section} += $usage
2664               unless scalar(@extra_sections);
2665           } elsif ( !$type ) {
2666             $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2667                                                + $cust_bill_pkg->recur;
2668           }
2669
2670         }
2671
2672       }
2673
2674   }
2675
2676   %pkg_category_cache = ();
2677
2678   # summary invoices need subtotals for all non-disabled package categories,
2679   # even if they're zero
2680   # but currently assume that there are no location sections, or at least
2681   # that the summary page doesn't care about them
2682   if ( $opt{summary} ) {
2683     foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2684       $subtotal{''}{$category->categoryname} ||= 0;
2685     }
2686     $subtotal{''}{''} ||= 0;
2687   }
2688
2689   my @sections;
2690   foreach my $post_total (0,1) {
2691     my @these;
2692     my $s = $post_total ? \%late_subtotal : \%subtotal;
2693     foreach my $locationnum (keys %$s) {
2694       foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2695         my $section = {
2696                         'subtotal'    => $s->{$locationnum}{$sectionname},
2697                         'sort_weight' => 0,
2698                       };
2699         if ( $locationnum ) {
2700           $section->{'locationnum'} = $locationnum;
2701           my $location = FS::cust_location->by_key($locationnum);
2702           $section->{'description'} = &{ $escape }($location->location_label);
2703           # Better ideas? This will roughly group them by proximity, 
2704           # which alpha sorting on any of the address fields won't.
2705           # Sorting by locationnum is meaningless.
2706           # We have to sort on _something_ or the order may change 
2707           # randomly from one invoice to the next, which will confuse
2708           # people.
2709           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2710                                       $locationnum;
2711           $section->{'location'} = {
2712             label_prefix => &{ $escape }($location->label_prefix),
2713             map { $_ => &{ $escape }($location->get($_)) }
2714               $location->fields
2715           };
2716         } else {
2717           $section->{'category'} = $sectionname;
2718           $section->{'description'} = &{ $escape }($sectionname);
2719           if ( _pkg_category($sectionname) ) {
2720             $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2721             if ( _pkg_category($sectionname)->condense ) {
2722               $section = { %$section, $self->_condense_section($opt{format}) };
2723             }
2724           }
2725         }
2726         if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2727           # then it's a tax-only section
2728           $section->{'summarized'} = 'Y';
2729           $section->{'tax_section'} = 'Y';
2730         }
2731         push @these, $section;
2732       } # foreach $sectionname
2733     } #foreach $locationnum
2734     push @these, @extra_sections if $post_total == 0;
2735     # need an alpha sort for location sections, because postal codes can 
2736     # be non-numeric
2737     $sections[ $post_total ] = [ sort {
2738       $opt{'by_location'} ? 
2739         ($a->{sort_weight} cmp $b->{sort_weight}) :
2740         ($a->{sort_weight} <=> $b->{sort_weight})
2741       } @these ];
2742   } #foreach $post_total
2743
2744   return @sections; # early, late
2745 }
2746
2747 #helper subs for above
2748
2749 sub cust_pkg_hash {
2750   my $self = shift;
2751   $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2752 }
2753
2754 sub _pkg_category {
2755   my $categoryname = shift;
2756   $pkg_category_cache{$categoryname} ||=
2757     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2758 }
2759
2760 my %condensed_format = (
2761   'label' => [ qw( Description Qty Amount ) ],
2762   'fields' => [
2763                 sub { shift->{description} },
2764                 sub { shift->{quantity} },
2765                 sub { my($href, %opt) = @_;
2766                       ($opt{dollar} || ''). $href->{amount};
2767                     },
2768               ],
2769   'align'  => [ qw( l r r ) ],
2770   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
2771   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
2772 );
2773
2774 sub _condense_section {
2775   my ( $self, $format ) = ( shift, shift );
2776   ( 'condensed' => 1,
2777     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2778       qw( description_generator
2779           header_generator
2780           total_generator
2781           total_line_generator
2782         )
2783   );
2784 }
2785
2786 sub _condensed_generator_defaults {
2787   my ( $self, $format ) = ( shift, shift );
2788   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2789 }
2790
2791 my %html_align = (
2792   'c' => 'center',
2793   'l' => 'left',
2794   'r' => 'right',
2795 );
2796
2797 sub _condensed_header_generator {
2798   my ( $self, $format ) = ( shift, shift );
2799
2800   my ( $f, $prefix, $suffix, $separator, $column ) =
2801     _condensed_generator_defaults($format);
2802
2803   if ($format eq 'latex') {
2804     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2805     $suffix = "\\\\\n\\hline";
2806     $separator = "&\n";
2807     $column =
2808       sub { my ($d,$a,$s,$w) = @_;
2809             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2810           };
2811   } elsif ( $format eq 'html' ) {
2812     $prefix = '<th></th>';
2813     $suffix = '';
2814     $separator = '';
2815     $column =
2816       sub { my ($d,$a,$s,$w) = @_;
2817             return qq!<th align="$html_align{$a}">$d</th>!;
2818       };
2819   }
2820
2821   sub {
2822     my @args = @_;
2823     my @result = ();
2824
2825     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2826       push @result,
2827         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2828     }
2829
2830     $prefix. join($separator, @result). $suffix;
2831   };
2832
2833 }
2834
2835 sub _condensed_description_generator {
2836   my ( $self, $format ) = ( shift, shift );
2837
2838   my ( $f, $prefix, $suffix, $separator, $column ) =
2839     _condensed_generator_defaults($format);
2840
2841   my $money_char = '$';
2842   if ($format eq 'latex') {
2843     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2844     $suffix = '\\\\';
2845     $separator = " & \n";
2846     $column =
2847       sub { my ($d,$a,$s,$w) = @_;
2848             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2849           };
2850     $money_char = '\\dollar';
2851   }elsif ( $format eq 'html' ) {
2852     $prefix = '"><td align="center"></td>';
2853     $suffix = '';
2854     $separator = '';
2855     $column =
2856       sub { my ($d,$a,$s,$w) = @_;
2857             return qq!<td align="$html_align{$a}">$d</td>!;
2858       };
2859     #$money_char = $conf->config('money_char') || '$';
2860     $money_char = '';  # this is madness
2861   }
2862
2863   sub {
2864     #my @args = @_;
2865     my $href = shift;
2866     my @result = ();
2867
2868     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2869       my $dollar = '';
2870       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2871       push @result,
2872         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2873                     map { $f->{$_}->[$i] } qw(align span width)
2874                   );
2875     }
2876
2877     $prefix. join( $separator, @result ). $suffix;
2878   };
2879
2880 }
2881
2882 sub _condensed_total_generator {
2883   my ( $self, $format ) = ( shift, shift );
2884
2885   my ( $f, $prefix, $suffix, $separator, $column ) =
2886     _condensed_generator_defaults($format);
2887   my $style = '';
2888
2889   if ($format eq 'latex') {
2890     $prefix = "& ";
2891     $suffix = "\\\\\n";
2892     $separator = " & \n";
2893     $column =
2894       sub { my ($d,$a,$s,$w) = @_;
2895             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2896           };
2897   }elsif ( $format eq 'html' ) {
2898     $prefix = '';
2899     $suffix = '';
2900     $separator = '';
2901     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2902     $column =
2903       sub { my ($d,$a,$s,$w) = @_;
2904             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2905       };
2906   }
2907
2908
2909   sub {
2910     my @args = @_;
2911     my @result = ();
2912
2913     #  my $r = &{$f->{fields}->[$i]}(@args);
2914     #  $r .= ' Total' unless $i;
2915
2916     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2917       push @result,
2918         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2919                     map { $f->{$_}->[$i] } qw(align span width)
2920                   );
2921     }
2922
2923     $prefix. join( $separator, @result ). $suffix;
2924   };
2925
2926 }
2927
2928 =item total_line_generator FORMAT
2929
2930 Returns a coderef used for generation of invoice total line items for this
2931 usage_class.  FORMAT is either html or latex
2932
2933 =cut
2934
2935 # should not be used: will have issues with hash element names (description vs
2936 # total_item and amount vs total_amount -- another array of functions?
2937
2938 sub _condensed_total_line_generator {
2939   my ( $self, $format ) = ( shift, shift );
2940
2941   my ( $f, $prefix, $suffix, $separator, $column ) =
2942     _condensed_generator_defaults($format);
2943   my $style = '';
2944
2945   if ($format eq 'latex') {
2946     $prefix = "& ";
2947     $suffix = "\\\\\n";
2948     $separator = " & \n";
2949     $column =
2950       sub { my ($d,$a,$s,$w) = @_;
2951             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2952           };
2953   }elsif ( $format eq 'html' ) {
2954     $prefix = '';
2955     $suffix = '';
2956     $separator = '';
2957     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2958     $column =
2959       sub { my ($d,$a,$s,$w) = @_;
2960             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2961       };
2962   }
2963
2964
2965   sub {
2966     my @args = @_;
2967     my @result = ();
2968
2969     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
2970       push @result,
2971         &{$column}( &{$f->{fields}->[$i]}(@args),
2972                     map { $f->{$_}->[$i] } qw(align span width)
2973                   );
2974     }
2975
2976     $prefix. join( $separator, @result ). $suffix;
2977   };
2978
2979 }
2980
2981 =item _items_pkg [ OPTIONS ]
2982
2983 Return line item hashes for each package item on this invoice. Nearly 
2984 equivalent to 
2985
2986 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2987
2988 OPTIONS are passed through to _items_cust_bill_pkg, and should include
2989 'format' and 'escape_function' at minimum.
2990
2991 To produce items for a specific invoice section, OPTIONS should include
2992 'section', a hashref containing 'category' and/or 'locationnum' keys.
2993
2994 'section' may also contain a key named 'condensed'. If this is present
2995 and has a true value, _items_pkg will try to merge identical items into items
2996 with 'quantity' equal to the number of items (not the sum of their separate
2997 quantities, for some reason).
2998
2999 =cut
3000
3001 sub _items_nontax {
3002   my $self = shift;
3003   # The order of these is important.  Bundled line items will be merged into
3004   # the most recent non-hidden item, so it needs to be the one with:
3005   # - the same pkgnum
3006   # - the same start date
3007   # - no pkgpart_override
3008   #
3009   # So: sort by pkgnum,
3010   # then by sdate
3011   # then sort the base line item before any overrides
3012   # then sort hidden before non-hidden add-ons
3013   # then sort by override pkgpart (for consistency)
3014   sort { $a->pkgnum <=> $b->pkgnum        or
3015          $a->sdate  <=> $b->sdate         or
3016          ($a->pkgpart_override ? 0 : -1)  or
3017          ($b->pkgpart_override ? 0 : 1)   or
3018          $b->hidden cmp $a->hidden        or
3019          $a->pkgpart_override <=> $b->pkgpart_override
3020        }
3021   # and of course exclude taxes and fees
3022   grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
3023 }
3024
3025 sub _items_fee {
3026   my $self = shift;
3027   my %options = @_;
3028   my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
3029   my $escape_function = $options{escape_function};
3030
3031   my @items;
3032   foreach my $cust_bill_pkg (@cust_bill_pkg) {
3033     # cache this, so we don't look it up again in every section
3034     my $part_fee = $cust_bill_pkg->get('part_fee')
3035        || $cust_bill_pkg->part_fee;
3036     $cust_bill_pkg->set('part_fee', $part_fee);
3037     if (!$part_fee) {
3038       #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
3039       warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
3040       next;
3041     }
3042     if ( exists($options{section}) and exists($options{section}{category}) )
3043     {
3044       my $categoryname = $options{section}{category};
3045       # then filter for items that have that section
3046       if ( $part_fee->categoryname ne $categoryname ) {
3047         warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
3048         next;
3049       }
3050     } # otherwise include them all in the main section
3051     # XXX what to do when sectioning by location?
3052     
3053     my @ext_desc;
3054     my %base_invnums; # invnum => invoice date
3055     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
3056       if ($_->base_invnum) {
3057         # XXX what if base_bill has been voided?
3058         my $base_bill = FS::cust_bill->by_key($_->base_invnum);
3059         my $base_date = $self->time2str_local('short', $base_bill->_date)
3060           if $base_bill;
3061         $base_invnums{$_->base_invnum} = $base_date || '';
3062       }
3063     }
3064     foreach (sort keys(%base_invnums)) {
3065       next if $_ == $self->invnum;
3066       # per convention, we must escape ext_description lines
3067       push @ext_desc,
3068         &{$escape_function}(
3069           $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
3070         );
3071     }
3072     my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
3073     # but not escape the base description line
3074
3075     push @items,
3076       { feepart     => $cust_bill_pkg->feepart,
3077         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
3078         description => $desc,
3079         ext_description => \@ext_desc
3080         # sdate/edate?
3081       };
3082   }
3083   @items;
3084 }
3085
3086 sub _items_pkg {
3087   my $self = shift;
3088   my %options = @_;
3089
3090   warn "$me _items_pkg searching for all package line items\n"
3091     if $DEBUG > 1;
3092
3093   my @cust_bill_pkg = $self->_items_nontax;
3094
3095   warn "$me _items_pkg filtering line items\n"
3096     if $DEBUG > 1;
3097   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3098
3099   if ($options{section} && $options{section}->{condensed}) {
3100
3101     warn "$me _items_pkg condensing section\n"
3102       if $DEBUG > 1;
3103
3104     my %itemshash = ();
3105     local $Storable::canonical = 1;
3106     foreach ( @items ) {
3107       my $item = { %$_ };
3108       delete $item->{ref};
3109       delete $item->{ext_description};
3110       my $key = freeze($item);
3111       $itemshash{$key} ||= 0;
3112       $itemshash{$key} ++; # += $item->{quantity};
3113     }
3114     @items = sort { $a->{description} cmp $b->{description} }
3115              map { my $i = thaw($_);
3116                    $i->{quantity} = $itemshash{$_};
3117                    $i->{amount} =
3118                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3119                    $i;
3120                  }
3121              keys %itemshash;
3122   }
3123
3124   warn "$me _items_pkg returning ". scalar(@items). " items\n"
3125     if $DEBUG > 1;
3126
3127   @items;
3128 }
3129
3130 sub _taxsort {
3131   return 0 unless $a->itemdesc cmp $b->itemdesc;
3132   return -1 if $b->itemdesc eq 'Tax';
3133   return 1 if $a->itemdesc eq 'Tax';
3134   return -1 if $b->itemdesc eq 'Other surcharges';
3135   return 1 if $a->itemdesc eq 'Other surcharges';
3136   $a->itemdesc cmp $b->itemdesc;
3137 }
3138
3139 sub _items_tax {
3140   my $self = shift;
3141   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
3142     $self->cust_bill_pkg;
3143   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3144
3145   if ( $self->conf->exists('always_show_tax') ) {
3146     my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
3147     if (0 == grep { $_->{description} eq $itemdesc } @items) {
3148       push @items,
3149         { 'description' => $itemdesc,
3150           'amount'      => 0.00 };
3151     }
3152   }
3153   @items;
3154 }
3155
3156 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
3157
3158 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
3159 list of hashrefs describing the line items they generate on the invoice.
3160
3161 OPTIONS may include:
3162
3163 format: the invoice format.
3164
3165 escape_function: the function used to escape strings.
3166
3167 DEPRECATED? (expensive, mostly unused?)
3168 format_function: the function used to format CDRs.
3169
3170 section: a hashref containing 'category' and/or 'locationnum'; if this 
3171 is present, only returns line items that belong to that category and/or
3172 location (whichever is defined).
3173
3174 multisection: a flag indicating that this is a multisection invoice,
3175 which does something complicated.
3176
3177 preref_callback: coderef run for each line item, code should return HTML to be
3178 displayed before that line item (quotations only)
3179
3180 Returns a list of hashrefs, each of which may contain:
3181
3182 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
3183 ext_description, which is an arrayref of detail lines to show below 
3184 the package line.
3185
3186 =cut
3187
3188 sub _items_cust_bill_pkg {
3189   my $self = shift;
3190   my $conf = $self->conf;
3191   my $cust_bill_pkgs = shift;
3192   my %opt = @_;
3193
3194   my $format = $opt{format} || '';
3195   my $escape_function = $opt{escape_function} || sub { shift };
3196   my $format_function = $opt{format_function} || '';
3197   my $no_usage = $opt{no_usage} || '';
3198   my $unsquelched = $opt{unsquelched} || ''; #unused
3199   my ($section, $locationnum, $category);
3200   if ( $opt{section} ) {
3201     $category = $opt{section}->{category};
3202     $locationnum = $opt{section}->{locationnum};
3203   }
3204   my $summary_page = $opt{summary_page} || ''; #unused
3205   my $multisection = defined($category) || defined($locationnum);
3206   # this variable is the value of the config setting, not whether it applies
3207   # to this particular line item.
3208   my $discount_show_always = $conf->exists('discount-show-always');
3209
3210   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
3211
3212   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
3213
3214   # for location labels: use default location on the invoice date
3215   my $default_locationnum;
3216   if ( $conf->exists('invoice-all_pkg_addresses') ) {
3217     $default_locationnum = 0; # treat them all as non-default
3218   } elsif ( $self->custnum ) {
3219     my $h_cust_main;
3220     my @h_search = FS::h_cust_main->sql_h_search($self->_date);
3221     $h_cust_main = qsearchs({
3222         'table'     => 'h_cust_main',
3223         'hashref'   => { custnum => $self->custnum },
3224         'extra_sql' => $h_search[1],
3225         'addl_from' => $h_search[3],
3226     }) || $cust_main;
3227     $default_locationnum = $h_cust_main->ship_locationnum;
3228   } elsif ( $self->prospectnum ) {
3229     my $cust_location = qsearchs('cust_location',
3230       { prospectnum => $self->prospectnum,
3231         disabled => '' });
3232     $default_locationnum = $cust_location->locationnum if $cust_location;
3233   }
3234
3235   my @b = (); # accumulator for the line item hashes that we'll return
3236   my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
3237             # the 'current' line item hashes for setup, recur, usage, discount
3238   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
3239   {
3240     # if the current line item is waiting to go out, and the one we're about
3241     # to start is not bundled, then push out the current one and start a new
3242     # one.
3243     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3244       if ( $_ && !$cust_bill_pkg->hidden ) {
3245         $_->{amount}      = sprintf( "%.2f", $_->{amount} );
3246         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3247         if (exists($_->{unit_amount})) {
3248           $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3249         }
3250         push @b, { %$_ };
3251         # we already decided to create this display line; don't reconsider it
3252         # now.
3253         #  if $_->{amount} != 0
3254         #  || $discount_show_always
3255         #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3256         #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3257         ;
3258         $_ = undef;
3259       }
3260     }
3261
3262     if ( $locationnum ) {
3263       # this is a location section; skip packages that aren't at this
3264       # service location.
3265       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
3266       next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
3267               != $locationnum;
3268     }
3269
3270     # Consider display records for this item to determine if it belongs
3271     # in this section.  Note that if there are no display records, there
3272     # will be a default pseudo-record that includes all charge types 
3273     # and has no section name.
3274     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
3275                                   ? $cust_bill_pkg->cust_bill_pkg_display
3276                                   : ( $cust_bill_pkg );
3277
3278     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
3279          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
3280       if $DEBUG > 1;
3281
3282     if ( defined($category) ) {
3283       # then this is a package category section; process all display records
3284       # that belong to this section.
3285       @cust_bill_pkg_display = grep { $_->section eq $category }
3286                                 @cust_bill_pkg_display;
3287     } else {
3288       # otherwise, process all display records that aren't usage summaries
3289       # (I don't think there should be usage summaries if you aren't using 
3290       # category sections, but this is the historical behavior)
3291       @cust_bill_pkg_display = grep { !$_->summary }
3292                                 @cust_bill_pkg_display;
3293     }
3294
3295     my $classname = ''; # package class name, will fill in later
3296
3297     foreach my $display (@cust_bill_pkg_display) {
3298
3299       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
3300            $display->billpkgdisplaynum. "\n"
3301         if $DEBUG > 1;
3302
3303       my $type = $display->type;
3304
3305       my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
3306       $desc = substr($desc, 0, $maxlength). '...'
3307         if $format eq 'latex' && length($desc) > $maxlength;
3308
3309       my %details_opt = ( 'format'          => $format,
3310                           'escape_function' => $escape_function,
3311                           'format_function' => $format_function,
3312                           'no_usage'        => $opt{'no_usage'},
3313                         );
3314
3315       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
3316         # XXX this should be pulled out into quotation_pkg
3317
3318         warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
3319           if $DEBUG > 1;
3320         # quotation_pkgs are never fees, so don't worry about the case where
3321         # part_pkg is undefined
3322
3323         my @details = $cust_bill_pkg->details;
3324
3325         # and I guess they're never bundled either?
3326         if (( $cust_bill_pkg->setup != 0 ) || ( $cust_bill_pkg->setup_show_zero )) {
3327           my $description = $desc;
3328           $description .= ' Setup'
3329             if $cust_bill_pkg->recur != 0
3330             || $discount_show_always
3331             || $cust_bill_pkg->recur_show_zero;
3332           #push @b, {
3333           # keep it consistent, please
3334           $s = {
3335             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3336             'description' => $description,
3337             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3338             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
3339             'quantity'    => $cust_bill_pkg->quantity,
3340             'ext_description' => \@details,
3341             'preref_html' => ( $opt{preref_callback}
3342                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3343                                  : ''
3344                              ),
3345           };
3346         }
3347         if (( $cust_bill_pkg->recur != 0 ) || ( $cust_bill_pkg->recur_show_zero )) {
3348           #push @b, {
3349           $r = {
3350             'pkgnum'      => $cust_bill_pkg->pkgpart, #so it displays in Ref
3351             'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
3352             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3353             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
3354             'quantity'    => $cust_bill_pkg->quantity,
3355             'ext_description' => \@details,
3356            'preref_html'  => ( $opt{preref_callback}
3357                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
3358                                  : ''
3359                              ),
3360           };
3361         }
3362
3363       } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
3364         # a "normal" package line item (not a quotation, not a fee, not a tax)
3365
3366         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
3367           if $DEBUG > 1;
3368  
3369         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3370         my $part_pkg = $cust_pkg->part_pkg;
3371
3372         # which pkgpart to show for display purposes?
3373         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
3374
3375         # start/end dates for invoice formats that do nonstandard 
3376         # things with them
3377         my %item_dates = ();
3378         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
3379           unless $part_pkg->option('disable_line_item_date_ranges',1);
3380
3381         # not normally used, but pass this to the template anyway
3382         $classname = $part_pkg->classname;
3383
3384         if (    (!$type || $type eq 'S')
3385              && (    $cust_bill_pkg->setup != 0
3386                   || $cust_bill_pkg->setup_show_zero
3387                   || ($discount_show_always and $cust_bill_pkg->unitsetup > 0)
3388                 )
3389            )
3390          {
3391
3392           warn "$me _items_cust_bill_pkg adding setup\n"
3393             if $DEBUG > 1;
3394
3395           # append the word 'Setup' to the setup line if there's going to be
3396           # a recur line for the same package (i.e. not a one-time charge) 
3397           # XXX localization
3398           my $description = $desc;
3399           $description .= ' Setup'
3400             if $cust_bill_pkg->recur != 0
3401             || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3402             || $cust_bill_pkg->recur_show_zero;
3403
3404           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3405                                                               $self->agentnum )
3406             if $part_pkg->is_prepaid #for prepaid, "display the validity period
3407                                      # triggered by the recurring charge freq
3408                                      # (RT#26274)
3409             && $cust_bill_pkg->recur == 0
3410             && ! $cust_bill_pkg->recur_show_zero;
3411
3412           my @d = ();
3413           my $svc_label;
3414
3415           # always pass the svc_label through to the template, even if 
3416           # not displaying it as an ext_description
3417           my @svc_labels = map &{$escape_function}($_),
3418             $cust_pkg->h_labels_short($self->_date,
3419                                       undef,
3420                                       'I',
3421                                       $self->conf->{locale},
3422                                      );
3423           $svc_label = $svc_labels[0];
3424
3425           unless ( $cust_pkg->part_pkg->hide_svc_detail
3426                 || $cust_bill_pkg->hidden )
3427           {
3428
3429             push @d, @svc_labels
3430               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3431             # show the location label if it's not the customer's default
3432             # location, and we're not grouping items by location already
3433             if ( $cust_pkg->locationnum != $default_locationnum
3434                   and !defined($locationnum) ) {
3435               my $loc = $cust_pkg->location_label;
3436               $loc = substr($loc, 0, $maxlength). '...'
3437                 if $format eq 'latex' && length($loc) > $maxlength;
3438               push @d, &{$escape_function}($loc);
3439             }
3440
3441           } #unless hiding service details
3442
3443           push @d, $cust_bill_pkg->details(%details_opt)
3444             if $cust_bill_pkg->recur == 0;
3445
3446           if ( $cust_bill_pkg->hidden ) {
3447             $s->{amount}      += $cust_bill_pkg->setup;
3448             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3449             push @{ $s->{ext_description} }, @d;
3450           } else {
3451             $s = {
3452               _is_setup       => 1,
3453               description     => $description,
3454               pkgpart         => $pkgpart,
3455               pkgnum          => $cust_bill_pkg->pkgnum,
3456               amount          => $cust_bill_pkg->setup,
3457               setup_show_zero => $cust_bill_pkg->setup_show_zero,
3458               unit_amount     => $cust_bill_pkg->unitsetup,
3459               quantity        => $cust_bill_pkg->quantity,
3460               ext_description => \@d,
3461               svc_label       => ($svc_label || ''),
3462               locationnum     => $cust_pkg->locationnum, # sure, why not?
3463             };
3464           };
3465
3466         }
3467
3468         # should we show a recur line?
3469         # if type eq 'S', then NO, because we've been told not to.
3470         # otherwise, show the recur line if:
3471         # - there's a recurring charge
3472         # - or recur_show_zero is on
3473         # - or there's a positive unitrecur (so it's been discounted to zero)
3474         #   and discount-show-always is on
3475         if (    ( !$type || $type eq 'R' || $type eq 'U' )
3476              && (
3477                      $cust_bill_pkg->recur != 0
3478                   || !defined($s)
3479                   || ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
3480                   || $cust_bill_pkg->recur_show_zero
3481                 )
3482            )
3483         {
3484
3485           warn "$me _items_cust_bill_pkg adding recur/usage\n"
3486             if $DEBUG > 1;
3487
3488           my $is_summary = $display->summary;
3489           my $description = $desc;
3490           if ( $type eq 'U' and defined($r) ) {
3491             # don't just show the same description as the recur line
3492             $description = $self->mt('Usage charges');
3493           }
3494
3495           my $part_pkg = $cust_pkg->part_pkg;
3496
3497           $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
3498                                                               $self->agentnum );
3499
3500           my @d = ();
3501           my @seconds = (); # for display of usage info
3502           my $svc_label = '';
3503
3504           #at least until cust_bill_pkg has "past" ranges in addition to
3505           #the "future" sdate/edate ones... see #3032
3506           my @dates = ( $self->_date );
3507           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3508           push @dates, $prev->sdate if $prev;
3509           push @dates, undef if !$prev;
3510
3511           my @svc_labels = map &{$escape_function}($_),
3512             $cust_pkg->h_labels_short(@dates,
3513                                       'I',
3514                                       $self->conf->{locale});
3515           $svc_label = $svc_labels[0];
3516
3517           # show service labels, unless...
3518                     # the package is set not to display them
3519           unless ( $part_pkg->hide_svc_detail
3520                     # or this is a tax-like line item
3521                 || $cust_bill_pkg->itemdesc
3522                     # or this is a hidden (bundled) line item
3523                 || $cust_bill_pkg->hidden
3524                     # or this is a usage summary line
3525                 || $is_summary && $type && $type eq 'U'
3526                     # or this is a usage line and there's a recurring line
3527                     # for the package in the same section (which will 
3528                     # have service labels already)
3529                 || ($type eq 'U' and defined($r))
3530               )
3531           {
3532
3533             warn "$me _items_cust_bill_pkg adding service details\n"
3534               if $DEBUG > 1;
3535
3536             push @d, @svc_labels
3537               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
3538             warn "$me _items_cust_bill_pkg done adding service details\n"
3539               if $DEBUG > 1;
3540
3541             # show the location label if it's not the customer's default
3542             # location, and we're not grouping items by location already
3543             if ( $cust_pkg->locationnum != $default_locationnum
3544                   and !defined($locationnum) ) {
3545               my $loc = $cust_pkg->location_label;
3546               $loc = substr($loc, 0, $maxlength). '...'
3547                 if $format eq 'latex' && length($loc) > $maxlength;
3548               push @d, &{$escape_function}($loc);
3549             }
3550
3551             # Display of seconds_since_sqlradacct:
3552             # On the invoice, when processing @detail_items, look for a field
3553             # named 'seconds'.  This will contain total seconds for each 
3554             # service, in the same order as @ext_description.  For services 
3555             # that don't support this it will show undef.
3556             if ( $conf->exists('svc_acct-usage_seconds') 
3557                  and ! $cust_bill_pkg->pkgpart_override ) {
3558               foreach my $cust_svc ( 
3559                   $cust_pkg->h_cust_svc(@dates, 'I') 
3560                 ) {
3561
3562                 # eval because not having any part_export_usage exports 
3563                 # is a fatal error, last_bill/_date because that's how 
3564                 # sqlradius_hour billing does it
3565                 my $sec = eval {
3566                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
3567                 };
3568                 push @seconds, $sec;
3569               }
3570             } #if svc_acct-usage_seconds
3571
3572           } # if we are showing service labels
3573
3574           unless ( $is_summary ) {
3575             warn "$me _items_cust_bill_pkg adding details\n"
3576               if $DEBUG > 1;
3577
3578             #instead of omitting details entirely in this case (unwanted side
3579             # effects), just omit CDRs
3580             $details_opt{'no_usage'} = 1
3581               if $type && $type eq 'R';
3582
3583             push @d, $cust_bill_pkg->details(%details_opt);
3584           }
3585
3586           warn "$me _items_cust_bill_pkg calculating amount\n"
3587             if $DEBUG > 1;
3588   
3589           my $amount = 0;
3590           if (!$type) {
3591             $amount = $cust_bill_pkg->recur;
3592           } elsif ($type eq 'R') {
3593             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3594           } elsif ($type eq 'U') {
3595             $amount = $cust_bill_pkg->usage;
3596           }
3597   
3598           if ( !$type || $type eq 'R' ) {
3599
3600             warn "$me _items_cust_bill_pkg adding recur\n"
3601               if $DEBUG > 1;
3602
3603             my $unit_amount =
3604               ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
3605                                                 : $amount;
3606
3607             if ( $cust_bill_pkg->hidden ) {
3608               $r->{amount}      += $amount;
3609               $r->{unit_amount} += $unit_amount;
3610               push @{ $r->{ext_description} }, @d;
3611             } else {
3612               $r = {
3613                 description     => $description,
3614                 pkgpart         => $pkgpart,
3615                 pkgnum          => $cust_bill_pkg->pkgnum,
3616                 amount          => $amount,
3617                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3618                 unit_amount     => $unit_amount,
3619                 quantity        => $cust_bill_pkg->quantity,
3620                 %item_dates,
3621                 ext_description => \@d,
3622                 svc_label       => ($svc_label || ''),
3623                 locationnum     => $cust_pkg->locationnum,
3624               };
3625               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3626             }
3627
3628           } else {  # $type eq 'U'
3629
3630             warn "$me _items_cust_bill_pkg adding usage\n"
3631               if $DEBUG > 1;
3632
3633             if ( $cust_bill_pkg->hidden and defined($u) ) {
3634               # if this is a hidden package and there's already a usage
3635               # line for the bundle, add this package's total amount and
3636               # usage details to it
3637               $u->{amount}      += $amount;
3638               push @{ $u->{ext_description} }, @d;
3639             } elsif ( $amount ) {
3640               # create a new usage line
3641               $u = {
3642                 description     => $description,
3643                 pkgpart         => $pkgpart,
3644                 pkgnum          => $cust_bill_pkg->pkgnum,
3645                 amount          => $amount,
3646                 usage_item      => 1,
3647                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3648                 %item_dates,
3649                 ext_description => \@d,
3650                 locationnum     => $cust_pkg->locationnum,
3651               };
3652             } # else this has no usage, so don't create a usage section
3653           }
3654
3655         } # recurring or usage with recurring charge
3656
3657       } else { # taxes and fees
3658
3659         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3660           if $DEBUG > 1;
3661
3662         # items of this kind should normally not have sdate/edate.
3663         push @b, {
3664           'description' => $desc,
3665           'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
3666                                            + $cust_bill_pkg->recur)
3667         };
3668
3669       } # if quotation / package line item / other line item
3670
3671       # decide whether to show active discounts here
3672       if (
3673           # case 1: we are showing a single line for the package
3674           ( !$type )
3675           # case 2: we are showing a setup line for a package that has
3676           # no base recurring fee
3677           or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3678           # case 3: we are showing a recur line for a package that has 
3679           # a base recurring fee
3680           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3681       ) {
3682
3683         my $item_discount = $cust_bill_pkg->_item_discount;
3684         if ( $item_discount ) {
3685           # $item_discount->{amount} is negative
3686
3687           if ( $d and $cust_bill_pkg->hidden ) {
3688             $d->{amount}      += $item_discount->{amount};
3689           } else {
3690             $d = $item_discount;
3691             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
3692           }
3693
3694           # update the active line (before the discount) to show the 
3695           # original price (whether this is a hidden line or not)
3696           #
3697           # quotation discounts keep track of setup and recur; invoice 
3698           # discounts currently don't
3699           if ( exists $item_discount->{setup_amount} ) {
3700
3701             $s->{amount} -= $item_discount->{setup_amount} if $s;
3702             $r->{amount} -= $item_discount->{recur_amount} if $r;
3703
3704           } else {
3705
3706             # $active_line is the line item hashref for the line that will
3707             # show the original price
3708             # (use the recur or single line for the package, unless we're 
3709             # showing a setup line for a package with no recurring fee)
3710             my $active_line = $r;
3711             if ( $type eq 'S' ) {
3712               $active_line = $s;
3713             }
3714             $active_line->{amount} -= $item_discount->{amount};
3715
3716           }
3717
3718         } # if there are any discounts
3719       } # if this is an appropriate place to show discounts
3720
3721     } # foreach $display
3722
3723   }
3724
3725   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3726     if ( $_  ) {
3727       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3728         if exists($_->{amount});
3729       $_->{amount}      =~ s/^\-0\.00$/0.00/;
3730       if (exists($_->{unit_amount})) {
3731         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3732       }
3733
3734       push @b, { %$_ };
3735       #if $_->{amount} != 0
3736       #  || $discount_show_always
3737       #  || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3738       #  || (   $_->{_is_setup} && $_->{setup_show_zero} )
3739     }
3740   }
3741
3742   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3743     if $DEBUG > 1;
3744
3745   @b;
3746
3747 }
3748
3749 =item _items_discounts_avail
3750
3751 Returns an array of line item hashrefs representing available term discounts
3752 for this invoice.  This makes the same assumptions that apply to term 
3753 discounts in general: that the package is billed monthly, at a flat rate, 
3754 with no usage charges.  A prorated first month will be handled, as will 
3755 a setup fee if the discount is allowed to apply to setup fees.
3756
3757 =cut
3758
3759 sub _items_discounts_avail {
3760   my $self = shift;
3761
3762   #maybe move this method from cust_bill when quotations support discount_plans 
3763   return () unless $self->can('discount_plans');
3764   my %plans = $self->discount_plans;
3765
3766   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3767   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3768
3769   map {
3770     my $months = $_;
3771     my $plan = $plans{$months};
3772
3773     my $term_total = sprintf('%.2f', $plan->discounted_total);
3774     my $percent = sprintf('%.0f', 
3775                           100 * (1 - $term_total / $plan->base_total) );
3776     my $permonth = sprintf('%.2f', $term_total / $months);
3777     my $detail = $self->mt('discount on item'). ' '.
3778                  join(', ', map { "#$_" } $plan->pkgnums)
3779       if $list_pkgnums;
3780
3781     # discounts for non-integer months don't work anyway
3782     $months = sprintf("%d", $months);
3783
3784     +{
3785       description => $self->mt('Save [_1]% by paying for [_2] months',
3786                                 $percent, $months),
3787       amount      => $self->mt('[_1] ([_2] per month)', 
3788                                 $term_total, $money_char.$permonth),
3789       ext_description => ($detail || ''),
3790     }
3791   } #map
3792   sort { $b <=> $a } keys %plans;
3793
3794 }
3795
3796 1;