RT# 79636 Add conf flag invoice_sections_multilocation
[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 dbh );
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 = $self->has_sections;
940   $invoice_data{'multisection'} = $multisection;
941   my $late_sections;
942   my $extra_sections = [];
943   my $extra_lines = ();
944
945   # default section ('Charges')
946   my $default_section = { 'description' => '',
947                           'subtotal'    => '', 
948                           'no_subtotal' => 1,
949                         };
950
951   # Previous Charges section
952   # subtotal is the first return value from $self->previous
953   my $previous_section;
954   # if the invoice has major sections, or if we're summarizing previous 
955   # charges with a single line, or if we've been specifically told to put them
956   # in a section, create a section for previous charges:
957   if ( $multisection or
958        $conf->exists('previous_balance-summary_only') or
959        $conf->exists('previous_balance-section') ) {
960     
961     $previous_section =  { 'description' => $self->mt('Previous Charges'),
962                            'subtotal'    => $other_money_char.
963                                             sprintf('%.2f', $pr_total),
964                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
965                          };
966     $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
967       join(' / ', map { $cust_main->balance_date_range(@$_) }
968                   $self->_prior_month30s
969           )
970       if $conf->exists('invoice_include_aging');
971
972   } else {
973     # otherwise put them in the main section
974     $previous_section = $default_section;
975   }
976
977   my $adjust_section = {
978     'description'    => $self->mt('Credits, Payments, and Adjustments'),
979     'adjust_section' => 1,
980     'subtotal'       => 0,   # adjusted below
981   };
982   my $adjust_weight = _pkg_category($adjust_section->{description})
983                         ? _pkg_category($adjust_section->{description})->weight
984                         : 0;
985   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
986   # Note: 'sort_weight' here is actually a flag telling whether there is an
987   # explicit package category for the adjust section. If so, certain behavior
988   # happens.
989   $adjust_section->{'sort_weight'} = $adjust_weight;
990
991
992   if ( $multisection ) {
993     ($extra_sections, $extra_lines) =
994       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
995       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
996       && $self->can('_items_extra_usage_sections');
997
998     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
999
1000     push @detail_items, @$extra_lines if $extra_lines;
1001
1002     # the code is written so that both methods can be used together, but
1003     # we haven't yet changed the template to take advantage of that, so for 
1004     # now, treat them as mutually exclusive.
1005     my %section_method = ( by_category => 1 );
1006     if ( $conf->config($tc.'sections_method') eq 'location' ) {
1007       %section_method = ( by_location => 1 );
1008     }
1009     my ($early, $late) =
1010       $self->_items_sections( 'summary' => $summarypage,
1011                               'escape'  => $escape_function_nonbsp,
1012                               'extra_sections' => $extra_sections,
1013                               'format'  => $format,
1014                               %section_method
1015                             );
1016     push @sections, @$early;
1017     $late_sections = $late;
1018
1019     if (    $conf->exists('svc_phone_sections')
1020          && $self->can('_items_svc_phone_sections')
1021        )
1022     {
1023       my ($phone_sections, $phone_lines) =
1024         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
1025       push @{$late_sections}, @$phone_sections;
1026       push @detail_items, @$phone_lines;
1027     }
1028     if ( $conf->exists('voip-cust_accountcode_cdr')
1029          && $cust_main->accountcode_cdr
1030          && $self->can('_items_accountcode_cdr')
1031        )
1032     {
1033       my ($accountcode_section, $accountcode_lines) =
1034         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
1035       if ( scalar(@$accountcode_lines) ) {
1036           push @{$late_sections}, $accountcode_section;
1037           push @detail_items, @$accountcode_lines;
1038       }
1039     }
1040   } else {# not multisection
1041     # make a default section
1042     push @sections, $default_section;
1043     # and calculate the finance charge total, since it won't get done otherwise.
1044     # and the default section total
1045     # XXX possibly finance_pkgclass should not be used in this manner?
1046     my @finance_charges;
1047     my @charges;
1048     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1049       if ( $invoice_data{finance_section} and 
1050         grep { $_->section eq $invoice_data{finance_section} }
1051            $cust_bill_pkg->cust_bill_pkg_display ) {
1052         # I think these are always setup fees, but just to be sure...
1053         push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1054       } else {
1055         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1056       }
1057     }
1058     $invoice_data{finance_amount} = 
1059       sprintf('%.2f', sum( @finance_charges ) || 0);
1060     $default_section->{subtotal} = $other_money_char.
1061                                     sprintf('%.2f', sum( @charges ) || 0);
1062   }
1063
1064   # start setting up summary subtotals
1065   my @summary_subtotals;
1066   my $method = $conf->config('summary_subtotals_method');
1067   if ( ( ref($self) ne 'FS::quotation' ) and $method and $method ne $conf->config($tc.'sections_method') ) {
1068     # then re-section them by the correct method
1069     my %section_method = ( by_category => 1 );
1070     if ( $conf->config('summary_subtotals_method') eq 'location' ) {
1071       %section_method = ( by_location => 1 );
1072     }
1073     my ($early, $late) =
1074       $self->_items_sections( 'summary' => $summarypage,
1075                               'escape'  => $escape_function_nonbsp,
1076                               'extra_sections' => $extra_sections,
1077                               'format'  => $format,
1078                               %section_method
1079                             );
1080     foreach ( @$early ) {
1081       next if $_->{subtotal} == 0;
1082       $_->{subtotal} = $other_money_char.sprintf('%.2f', $_->{subtotal});
1083       push @summary_subtotals, $_;
1084     }
1085   } else {
1086     # subtotal sectioning is the same as for the actual invoice sections
1087     @summary_subtotals = @sections;
1088   }
1089
1090   # Hereafter, push sections to both @sections and @summary_subtotals
1091   # if they belong in both places (e.g. tax section).  Late sections are
1092   # never in @summary_subtotals.
1093
1094   # previous invoice balances in the Previous Charges section if there
1095   # is one, otherwise in the main detail section
1096   # (except if summary_only is enabled, don't show them at all)
1097   if ( $self->can('_items_previous') &&
1098        $self->enable_previous &&
1099        ! $conf->exists('previous_balance-summary_only') ) {
1100
1101     warn "$me adding previous balances\n"
1102       if $DEBUG > 1;
1103
1104     foreach my $line_item ( $self->_items_previous ) {
1105
1106       my $detail = {
1107         ref             => $line_item->{'pkgnum'},
1108         pkgpart         => $line_item->{'pkgpart'},
1109         #quantity        => 1, # not really correct
1110         section         => $previous_section, # which might be $default_section
1111         description     => &$escape_function($line_item->{'description'}),
1112         ext_description => [ map { &$escape_function($_) } 
1113                              @{ $line_item->{'ext_description'} || [] }
1114                            ],
1115         amount          => $money_char . $line_item->{'amount'},
1116         product_code    => $line_item->{'pkgpart'} || 'N/A',
1117       };
1118
1119       push @detail_items, $detail;
1120       push @buf, [ $detail->{'description'},
1121                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1122                  ];
1123     }
1124
1125   }
1126
1127   if ( @pr_cust_bill && $self->enable_previous ) {
1128     push @buf, ['','-----------'];
1129     push @buf, [ $self->mt('Total Previous Balance'),
1130                  $money_char. sprintf("%10.2f", $pr_total) ];
1131     push @buf, ['',''];
1132   }
1133  
1134   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
1135       warn "$me adding DID summary\n"
1136         if $DEBUG > 1;
1137
1138       my ($didsummary,$minutes) = $self->_did_summary;
1139       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
1140       push @detail_items, 
1141        { 'description' => $didsummary_desc,
1142            'ext_description' => [ $didsummary, $minutes ],
1143        };
1144   }
1145
1146   foreach my $section (@sections, @$late_sections) {
1147
1148     # begin some normalization
1149     $section->{'subtotal'} = $section->{'amount'}
1150       if $multisection
1151          && !exists($section->{subtotal})
1152          && exists($section->{amount});
1153
1154     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1155       if ( $invoice_data{finance_section} &&
1156            $section->{'description'} eq $invoice_data{finance_section} );
1157
1158     if ( $multisection ) {
1159
1160       if ( ref($section->{'subtotal'}) ) {
1161
1162         $section->{'subtotal'} =
1163           sprintf("$other_money_char%.2f to $other_money_char%.2f",
1164                     $section->{'subtotal'}[0],
1165                     $section->{'subtotal'}[1]
1166                  );
1167
1168       } else {
1169
1170         $section->{'subtotal'} = $other_money_char.
1171                                  sprintf('%.2f', $section->{'subtotal'})
1172
1173       }
1174
1175       # continue some normalization
1176       $section->{'amount'}   = $section->{'subtotal'}
1177
1178     }
1179
1180     if ( $section->{'description'} ) {
1181       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1182                    [ '', '' ],
1183                  );
1184     }
1185
1186     warn "$me   setting options\n"
1187       if $DEBUG > 1;
1188
1189     my %options = ();
1190     $options{'section'} = $section if $multisection;
1191     $options{'format'} = $format;
1192     $options{'escape_function'} = $escape_function;
1193     $options{'no_usage'} = 1 unless $unsquelched;
1194     $options{'unsquelched'} = $unsquelched;
1195     $options{'summary_page'} = $summarypage;
1196     $options{'skip_usage'} =
1197       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1198     $options{'preref_callback'} = $params{'preref_callback'};
1199
1200     warn "$me   searching for line items\n"
1201       if $DEBUG > 1;
1202
1203     foreach my $line_item ( $self->_items_pkg(%options),
1204                             $self->_items_fee(%options) ) {
1205
1206       warn "$me     adding line item ".
1207            join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
1208         if $DEBUG > 1;
1209
1210       push @buf, ( [ $line_item->{'description'},
1211                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1212                    ],
1213                    map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1214                  );
1215
1216       $line_item->{'ref'} = $line_item->{'pkgnum'};
1217       $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1218       $line_item->{'section'} = $section;
1219       $line_item->{'description'} = &$escape_function($line_item->{'description'});
1220       $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1221
1222       if ( length($line_item->{'unit_amount'}) ) {
1223         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1224       }
1225       $line_item->{'ext_description'} ||= [];
1226  
1227       push @detail_items, $line_item;
1228     }
1229
1230     if ( $section->{'description'} ) {
1231       push @buf, ( ['','-----------'],
1232                    [ $section->{'description'}. ' sub-total',
1233                       $section->{'subtotal'} # already formatted this 
1234                    ],
1235                    [ '', '' ],
1236                    [ '', '' ],
1237                  );
1238     }
1239   
1240   }
1241
1242   $invoice_data{current_less_finance} =
1243     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1244
1245   # if there's anything in the Previous Charges section, prepend it to the list
1246   if ( $pr_total and $previous_section ne $default_section ) {
1247     unshift @sections, $previous_section;
1248     # but not @summary_subtotals
1249   }
1250
1251   warn "$me adding taxes\n"
1252     if $DEBUG > 1;
1253
1254   # create a tax section if we don't yet have one
1255   my $tax_description = 'Taxes, Surcharges, and Fees';
1256   my $tax_section =
1257     List::Util::first { $_->{description} eq $tax_description } @sections;
1258   if (!$tax_section) {
1259     $tax_section = { 'description' => $tax_description };
1260     push @sections, $tax_section if $multisection;
1261   }
1262   $tax_section->{tax_section} = 1; # mark this section as containing taxes
1263   # if this is an existing tax section, we're merging the tax items into it.
1264   # grab the taxtotal that's already there, strip the money symbol if any
1265   my $taxtotal = $tax_section->{'subtotal'} || 0;
1266   $taxtotal =~ s/^\Q$other_money_char\E//;
1267
1268   # this does nothing
1269   #my $tax_weight = _pkg_category($tax_section->{description})
1270   #                      ? _pkg_category($tax_section->{description})->weight
1271   #                      : 0;
1272   #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
1273   #$tax_section->{'sort_weight'} = $tax_weight;
1274
1275   my @items_tax = $self->_items_tax;
1276   foreach my $tax ( @items_tax ) {
1277
1278     $taxtotal += $tax->{'amount'};
1279
1280     my $description = &$escape_function( $tax->{'description'} );
1281     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
1282
1283     if ( $multisection ) {
1284
1285       push @detail_items, {
1286         ext_description => [],
1287         ref          => '',
1288         quantity     => '',
1289         description  => $description,
1290         amount       => $money_char. $amount,
1291         product_code => '',
1292         section      => $tax_section,
1293       };
1294
1295     } else {
1296
1297       push @total_items, {
1298         'total_item'   => $description,
1299         'total_amount' => $other_money_char. $amount,
1300       };
1301
1302     }
1303
1304     push @buf,[ $description,
1305                 $money_char. $amount,
1306               ];
1307
1308   }
1309  
1310   if ( @items_tax ) {
1311     my $total = {};
1312     $total->{'total_item'} = $self->mt('Sub-total');
1313     $total->{'total_amount'} =
1314       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1315
1316     if ( $multisection ) {
1317       if ( $taxtotal > 0 ) {
1318         # there are taxes, so prepare the section to be displayed.
1319         # $taxtotal already includes any line items that were already in the
1320         # section (fees, taxes that are charged as packages for some reason).
1321         # also set 'summarized' to false so that this isn't a summary-only
1322         # section.
1323         $tax_section->{'subtotal'} = $other_money_char.
1324                                      sprintf('%.2f', $taxtotal);
1325         $tax_section->{'pretotal'} = 'New charges sub-total '.
1326                                      $total->{'total_amount'};
1327         $tax_section->{'description'} = $self->mt($tax_description);
1328         $tax_section->{'summarized'} = '';
1329
1330         # append it if it's not already there
1331         if ( !grep $tax_section, @sections ) {
1332           push @sections, $tax_section;
1333           push @summary_subtotals, $tax_section;
1334         }
1335       }
1336
1337     } else {
1338       unshift @total_items, $total;
1339     }
1340   }
1341   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1342
1343   ###
1344   # Totals
1345   ###
1346
1347   my %embolden_functions = (
1348     'latex'    => sub { return '\textbf{'. shift(). '}' },
1349     'html'     => sub { return '<b>'. shift(). '</b>' },
1350     'template' => sub { shift },
1351   );
1352   my $embolden_function = $embolden_functions{$format};
1353
1354   if ( $multisection ) {
1355
1356     if ( $adjust_section->{'sort_weight'} ) {
1357       $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1358         $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
1359     } else{
1360       $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1361         $other_money_char.  sprintf('%.2f', $self->charged );
1362     }
1363
1364   }
1365   
1366   if ( $self->can('_items_total') ) { # should always be true now
1367
1368     # even for multisection, need plain text version
1369
1370     my @new_total_items = $self->_items_total;
1371
1372     push @buf,['','-----------'];
1373
1374     foreach ( @new_total_items ) {
1375       my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'});
1376       $_->{'total_item'}   = &$embolden_function( $item );
1377
1378       if ( ref($amount) ) {
1379         $_->{'total_amount'} = &$embolden_function(
1380                                  $other_money_char.$amount->[0]. ' to '.
1381                                  $other_money_char.$amount->[1]
1382                                );
1383       } else {
1384       $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
1385       }
1386
1387       # but if it's multisection, don't append to @total_items. the adjust
1388       # section has all this stuff
1389       push @total_items, $_ if !$multisection;
1390       push @buf, [ $item, $money_char.sprintf('%10.2f',$amount) ];
1391     }
1392
1393     push @buf, [ '', '' ];
1394
1395     # if we're showing previous invoices, also show previous
1396     # credits and payments 
1397     if ( $self->enable_previous 
1398           and $self->can('_items_credits')
1399           and $self->can('_items_payments') )
1400       {
1401     
1402       # credits
1403       my $credittotal = 0;
1404       foreach my $credit (
1405         $self->_items_credits( 'template' => $template, 'trim_len' => 40 )
1406       ) {
1407
1408         my $total;
1409         $total->{'total_item'} = &$escape_function($credit->{'description'});
1410         $credittotal += $credit->{'amount'};
1411         $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1412         if ( $multisection ) {
1413           push @detail_items, {
1414             ext_description => [],
1415             ref          => '',
1416             quantity     => '',
1417             description  => &$escape_function($credit->{'description'}),
1418             amount       => $money_char . $credit->{'amount'},
1419             product_code => '',
1420             section      => $adjust_section,
1421           };
1422         } else {
1423           push @total_items, $total;
1424         }
1425
1426       }
1427       $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1428
1429       #credits (again)
1430       foreach my $credit (
1431         $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1432       ) {
1433         push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1434       }
1435
1436       # payments
1437       my $paymenttotal = 0;
1438       foreach my $payment (
1439         $self->_items_payments( 'template' => $template )
1440       ) {
1441         my $total = {};
1442         $total->{'total_item'} = &$escape_function($payment->{'description'});
1443         $paymenttotal += $payment->{'amount'};
1444         $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1445         if ( $multisection ) {
1446           push @detail_items, {
1447             ext_description => [],
1448             ref          => '',
1449             quantity     => '',
1450             description  => &$escape_function($payment->{'description'}),
1451             amount       => $money_char . $payment->{'amount'},
1452             product_code => '',
1453             section      => $adjust_section,
1454           };
1455         }else{
1456           push @total_items, $total;
1457         }
1458         push @buf, [ $payment->{'description'},
1459                      $money_char. sprintf("%10.2f", $payment->{'amount'}),
1460                    ];
1461       }
1462       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1463     
1464       if ( $multisection ) {
1465         $adjust_section->{'subtotal'} = $other_money_char.
1466                                         sprintf('%.2f', $credittotal + $paymenttotal);
1467
1468         #why this? because {sort_weight} forces the adjust_section to appear
1469         #in @extra_sections instead of @sections. obviously.
1470         push @sections, $adjust_section
1471           unless $adjust_section->{sort_weight};
1472         # do not summarize; adjustments there are shown according to 
1473         # different rules
1474       }
1475
1476       # create Balance Due message
1477       { 
1478         my $total;
1479         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1480         $total->{'total_amount'} =
1481           &$embolden_function(
1482             $other_money_char. sprintf('%.2f', #why? $summarypage 
1483                                                #  ? $self->charged +
1484                                                #    $self->billing_balance
1485                                                #  :
1486                                                    $self->owed + $pr_total
1487                                       )
1488           );
1489         if ( $multisection && !$adjust_section->{sort_weight} ) {
1490           $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1491                                            $total->{'total_amount'};
1492         } else {
1493           push @total_items, $total;
1494         }
1495         push @buf,['','-----------'];
1496         push @buf,[$self->balance_due_msg, $money_char. 
1497           sprintf("%10.2f", $balance_due ) ];
1498       }
1499
1500       if ( $conf->exists('previous_balance-show_credit')
1501           and $cust_main->balance < 0 ) {
1502         my $credit_total = {
1503           'total_item'    => &$embolden_function($self->credit_balance_msg),
1504           'total_amount'  => &$embolden_function(
1505             $other_money_char. sprintf('%.2f', -$cust_main->balance)
1506           ),
1507         };
1508         if ( $multisection ) {
1509           $adjust_section->{'posttotal'} .= $newline_token .
1510             $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1511         }
1512         else {
1513           push @total_items, $credit_total;
1514         }
1515         push @buf,['','-----------'];
1516         push @buf,[$self->credit_balance_msg, $money_char. 
1517           sprintf("%10.2f", -$cust_main->balance ) ];
1518       }
1519     }
1520
1521   } #end of default total adding ! can('_items_total')
1522
1523   if ( $multisection ) {
1524     if (    $conf->exists('svc_phone_sections')
1525          && $self->can('_items_svc_phone_sections')
1526        )
1527     {
1528       my $total;
1529       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1530       $total->{'total_amount'} =
1531         &$embolden_function(
1532           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1533         );
1534       my $last_section = pop @sections;
1535       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1536                                      $total->{'total_amount'};
1537       push @sections, $last_section;
1538     }
1539     push @sections, @$late_sections
1540       if $unsquelched;
1541   }
1542
1543   # make a discounts-available section, even without multisection
1544   if ( $conf->exists('discount-show_available') 
1545        and my @discounts_avail = $self->_items_discounts_avail ) {
1546     my $discount_section = {
1547       'description' => $self->mt('Discounts Available'),
1548       'subtotal'    => '',
1549       'no_subtotal' => 1,
1550     };
1551
1552     push @sections, $discount_section; # do not summarize
1553     push @detail_items, map { +{
1554         'ref'         => '', #should this be something else?
1555         'section'     => $discount_section,
1556         'description' => &$escape_function( $_->{description} ),
1557         'amount'      => $money_char . &$escape_function( $_->{amount} ),
1558         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1559     } } @discounts_avail;
1560   }
1561
1562   # not adding any more sections after this
1563   $invoice_data{summary_subtotals} = \@summary_subtotals;
1564
1565   # usage subtotals
1566   if ( $conf->exists('usage_class_summary')
1567        and $self->can('_items_usage_class_summary') ) {
1568     my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function, 'money_char' => $other_money_char);
1569     if ( @usage_subtotals ) {
1570       unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1571       unshift @detail_items, @usage_subtotals;
1572     }
1573   }
1574
1575   # invoice history "section" (not really a section)
1576   # not to be included in any subtotals, completely independent of 
1577   # everything...
1578   if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
1579     my %history;
1580     my %monthorder;
1581     foreach my $cust_bill ( $cust_main->cust_bill ) {
1582       # XXX hardcoded format, and currently only 'charged'; add other fields
1583       # if they become necessary
1584       my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1585       $history{$date} ||= 0;
1586       $history{$date} += $cust_bill->charged;
1587       # just so we have a numeric sort key
1588       $monthorder{$date} ||= $cust_bill->_date;
1589     }
1590     my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1591                         keys %history;
1592     my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1593     $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1594   }
1595
1596   # service locations: another option for template customization
1597   my %location_info;
1598   foreach my $item (@detail_items) {
1599     if ( $item->{locationnum} ) {
1600       $location_info{ $item->{locationnum} } ||= {
1601         FS::cust_location->by_key( $item->{locationnum} )->location_hash
1602       };
1603     }
1604   }
1605   $invoice_data{location_info} = \%location_info;
1606
1607   # debugging hook: call this with 'diag' => 1 to just get a hash of 
1608   # the invoice variables
1609   return \%invoice_data if ( $params{'diag'} );
1610
1611   # All sections and items are built; now fill in templates.
1612   my @includelist = ();
1613   push @includelist, 'summary' if $summarypage;
1614   foreach my $include ( @includelist ) {
1615
1616     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1617     my @inc_src;
1618
1619     if ( length( $conf->config($inc_file, $agentnum) ) ) {
1620
1621       @inc_src = $conf->config($inc_file, $agentnum);
1622
1623     } else {
1624
1625       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1626
1627       my $convert_map = $convert_maps{$format}{$include};
1628
1629       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1630                        s/--\@\]/$delimiters{$format}[1]/g;
1631                        $_;
1632                      } 
1633                  &$convert_map( $conf->config($inc_file, $agentnum) );
1634
1635     }
1636
1637     my $inc_tt = new Text::Template (
1638       TYPE       => 'ARRAY',
1639       SOURCE     => [ map "$_\n", @inc_src ],
1640       DELIMITERS => $delimiters{$format},
1641     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1642
1643     unless ( $inc_tt->compile() ) {
1644       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1645       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1646       die $error;
1647     }
1648
1649     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1650
1651     $invoice_data{$include} =~ s/\n+$//
1652       if ($format eq 'latex');
1653   }
1654
1655   $invoice_lines = 0;
1656   my $wasfunc = 0;
1657   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1658     /invoice_lines\((\d*)\)/;
1659     $invoice_lines += $1 || scalar(@buf);
1660     $wasfunc=1;
1661   }
1662   die "no invoice_lines() functions in template?"
1663     if ( $format eq 'template' && !$wasfunc );
1664
1665   if ( $invoice_lines ) {
1666     $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1667     $invoice_data{'total_pages'}++
1668       if scalar(@buf) % $invoice_lines;
1669   }
1670
1671   #setup subroutine for the template
1672   $invoice_data{invoice_lines} = sub {
1673     my $lines = shift || scalar(@buf);
1674     map { 
1675       scalar(@buf)
1676         ? shift @buf
1677         : [ '', '' ];
1678     }
1679     ( 1 .. $lines );
1680   };
1681
1682   if ($format eq 'template') {
1683
1684     my $lines;
1685     my @collect;
1686     while (@buf) {
1687       push @collect, split("\n",
1688         $text_template->fill_in( HASH => \%invoice_data )
1689       );
1690       $invoice_data{'page'}++;
1691     }
1692     map "$_\n", @collect;
1693
1694   } else { # this is where we actually create the invoice
1695
1696     if ( $params{no_addresses} ) {
1697       delete $invoice_data{$_} foreach qw(
1698         payname company address1 address2 city state zip country
1699       );
1700       $invoice_data{returnaddress} = '~';
1701     }
1702
1703     warn "filling in template for invoice ". $self->invnum. "\n"
1704       if $DEBUG;
1705     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1706       if $DEBUG > 1;
1707
1708     $text_template->fill_in(HASH => \%invoice_data);
1709   }
1710 }
1711
1712 sub notice_name { '('.shift->table.')'; }
1713
1714 sub template_conf { 'invoice_'; }
1715
1716 # helper routine for generating date ranges
1717 sub _prior_month30s {
1718   my $self = shift;
1719   my @ranges = (
1720    [ 1,       2592000 ], # 0-30 days ago
1721    [ 2592000, 5184000 ], # 30-60 days ago
1722    [ 5184000, 7776000 ], # 60-90 days ago
1723    [ 7776000, 0       ], # 90+   days ago
1724   );
1725
1726   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1727           $_->[1] ? $self->_date - $_->[1] - 1 : '',
1728       ] }
1729   @ranges;
1730 }
1731
1732 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1733
1734 Returns an postscript invoice, as a scalar.
1735
1736 Options can be passed as a hashref (recommended) or as a list of time, template
1737 and then any key/value pairs for any other options.
1738
1739 I<time> an optional value used to control the printing of overdue messages.  The
1740 default is now.  It isn't the date of the invoice; that's the `_date' field.
1741 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1742 L<Time::Local> and L<Date::Parse> for conversion functions.
1743
1744 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1745
1746 =cut
1747
1748 sub print_ps {
1749   my $self = shift;
1750
1751   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1752   my $ps = generate_ps($file);
1753   unlink($logofile);
1754   unlink($barcodefile) if $barcodefile;
1755
1756   $ps;
1757 }
1758
1759 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1760
1761 Returns an PDF invoice, as a scalar.
1762
1763 Options can be passed as a hashref (recommended) or as a list of time, template
1764 and then any key/value pairs for any other options.
1765
1766 I<time> an optional value used to control the printing of overdue messages.  The
1767 default is now.  It isn't the date of the invoice; that's the `_date' field.
1768 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1769 L<Time::Local> and L<Date::Parse> for conversion functions.
1770
1771 I<template>, if specified, is the name of a suffix for alternate invoices.
1772
1773 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1774
1775 =cut
1776
1777 sub print_pdf {
1778   my $self = shift;
1779
1780   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1781   my $pdf = generate_pdf($file);
1782   unlink($logofile);
1783   unlink($barcodefile) if $barcodefile;
1784
1785   $pdf;
1786 }
1787
1788 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1789
1790 Returns an HTML invoice, as a scalar.
1791
1792 I<time> an optional value used to control the printing of overdue messages.  The
1793 default is now.  It isn't the date of the invoice; that's the `_date' field.
1794 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1795 L<Time::Local> and L<Date::Parse> for conversion functions.
1796
1797 I<template>, if specified, is the name of a suffix for alternate invoices.
1798
1799 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1800
1801 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1802 when emailing the invoice as part of a multipart/related MIME email.
1803
1804 =cut
1805
1806 sub print_html {
1807   my $self = shift;
1808   my %params;
1809   if ( ref($_[0]) ) {
1810     %params = %{ shift() }; 
1811   } else {
1812     %params = @_;
1813   }
1814   $params{'format'} = 'html';
1815   
1816   $self->print_generic( %params );
1817 }
1818
1819 # quick subroutine for print_latex
1820 #
1821 # There are ten characters that LaTeX treats as special characters, which
1822 # means that they do not simply typeset themselves: 
1823 #      # $ % & ~ _ ^ \ { }
1824 #
1825 # TeX ignores blanks following an escaped character; if you want a blank (as
1826 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1827
1828 sub _latex_escape {
1829   my $value = shift;
1830   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1831   $value =~ s/([<>])/\$$1\$/g;
1832   $value;
1833 }
1834
1835 sub _html_escape {
1836   my $value = shift;
1837   encode_entities($value);
1838   $value;
1839 }
1840
1841 sub _html_escape_nbsp {
1842   my $value = _html_escape(shift);
1843   $value =~ s/ +/&nbsp;/g;
1844   $value;
1845 }
1846
1847 #utility methods for print_*
1848
1849 sub _translate_old_latex_format {
1850   warn "_translate_old_latex_format called\n"
1851     if $DEBUG; 
1852
1853   my @template = ();
1854   while ( @_ ) {
1855     my $line = shift;
1856   
1857     if ( $line =~ /^%%Detail\s*$/ ) {
1858   
1859       push @template, q![@--!,
1860                       q!  foreach my $_tr_line (@detail_items) {!,
1861                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1862                       q!      $_tr_line->{'description'} .= !, 
1863                       q!        "\\tabularnewline\n~~".!,
1864                       q!        join( "\\tabularnewline\n~~",!,
1865                       q!          @{$_tr_line->{'ext_description'}}!,
1866                       q!        );!,
1867                       q!    }!;
1868
1869       while ( ( my $line_item_line = shift )
1870               !~ /^%%EndDetail\s*$/                            ) {
1871         $line_item_line =~ s/'/\\'/g;    # nice LTS
1872         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1873         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1874         push @template, "    \$OUT .= '$line_item_line';";
1875       }
1876
1877       push @template, '}',
1878                       '--@]';
1879       #' doh, gvim
1880     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1881
1882       push @template, '[@--',
1883                       '  foreach my $_tr_line (@total_items) {';
1884
1885       while ( ( my $total_item_line = shift )
1886               !~ /^%%EndTotalDetails\s*$/                      ) {
1887         $total_item_line =~ s/'/\\'/g;    # nice LTS
1888         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
1889         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1890         push @template, "    \$OUT .= '$total_item_line';";
1891       }
1892
1893       push @template, '}',
1894                       '--@]';
1895
1896     } else {
1897       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1898       push @template, $line;  
1899     }
1900   
1901   }
1902
1903   if ($DEBUG) {
1904     warn "$_\n" foreach @template;
1905   }
1906
1907   (@template);
1908 }
1909
1910 =item terms
1911
1912 =cut
1913
1914 sub terms {
1915   my $self = shift;
1916   my $conf = $self->conf;
1917
1918   #check for an invoice-specific override
1919   return $self->invoice_terms if $self->invoice_terms;
1920   
1921   #check for a customer- specific override
1922   my $cust_main = $self->cust_main;
1923   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1924
1925   my $agentnum = '';
1926   if ( $cust_main ) {
1927     $agentnum = $cust_main->agentnum;
1928   } elsif ( my $prospect_main = $self->prospect_main ) {
1929     $agentnum = $prospect_main->agentnum;
1930   }
1931
1932   #use configured default
1933   $conf->config('invoice_default_terms', $agentnum) || '';
1934 }
1935
1936 =item due_date
1937
1938 =cut
1939
1940 sub due_date {
1941   my $self = shift;
1942   my $duedate = '';
1943   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1944     $duedate = $self->_date() + ( $1 * 86400 );
1945   } elsif ( $self->terms =~ /^End of Month$/ ) {
1946     my ($mon,$year) = (localtime($self->_date) )[4,5];
1947     $mon++;
1948     until ( $mon < 12 ) { $mon -= 12; $year++; }
1949     my $nextmonth_first = timelocal(0,0,0,1,$mon,$year);
1950     $duedate = $nextmonth_first - 86400;
1951   }
1952   $duedate;
1953 }
1954
1955 =item due_date2str
1956
1957 =cut
1958
1959 sub due_date2str {
1960   my $self = shift;
1961   $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1962 }
1963
1964 =item balance_due_msg
1965
1966 =cut
1967
1968 sub balance_due_msg {
1969   my $self = shift;
1970   my $msg = $self->mt('Balance Due');
1971   return $msg unless $self->terms; # huh?
1972   if ( !$self->conf->exists('invoice_show_prior_due_date')
1973        or $self->conf->exists('invoice_sections') ) {
1974     # if enabled, the due date is shown with Total New Charges (see 
1975     # _items_total) and not here
1976     # (yes, or if invoice_sections is enabled; this is just for compatibility)
1977     if ( $self->due_date ) {
1978       my $please_pay_by =
1979         $self->conf->config('invoice_pay_by_msg', $self->agentnum)
1980         || 'Please pay by [_1]';
1981       $msg .= ' - ' . $self->mt($please_pay_by, $self->due_date2str('short')).
1982               ' '
1983        unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum);
1984     } elsif ( $self->terms ) {
1985       $msg .= ' - '. $self->mt($self->terms);
1986     }
1987   }
1988   $msg;
1989 }
1990
1991 =item balance_due_date
1992
1993 =cut
1994
1995 sub balance_due_date {
1996   my $self = shift;
1997   my $conf = $self->conf;
1998   my $duedate = '';
1999   my $terms = $self->terms;
2000   if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2001     $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
2002   }
2003   $duedate;
2004 }
2005
2006 sub credit_balance_msg { 
2007   my $self = shift;
2008   $self->mt('Credit Balance Remaining')
2009 }
2010
2011 =item _date_pretty
2012
2013 Returns a string with the date, for example: "3/20/2008", localized for the
2014 customer.  Use _date_pretty_unlocalized for non-end-customer display use.
2015
2016 =cut
2017
2018 sub _date_pretty {
2019   my $self = shift;
2020   $self->time2str_local('short', $self->_date);
2021 }
2022
2023 =item _date_pretty_unlocalized
2024
2025 Returns a string with the date, for example: "3/20/2008", in the format
2026 configured for the back-office.  Use _date_pretty for end-customer display use.
2027
2028 =cut
2029
2030 sub _date_pretty_unlocalized {
2031   my $self = shift;
2032   time2str($date_format, $self->_date);
2033 }
2034
2035 =item email HASHREF
2036
2037 Emails this template.
2038
2039 Options are passed as a hashref.  Available options:
2040
2041 =over 4
2042
2043 =item from
2044
2045 If specified, overrides the default From: address.
2046
2047 =item notice_name
2048
2049 If specified, overrides the name of the sent document ("Invoice" or "Quotation")
2050
2051 =item template
2052
2053 (Deprecated) If specified, is the name of a suffix for alternate template files.
2054
2055 =back
2056
2057 Options accepted by generate_email can also be used.
2058
2059 =cut
2060
2061 sub email {
2062   my $self = shift;
2063   my $opt = shift || {};
2064   if ($opt and !ref($opt)) {
2065     die ref($self). '->email called with positional parameters';
2066   }
2067
2068   return if $self->hide;
2069
2070   my $error = send_email(
2071     $self->generate_email(
2072       'subject'     => $self->email_subject($opt->{template}),
2073       %$opt, # template, etc.
2074     )
2075   );
2076
2077   die "can't email: $error\n" if $error;
2078 }
2079
2080 =item generate_email OPTION => VALUE ...
2081
2082 Options:
2083
2084 =over 4
2085
2086 =item from
2087
2088 sender address, required
2089
2090 =item template
2091
2092 alternate template name, optional
2093
2094 =item subject
2095
2096 email subject, optional
2097
2098 =item notice_name
2099
2100 notice name instead of "Invoice", optional
2101
2102 =back
2103
2104 Returns an argument list to be passed to L<FS::Misc::send_email>.
2105
2106 =cut
2107
2108 use MIME::Entity;
2109 use Encode;
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 = map Encode::encode_utf8($_), $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                          Encode::encode_utf8($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 =item has_sections AGENTNUM
3797
3798 Return true if invoice_sections should be enabled for this bill.
3799  (Inherited by both cust_bill and cust_bill_void)
3800
3801 Determination:
3802 * False if not an invoice
3803 * True always if conf invoice_sections is enabled
3804 * True always if sections_by_location is enabled
3805 * True if conf invoice_sections_multilocation > 1,
3806   and location_count >= invoice_sections_multilocation
3807 * Else, False
3808
3809 =cut
3810
3811 sub has_sections {
3812   my ($self, $agentnum) = @_;
3813
3814   return 0 unless $self->invnum > 0;
3815
3816   $agentnum ||= $self->cust_main->agentnum;
3817   return 1 if $self->conf->exists('invoice_sections', $agentnum);
3818   return 1 if $self->conf->exists('sections_by_location', $agentnum);
3819
3820   my $location_min = $self->conf->config(
3821     'invoice_sections_multilocation', $agentnum,
3822   );
3823
3824   return 1
3825     if $location_min
3826     && $self->location_count >= $location_min;
3827
3828   0;
3829 }
3830
3831
3832 =item location_count
3833
3834 Return the number of locations billed on this invoice
3835
3836 =cut
3837
3838 sub location_count {
3839   my ($self) = @_;
3840   return 0 unless $self->invnum;
3841
3842   # SELECT COUNT( DISTINCT cust_pkg.locationnum )
3843   # FROM cust_bill_pkg
3844   # LEFT JOIN cust_pkg USING (pkgnum)
3845   # WHERE invnum = 278
3846   #   AND cust_bill_pkg.pkgnum > 0
3847
3848   my $result = qsearchs({
3849     select    => 'COUNT(DISTINCT cust_pkg.locationnum) as location_count',
3850     table     => 'cust_bill_pkg',
3851     addl_from => 'LEFT JOIN cust_pkg USING (pkgnum)',
3852     extra_sql => 'WHERE invnum = '.dbh->quote( $self->invnum )
3853                . '  AND cust_bill_pkg.pkgnum > 0'
3854   });
3855   ref $result ? $result->location_count : 0;
3856 }
3857
3858 1;