and turn it back off as an agent override, RT#79636
[freeside.git] / FS / FS / Template_Mixin.pm
index 5153f87..8bfc51c 100644 (file)
@@ -10,6 +10,7 @@ use vars qw( $invoice_lines @buf ); #yuck
 use List::Util qw(sum); #can't import first, it conflicts with cust_main.first
 use Date::Format;
 use Date::Language;
+use Time::Local qw( timelocal );
 use Text::Template 1.20;
 use File::Temp 0.14;
 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
@@ -18,7 +19,7 @@ use HTML::Entities;
 use Cwd;
 use FS::UID;
 use FS::Misc qw( send_email );
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbh );
 use FS::Conf;
 use FS::Misc qw( generate_ps generate_pdf );
 use FS::pkg_category;
@@ -28,9 +29,9 @@ use FS::L10N;
 
 $DEBUG = 0;
 $me = '[FS::Template_Mixin]';
-FS::UID->install_callback( sub { 
+FS::UID->install_callback( sub {
   my $conf = new FS::Conf; #global
-  $money_char  = $conf->config('money_char')  || '$';  
+  $money_char  = $conf->config('money_char')  || '$';
   $date_format = $conf->config('date_format') || '%x'; #/YY
 } );
 
@@ -120,7 +121,7 @@ default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
-I<template>, if specified, is the name of a suffix for alternate invoices.  
+I<template>, if specified, is the name of a suffix for alternate invoices.
 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
 customize invoice templates for different purposes.
 
@@ -174,7 +175,7 @@ sub print_latex {
   close $lh;
   $params{'logo_file'} = $lh->filename;
 
-  if( $conf->exists('invoice-barcode') 
+  if( $conf->exists('invoice-barcode')
         && $self->can('invoice_barcode')
         && $self->invnum ) { # don't try to barcode statements
       my $png_file = $self->invoice_barcode($dir);
@@ -186,7 +187,7 @@ sub print_latex {
       $eps_file = $1;
 
       my $curr_dir = cwd();
-      chdir($dir); 
+      chdir($dir);
       # after painfuly long experimentation, it was determined that sam2p won't
       #        accept : and other chars in the path, no matter how hard I tried to
       # escape them, hence the chdir (and chdir back, just to be safe)
@@ -199,7 +200,7 @@ sub print_latex {
   }
 
   my @filled_in = $self->print_generic( %params );
-  
+
   my $fh = new File::Temp( TEMPLATE => $tmp_template,
                            DIR      => $dir,
                            SUFFIX   => '.tex',
@@ -298,7 +299,7 @@ before that line item (quotations only)
 
 =item template
 
-Dprecated.  Used as a suffix for a configuration template.  Please 
+Deprecated.  Used as a suffix for a configuration template.  Please
 don't use this, it deprecated in favor of more flexible alternatives.
 
 =back
@@ -343,6 +344,8 @@ sub print_generic {
   $templatefile .= "_$template"
     if length($template) && $conf->exists($templatefile."_$template");
 
+  $self->set('_template',$template);
+
   # the base template
   my @invoice_template = map "$_\n", $conf->config($templatefile)
     or die "cannot load config data $templatefile";
@@ -354,7 +357,7 @@ sub print_generic {
          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
          #$old_latex = 'true';
          #@invoice_template = _translate_old_latex_format(@invoice_template);
-  } 
+  }
 
   warn "$me print_generic creating T:T object\n"
     if $DEBUG > 1;
@@ -373,7 +376,7 @@ sub print_generic {
 
 
   # additional substitution could possibly cause breakage in existing templates
-  my %convert_maps = ( 
+  my %convert_maps = (
     'latex' => {
                  'notes'         => sub { map "$_", @_ },
                  'footer'        => sub { map "$_", @_ },
@@ -385,7 +388,7 @@ sub print_generic {
     'html'  => {
                  'notes' =>
                    sub {
-                     map { 
+                     map {
                        s/%%(.*)$/<!-- $1 -->/g;
                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
                        s/\\begin\{enumerate\}/<ol>/g;
@@ -405,7 +408,7 @@ sub print_generic {
                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
                  'returnaddress' =>
                    sub {
-                     map { 
+                     map {
                        s/~/&nbsp;/g;
                        s/\\\\\*?\s*$/<BR>/;
                        s/\\hyphenation\{[\w\s\-]+}//;
@@ -419,7 +422,7 @@ sub print_generic {
     'template' => {
                  'notes' =>
                    sub {
-                     map { 
+                     map {
                        s/%%.*$//g;
                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
                        s/\\begin\{enumerate\}//g;
@@ -437,7 +440,7 @@ sub print_generic {
                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
                  'returnaddress' =>
                    sub {
-                     map { 
+                     map {
                        s/~/ /g;
                        s/\\\\\*?\s*$/\n/;             # dubious
                        s/\\hyphenation\{[\w\s\-]+}//;
@@ -547,7 +550,7 @@ sub print_generic {
     'quotationnum'    => $self->quotationnum,
     'no_date'         => $params{'no_date'},
     '_date'           => ( $params{'no_date'} ? '' : $self->_date ),
-      # workaround for inconsistent behavior in the early plain text 
+      # workaround for inconsistent behavior in the early plain text
       # templates; see RT#28271
     'date'            => ( $params{'no_date'}
                              ? ''
@@ -562,6 +565,7 @@ sub print_generic {
     'notice_name'     => $notice_name, # escape?
     'current_charges' => sprintf("%.2f", $self->charged),
     'duedate'         => $self->due_date2str('rdate'), #date_format?
+    'duedate_long'    => $self->due_date2str('long'),
 
     #customer info
     'custnum'         => $cust_main->display_custnum,
@@ -579,7 +583,7 @@ sub print_generic {
     'smallernotes'    => $conf->exists('invoice-smallernotes'),
     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
-   
+
     #layout info -- would be fancy to calc some of this and bury the template
     #               here in the code
     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
@@ -601,7 +605,10 @@ sub print_generic {
     'total_pages'     => 1,
 
   );
+
+  #quotations have $name
+  $invoice_data{'name'} = $invoice_data{'payname'};
+
   #localization
   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
   # prototype here to silence warnings
@@ -619,7 +626,7 @@ sub print_generic {
 
   $invoice_data{'bill_period'} = '';
   $invoice_data{'bill_period'} =
-      $self->time2str_local('%e %h', $min_sdate, $format) 
+      $self->time2str_local('%e %h', $min_sdate, $format)
       . " to " .
       $self->time2str_local('%e %h', $max_edate, $format)
     if ($max_edate != 0 && $min_sdate != 999999999999);
@@ -629,7 +636,7 @@ sub print_generic {
     my $pkg_class =
       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
     $invoice_data{finance_section} = $pkg_class->categoryname;
-  } 
+  }
   $invoice_data{finance_amount} = '0.00';
   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
 
@@ -646,14 +653,15 @@ sub print_generic {
   $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
   $invoice_data{'ship_country'} = ''
     if ( $invoice_data{'ship_country'} eq $countrydefault );
-  
+
   $invoice_data{'cid'} = $params{'cid'}
     if $params{'cid'};
 
-  if ( $cust_main->country eq $countrydefault ) {
-    $invoice_data{'country'} = '';
-  } else {
+  if ( $cust_main->bill_locationnum
+       && $cust_main->bill_location->country ne $countrydefault ) {
     $invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
+  } else {
+    $invoice_data{'country'} = '';
   }
 
   my @address = ();
@@ -686,18 +694,34 @@ sub print_generic {
   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
     if $params{'barcode_cid'};
 
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
-  #my $balance_due = $self->owed + $pr_total - $cr_total;
-  my $balance_due = $self->owed;
-  if ( $self->enable_previous ) {
-    $balance_due += $pr_total;
-  }
-  # otherwise the previous balance is not shown, so including it in the
-  # balance due is just confusing
 
-  # the sum of amount owed on all invoices
-  # (this is used in the summary & on the payment coupon)
+  # re: rt:78190
+  #   using owed_on_invoice() instead of owed() here for $balance_due
+  #   using _items_previous_total() instead of ->previous() for $pr_total
+  #
+  #   owed_on_invoice() is aware of configuration flags that affect how an
+  #     invoice is rendered.  May not return actual current balance. Will
+  #     return balance appropriate for the invoice being rendered, based
+  #     on which past due items, current charges, and future payments are
+  #     displayed.
+  #
+  #   Going forward, usage of owed(), or bypassing cust_bill helper methods
+  #     when generating invoice lines may lead to incorrect or misleading
+  #     math on invoices.
+  #
+  #   Helper methods that are aware of invoicing conf flags:
+  #   - owed_on_invoice          # use instead of owed()
+  #   - _items_previous()        # use instead of previous()
+  #   - _items_credits()         # use instead of cust_credit()
+  #   - _items_payments()
+  #   - _items_total()
+  #   - _items_previous_total()  # use instead of previous()
+  #   - _items_payments_total()
+  #   - _items_credits_total()   # use instead of cust_credit()
+
+  my $pr_total    = $self->_items_previous_total();
+
+  my $balance_due = $self->owed_on_invoice();
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
   # flag telling this invoice to have a first-page summary
@@ -707,128 +731,101 @@ sub print_generic {
     # XXX should be an FS::cust_bill method to set the defaults, instead
     # of checking the type here
 
-    # info from customer's last invoice before this one, for some 
+    # info from customer's last invoice before this one, for some
     # summary formats
     $invoice_data{'last_bill'} = {};
-    my $last_bill = $self->previous_bill;
-    if ( $last_bill ) {
 
-      # "balance_date_range" unfortunately is unsuitable for this, since it
-      # cares about application dates.  We want to know the sum of all 
-      # _top-level transactions_ dated before the last invoice.
-      #
-      # still do this for the "Previous Balance" line of the summary block
-      my @sql =
-        map "$_ WHERE _date <= ? AND custnum = ?", (
-          "SELECT      COALESCE( SUM(charged), 0 ) FROM cust_bill",
-          "SELECT -1 * COALESCE( SUM(amount),  0 ) FROM cust_credit",
-          "SELECT -1 * COALESCE( SUM(paid),    0 ) FROM cust_pay",
-          "SELECT      COALESCE( SUM(refund),  0 ) FROM cust_refund",
-        );
+    #    my $last_bill = $self->previous_bill;
+    # if ( $last_bill ) {
 
-      # the customer's current balance immediately after generating the last 
-      # bill
+    # Populate template stash for previous balance and payments
+    if ($pr_total) {
+      # Used on summary page as "Previous Balance"
+      $invoice_data{'true_previous_balance'} = sprintf("%.2f", $pr_total);
 
-      my $last_bill_balance = $last_bill->charged;
-      foreach (@sql) {
-        my $delta = FS::Record->scalar_sql(
-          $_,
-          $last_bill->_date - 1,
-          $self->custnum,
-        );
-        $last_bill_balance += $delta;
-      }
-
-      $last_bill_balance = sprintf("%.2f", $last_bill_balance);
-
-      warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
-        $last_bill->invnum,
-        $self->time2str_local('%D', $last_bill->_date),
-        $last_bill_balance
-      ) if $DEBUG > 0;
-      # ("true_previous_balance" is a terrible name, but at least it's no
-      # longer stored in the database)
-      $invoice_data{'true_previous_balance'} = $last_bill_balance;
-
-      # Now, get all applications of credits/payments dated on or after the
-      # previous bill, to invoices before the current bill. (The
-      # credit/payment date restriction prevents these from intersecting
-      # the "Previous Balance" set.)
-      # These are "adjustments". The past due balance will be shown as
-      # Previous Balance - Adjustments.
-      my $adjustments = 0;
-      @sql = map {
-        "SELECT COALESCE(SUM(y.amount),0) FROM $_ JOIN cust_bill USING (invnum)
-         WHERE cust_bill._date < ?
-           AND x._date >= ?
-           AND cust_bill.custnum = ?"
-        } "cust_credit AS x JOIN cust_credit_bill y USING (crednum)",
-          "cust_pay    AS x JOIN cust_bill_pay    y USING (paynum)"
-      ;
-      foreach (@sql) {
-        my $delta = FS::Record->scalar_sql(
-          $_,
-          $self->_date,
-          $last_bill->_date,
-          $self->custnum,
-        );
-        $adjustments += $delta;
-      }
-      $invoice_data{'balance_adjustments'} = sprintf("%.2f", $adjustments);
-
-      warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
-                   $invoice_data{'balance_adjustments'}
-      ) if $DEBUG > 0;
+      # Used on summary page as "Payments"
+      $invoice_data{'balance_adjustments'} = sprintf("%.2f",
+        $self->_items_payments_total() + $self->_items_credits_total()
+      );
 
-      # the sum of amount owed on all previous invoices
-      # ($pr_total is used elsewhere but not as $previous_balance)
+      # Used in invoice template as "Previous Balance"
       $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
 
-      $invoice_data{'last_bill'}{'_date'} = $last_bill->_date; #unformatted
-      my (@payments, @credits);
-      # for formats that itemize previous payments
-      foreach my $cust_pay ( qsearch('cust_pay', {
-                              'custnum' => $self->custnum,
-                              '_date'   => { op => '>=',
-                                             value => $last_bill->_date }
-                             } ) )
-      {
-        next if $cust_pay->_date > $self->_date;
-        push @payments, {
-            '_date'       => $cust_pay->_date,
-            'date'        => $self->time2str_local('long', $cust_pay->_date, $format),
-            'payinfo'     => $cust_pay->payby_payinfo_pretty,
-            'amount'      => sprintf('%.2f', $cust_pay->paid),
-        };
-        # not concerned about applications
-      }
-      foreach my $cust_credit ( qsearch('cust_credit', {
-                              'custnum' => $self->custnum,
-                              '_date'   => { op => '>=',
-                                             value => $last_bill->_date }
-                             } ) )
-      {
-        next if $cust_credit->_date > $self->_date;
-        push @credits, {
-            '_date'       => $cust_credit->_date,
-            'date'        => $self->time2str_local('long', $cust_credit->_date, $format),
-            'creditreason'=> $cust_credit->reason,
-            'amount'      => sprintf('%.2f', $cust_credit->amount),
-        };
+      # $invoice_data{last_bill}{_date}:
+      # Not used in default templates, but may be in use by someone
+      #
+      # ! May be a problem field if they are using it... this field
+      #   stores the date of the previous invoice... it is possible to
+      #   carry a balance, but have the immediately previous invoice paid off.
+      #   In this case, this field might be presenting bad data?  Not
+      #   altering the problematic behavior, because someone might be
+      #   expecting this bad behavior in their templates for some other
+      #   purpose, such as a "your last bill was dated %_date%"
+      my $last_bill = $self->previous_bill;
+      $invoice_data{'last_bill'}{'_date'}
+        = ref $last_bill
+        ? $last_bill->_date()
+        : undef;
+
+      # $invoice_data{previous_payments}
+      # Not used in default templates, but may be in use by someone
+      #
+      # Returns an array of hrefs representing payments, each with keys:
+      #  - _date:       epoch timestamp
+      #  - date:        text formatted date
+      #  - amount:      money formatted amount string
+      #  - payinfo:     string from payby_payinfo_pretty()
+      #  - paynum:      id for cust_pay
+      #  - description: Text description for bill line item
+      #
+      my @payments = $self->_items_payments();
+      $invoice_data{previous_payments} = \@payments;
+
+      # $invoice_data{previous_credits}
+      # Not used in default templates, but may be in use by someone
+      #
+      # Returns an array of hrefs representing credits, each with keys:
+      #  - _date:        epoch timestamp
+      #  - date:         text formatted date
+      #  - amount:       money formatted amount string
+      #  - crednum:      id for cust_credit
+      #  - description:  Text description for bill line item
+      #  - creditreason: reason() from cust_credit
+      #
+      my @credits = $self->_items_credits();
+      $invoice_data{previous_credits} = \@credits;
+
+      # Populate formatted date field
+      for my $pmt_href (@payments, @credits) {
+        $pmt_href->{date} = $self->time2str_local(
+          'long',
+          $pmt_href->{_date},
+          $format
+        );
       }
-      $invoice_data{'previous_payments'} = \@payments;
-      $invoice_data{'previous_credits'}  = \@credits;
+
     } else {
-      # there is no $last_bill
+      # There are no outstanding invoices    = YAPH
       $invoice_data{'true_previous_balance'} =
       $invoice_data{'balance_adjustments'}   =
       $invoice_data{'previous_balance'}      = '0.00';
-      $invoice_data{'previous_payments'} = [];
-      $invoice_data{'previous_credits'} = [];
+      $invoice_data{'previous_payments'}     =
+      $invoice_data{'previous_credits'}      = [];
     }
-    if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+
+    # Condencing a lot of debug staements here
+    if ($DEBUG) {
+      warn "\$invoice_data{$_}: $invoice_data{$_}"
+        for qw(
+          true_previous_balance
+          balance_adjustments
+          previous_balance
+          previous_payments
+          previous_credits
+        );
+    }
+
+    if ( $conf->config_bool('invoice_usesummary', $agentnum) ) {
       $invoice_data{'summarypage'} = $summarypage = 1;
     }
 
@@ -895,9 +892,9 @@ sub print_generic {
 # if (well, probably when) we still need PO numbers in the brave new world of
 # 4.x, then we'll have to add them back as their own customer fields
 #  # let invoices use either of these as needed
-#  $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
+#  $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
 #    ? $cust_main->payinfo : '';
-#  $invoice_data{'po_line'} = 
+#  $invoice_data{'po_line'} =
 #    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
 #      ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
 #      : $nbsp;
@@ -936,8 +933,6 @@ sub print_generic {
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $self->has_sections;
-  $conf->exists($tc.'sections', $cust_main->agentnum) ||
-                     $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
   $invoice_data{'multisection'} = $multisection;
   my $late_sections;
   my $extra_sections = [];
@@ -945,30 +940,40 @@ sub print_generic {
 
   # default section ('Charges')
   my $default_section = { 'description' => '',
-                          'subtotal'    => '', 
+                          'subtotal'    => '',
                           'no_subtotal' => 1,
                         };
 
   # Previous Charges section
   # subtotal is the first return value from $self->previous
   my $previous_section;
-  # if the invoice has major sections, or if we're summarizing previous 
+  # if the invoice has major sections, or if we're summarizing previous
   # charges with a single line, or if we've been specifically told to put them
   # in a section, create a section for previous charges:
   if ( $multisection or
        $conf->exists('previous_balance-summary_only') or
        $conf->exists('previous_balance-section') ) {
-    
+
     $previous_section =  { 'description' => $self->mt('Previous Charges'),
                            'subtotal'    => $other_money_char.
                                             sprintf('%.2f', $pr_total),
                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
                          };
-    $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
-      join(' / ', map { $cust_main->balance_date_range(@$_) }
-                  $self->_prior_month30s
-          )
-      if $conf->exists('invoice_include_aging');
+
+    # Include balance aging line and template variables
+    my @aged_balances = $self->_items_aging_balances();
+    ( $invoice_data{aged_balance_current},
+      $invoice_data{aged_balance_30d},
+      $invoice_data{aged_balance_60d},
+      $invoice_data{aged_balance_90d}
+    ) = @aged_balances;
+
+    if ($conf->exists('invoice_include_aging')) {
+      $previous_section->{posttotal} = sprintf(
+        '0 / 30 / 60 / 90 days overdue %.2f / %.2f / %.2f / %.2f',
+        @aged_balances,
+      );
+    }
 
   } else {
     # otherwise put them in the main section
@@ -1001,7 +1006,7 @@ sub print_generic {
     push @detail_items, @$extra_lines if $extra_lines;
 
     # the code is written so that both methods can be used together, but
-    # we haven't yet changed the template to take advantage of that, so for 
+    # we haven't yet changed the template to take advantage of that, so for
     # now, treat them as mutually exclusive.
     my %section_method = ( by_category => 1 );
     if ( $conf->config($tc.'sections_method') eq 'location' ) {
@@ -1047,7 +1052,7 @@ sub print_generic {
     my @finance_charges;
     my @charges;
     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
-      if ( $invoice_data{finance_section} and 
+      if ( $invoice_data{finance_section} and
         grep { $_->section eq $invoice_data{finance_section} }
            $cust_bill_pkg->cust_bill_pkg_display ) {
         # I think these are always setup fees, but just to be sure...
@@ -1056,7 +1061,7 @@ sub print_generic {
         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
       }
     }
-    $invoice_data{finance_amount} = 
+    $invoice_data{finance_amount} =
       sprintf('%.2f', sum( @finance_charges ) || 0);
     $default_section->{subtotal} = $other_money_char.
                                     sprintf('%.2f', sum( @charges ) || 0);
@@ -1110,7 +1115,7 @@ sub print_generic {
         #quantity        => 1, # not really correct
         section         => $previous_section, # which might be $default_section
         description     => &$escape_function($line_item->{'description'}),
-        ext_description => [ map { &$escape_function($_) } 
+        ext_description => [ map { &$escape_function($_) }
                              @{ $line_item->{'ext_description'} || [] }
                            ],
         amount          => $money_char . $line_item->{'amount'},
@@ -1125,20 +1130,20 @@ sub print_generic {
 
   }
 
-  if ( @pr_cust_bill && $self->enable_previous ) {
+  if ( $pr_total && $self->enable_previous ) {
     push @buf, ['','-----------'];
     push @buf, [ $self->mt('Total Previous Balance'),
                  $money_char. sprintf("%10.2f", $pr_total) ];
     push @buf, ['',''];
   }
+
   if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
       warn "$me adding DID summary\n"
         if $DEBUG > 1;
 
       my ($didsummary,$minutes) = $self->_did_summary;
       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
-      push @detail_items, 
+      push @detail_items,
        { 'description' => $didsummary_desc,
            'ext_description' => [ $didsummary, $minutes ],
        };
@@ -1156,14 +1161,27 @@ sub print_generic {
       if ( $invoice_data{finance_section} &&
            $section->{'description'} eq $invoice_data{finance_section} );
 
-    $section->{'subtotal'} = $other_money_char.
-                             sprintf('%.2f', $section->{'subtotal'})
-      if $multisection;
+    if ( $multisection ) {
+
+      if ( ref($section->{'subtotal'}) ) {
 
-    # continue some normalization
-    $section->{'amount'}   = $section->{'subtotal'}
-      if $multisection;
+        $section->{'subtotal'} =
+          sprintf("$other_money_char%.2f to $other_money_char%.2f",
+                    $section->{'subtotal'}[0],
+                    $section->{'subtotal'}[1]
+                 );
+
+      } else {
+
+        $section->{'subtotal'} = $other_money_char.
+                                 sprintf('%.2f', $section->{'subtotal'})
+
+      }
+
+      # continue some normalization
+      $section->{'amount'}   = $section->{'subtotal'}
 
+    }
 
     if ( $section->{'description'} ) {
       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
@@ -1176,6 +1194,8 @@ sub print_generic {
 
     my %options = ();
     $options{'section'} = $section if $multisection;
+    $options{'section_with_taxes'} = 1
+      if $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum);
     $options{'format'} = $format;
     $options{'escape_function'} = $escape_function;
     $options{'no_usage'} = 1 unless $unsquelched;
@@ -1188,6 +1208,8 @@ sub print_generic {
     warn "$me   searching for line items\n"
       if $DEBUG > 1;
 
+    my %section_tax_lines;
+    my %seen_tax_lines;
     foreach my $line_item ( $self->_items_pkg(%options),
                             $self->_items_fee(%options) ) {
 
@@ -1211,20 +1233,67 @@ sub print_generic {
         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
       }
       $line_item->{'ext_description'} ||= [];
+
+      if ( $options{section_with_taxes} && ref $line_item->{pkg_tax} ) {
+        for my $line_tax ( @{$ line_item->{pkg_tax} } ) {
+
+          # It is rarely possible for the same tax record to be presented here
+          # multiple times.  See cust_bill_pkg::_pkg_tax_list for more info
+          next if $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} };
+          $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} } = 1;
+
+          $section_tax_lines{ $line_tax->{taxname} } += $line_tax->{amount};
+        }
+      }
+
       push @detail_items, $line_item;
     }
 
+    # If conf flag invoice_sections_with_taxes:
+    # - Add @detail_items for taxes into each section
+    # - Update section subtotal to include taxes
+    if ( $options{section_with_taxes} && %section_tax_lines ) {
+      for my $taxname ( keys %section_tax_lines ) {
+
+        push @detail_items, {
+          section => $section,
+          amount  => sprintf($money_char."%.2f",$section_tax_lines{$taxname}),
+          description => &$escape_function($taxname),
+        };
+
+        # Append taxes to total.  If line format resembles "$5.00 to $12.00"
+        # append to the second value.
+
+        # $section->{subtotal} = '$5.00 to 12.00'; # for testing:
+        if ($section->{subtotal} =~ /to/) {
+          my @subtotal = split /\s/, $section->{subtotal};
+          $subtotal[2] =~ s/[^\d\.]//g;
+          $subtotal[2] = sprintf(
+            $money_char."%.2f",
+            ( $subtotal[2] + $section_tax_lines{$taxname} )
+          );
+          $section->{subtotal} = join ' ', @subtotal;
+        } else {
+          $section->{subtotal} =~ s/[^\d\.]//g;
+          $section->{subtotal} = sprintf(
+            $money_char . "%.2f",
+            ( $section->{subtotal} + $section_tax_lines{$taxname} )
+          );
+        }
+
+      }
+    }
+
     if ( $section->{'description'} ) {
       push @buf, ( ['','-----------'],
                    [ $section->{'description'}. ' sub-total',
-                      $section->{'subtotal'} # already formatted this 
+                      $section->{'subtotal'} # already formatted this
                    ],
                    [ '', '' ],
                    [ '', '' ],
                  );
     }
-  
+
   }
 
   $invoice_data{current_less_finance} =
@@ -1240,11 +1309,13 @@ sub print_generic {
     if $DEBUG > 1;
 
   # create a tax section if we don't yet have one
+  my @items_tax = $self->_items_tax;
   my $tax_description = 'Taxes, Surcharges, and Fees';
   my $tax_section =
     List::Util::first { $_->{description} eq $tax_description } @sections;
   if (!$tax_section) {
     $tax_section = { 'description' => $tax_description };
+    push @sections, $tax_section if $multisection and @items_tax > 0;
   }
   $tax_section->{tax_section} = 1; # mark this section as containing taxes
   # if this is an existing tax section, we're merging the tax items into it.
@@ -1259,9 +1330,6 @@ sub print_generic {
   #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
   #$tax_section->{'sort_weight'} = $tax_weight;
 
-  my @items_tax = $self->_items_tax;
-  push @sections, $tax_section if $multisection and @items_tax > 0;
-
   foreach my $tax ( @items_tax ) {
 
     $taxtotal += $tax->{'amount'};
@@ -1295,7 +1363,7 @@ sub print_generic {
               ];
 
   }
+
   if ( @items_tax ) {
     my $total = {};
     $total->{'total_item'} = $self->mt('Sub-total');
@@ -1316,13 +1384,20 @@ sub print_generic {
         $tax_section->{'description'} = $self->mt($tax_description);
         $tax_section->{'summarized'} = '';
 
-        # append it if it's not already there
-        if ( !grep $tax_section, @sections ) {
+        if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
+
+          # remove tax section if taxes are itemized within other sections
+          @sections = grep{ $_ ne $tax_section } @sections;
+
+        } elsif ( !grep $tax_section, @sections ) {
+
+          # append it if it's not already there
           push @sections, $tax_section;
           push @summary_subtotals, $tax_section;
+
         }
-      }
 
+      }
     } else {
       unshift @total_items, $total;
     }
@@ -1351,7 +1426,7 @@ sub print_generic {
     }
 
   }
-  
+
   if ( $self->can('_items_total') ) { # should always be true now
 
     # even for multisection, need plain text version
@@ -1363,7 +1438,16 @@ sub print_generic {
     foreach ( @new_total_items ) {
       my ($item, $amount) = ($_->{'total_item'}, $_->{'total_amount'});
       $_->{'total_item'}   = &$embolden_function( $item );
-      $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
+
+      if ( ref($amount) ) {
+        $_->{'total_amount'} = &$embolden_function(
+                                 $other_money_char.$amount->[0]. ' to '.
+                                 $other_money_char.$amount->[1]
+                               );
+      } else {
+        $_->{'total_amount'} = &$embolden_function( $other_money_char.$amount );
+      }
+
       # but if it's multisection, don't append to @total_items. the adjust
       # section has all this stuff
       push @total_items, $_ if !$multisection;
@@ -1373,12 +1457,12 @@ sub print_generic {
     push @buf, [ '', '' ];
 
     # if we're showing previous invoices, also show previous
-    # credits and payments 
-    if ( $self->enable_previous 
+    # credits and payments
+    if ( $self->enable_previous
           and $self->can('_items_credits')
           and $self->can('_items_payments') )
       {
-    
+
       # credits
       my $credittotal = 0;
       foreach my $credit (
@@ -1440,7 +1524,7 @@ sub print_generic {
                    ];
       }
       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-    
+
       if ( $multisection ) {
         $adjust_section->{'subtotal'} = $other_money_char.
                                         sprintf('%.2f', $credittotal + $paymenttotal);
@@ -1449,21 +1533,21 @@ sub print_generic {
         #in @extra_sections instead of @sections. obviously.
         push @sections, $adjust_section
           unless $adjust_section->{sort_weight};
-        # do not summarize; adjustments there are shown according to 
+        # do not summarize; adjustments there are shown according to
         # different rules
       }
 
       # create Balance Due message
-      { 
+      {
         my $total;
         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
         $total->{'total_amount'} =
           &$embolden_function(
-            $other_money_char. sprintf('%.2f', #why? $summarypage 
+            $other_money_char. sprintf('%.2f', #why? $summarypage
                                                #  ? $self->charged +
                                                #    $self->billing_balance
                                                #  :
-                                                   $self->owed + $pr_total
+                                                   $balance_due
                                       )
           );
         if ( $multisection && !$adjust_section->{sort_weight} ) {
@@ -1473,7 +1557,7 @@ sub print_generic {
           push @total_items, $total;
         }
         push @buf,['','-----------'];
-        push @buf,[$self->balance_due_msg, $money_char. 
+        push @buf,[$self->balance_due_msg, $money_char.
           sprintf("%10.2f", $balance_due ) ];
       }
 
@@ -1493,7 +1577,7 @@ sub print_generic {
           push @total_items, $credit_total;
         }
         push @buf,['','-----------'];
-        push @buf,[$self->credit_balance_msg, $money_char. 
+        push @buf,[$self->credit_balance_msg, $money_char.
           sprintf("%10.2f", -$cust_main->balance ) ];
       }
     }
@@ -1509,7 +1593,7 @@ sub print_generic {
       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
       $total->{'total_amount'} =
         &$embolden_function(
-          $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
+          $other_money_char. sprintf('%.2f', $balance_due)
         );
       my $last_section = pop @sections;
       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
@@ -1521,7 +1605,7 @@ sub print_generic {
   }
 
   # make a discounts-available section, even without multisection
-  if ( $conf->exists('discount-show_available') 
+  if ( $conf->exists('discount-show_available')
        and my @discounts_avail = $self->_items_discounts_avail ) {
     my $discount_section = {
       'description' => $self->mt('Discounts Available'),
@@ -1553,7 +1637,7 @@ sub print_generic {
   }
 
   # invoice history "section" (not really a section)
-  # not to be included in any subtotals, completely independent of 
+  # not to be included in any subtotals, completely independent of
   # everything...
   if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
     my %history;
@@ -1584,7 +1668,7 @@ sub print_generic {
   }
   $invoice_data{location_info} = \%location_info;
 
-  # debugging hook: call this with 'diag' => 1 to just get a hash of 
+  # debugging hook: call this with 'diag' => 1 to just get a hash of
   # the invoice variables
   return \%invoice_data if ( $params{'diag'} );
 
@@ -1609,7 +1693,7 @@ sub print_generic {
       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
                        s/--\@\]/$delimiters{$format}[1]/g;
                        $_;
-                     } 
+                     }
                  &$convert_map( $conf->config($inc_file, $agentnum) );
 
     }
@@ -1642,24 +1726,24 @@ sub print_generic {
   die "no invoice_lines() functions in template?"
     if ( $format eq 'template' && !$wasfunc );
 
-  if ($format eq 'template') {
+  if ( $invoice_lines ) {
+    $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
+    $invoice_data{'total_pages'}++
+      if scalar(@buf) % $invoice_lines;
+  }
 
-    if ( $invoice_lines ) {
-      $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
-      $invoice_data{'total_pages'}++
-        if scalar(@buf) % $invoice_lines;
+  #setup subroutine for the template
+  $invoice_data{invoice_lines} = sub {
+    my $lines = shift || scalar(@buf);
+    map {
+      scalar(@buf)
+        ? shift @buf
+        : [ '', '' ];
     }
+    ( 1 .. $lines );
+  };
 
-    #setup subroutine for the template
-    $invoice_data{invoice_lines} = sub {
-      my $lines = shift || scalar(@buf);
-      map { 
-        scalar(@buf)
-          ? shift @buf
-          : [ '', '' ];
-      }
-      ( 1 .. $lines );
-    };
+  if ($format eq 'template') {
 
     my $lines;
     my @collect;
@@ -1696,22 +1780,6 @@ sub template_conf { warn "bare FS::Template_Mixin::template_conf";
   'invoice_';
 }
 
-# helper routine for generating date ranges
-sub _prior_month30s {
-  my $self = shift;
-  my @ranges = (
-   [ 1,       2592000 ], # 0-30 days ago
-   [ 2592000, 5184000 ], # 30-60 days ago
-   [ 5184000, 7776000 ], # 60-90 days ago
-   [ 7776000, 0       ], # 90+   days ago
-  );
-
-  map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
-          $_->[1] ? $self->_date - $_->[1] - 1 : '',
-      ] }
-  @ranges;
-}
-
 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an postscript invoice, as a scalar.
@@ -1790,23 +1858,23 @@ sub print_html {
   my $self = shift;
   my %params;
   if ( ref($_[0]) ) {
-    %params = %{ shift() }; 
+    %params = %{ shift() };
   } else {
     %params = @_;
   }
   $params{'format'} = 'html';
-  
+
   $self->print_generic( %params );
 }
 
 # quick subroutine for print_latex
 #
 # There are ten characters that LaTeX treats as special characters, which
-# means that they do not simply typeset themselves: 
+# means that they do not simply typeset themselves:
 #      # $ % & ~ _ ^ \ { }
 #
 # TeX ignores blanks following an escaped character; if you want a blank (as
-# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
+# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
 
 sub _latex_escape {
   my $value = shift;
@@ -1831,18 +1899,18 @@ sub _html_escape_nbsp {
 
 sub _translate_old_latex_format {
   warn "_translate_old_latex_format called\n"
-    if $DEBUG; 
+    if $DEBUG;
 
   my @template = ();
   while ( @_ ) {
     my $line = shift;
-  
+
     if ( $line =~ /^%%Detail\s*$/ ) {
-  
+
       push @template, q![@--!,
                       q!  foreach my $_tr_line (@detail_items) {!,
                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
-                      q!      $_tr_line->{'description'} .= !, 
+                      q!      $_tr_line->{'description'} .= !,
                       q!        "\\tabularnewline\n~~".!,
                       q!        join( "\\tabularnewline\n~~",!,
                       q!          @{$_tr_line->{'ext_description'}}!,
@@ -1878,9 +1946,9 @@ sub _translate_old_latex_format {
 
     } else {
       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
-      push @template, $line;  
+      push @template, $line;
     }
-  
+
   }
 
   if ($DEBUG) {
@@ -1900,7 +1968,7 @@ sub terms {
 
   #check for an invoice-specific override
   return $self->invoice_terms if $self->invoice_terms;
-  
+
   #check for a customer- specific override
   my $cust_main = $self->cust_main;
   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
@@ -1925,6 +1993,12 @@ sub due_date {
   my $duedate = '';
   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
     $duedate = $self->_date() + ( $1 * 86400 );
+  } elsif ( $self->terms =~ /^End of Month$/ ) {
+    my ($mon,$year) = (localtime($self->_date) )[4,5];
+    $mon++;
+    until ( $mon < 12 ) { $mon -= 12; $year++; }
+    my $nextmonth_first = timelocal(0,0,0,1,$mon,$year);
+    $duedate = $nextmonth_first - 86400;
   }
   $duedate;
 }
@@ -1938,6 +2012,23 @@ sub due_date2str {
   $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
 }
 
+=item invoice_pay_by_msg
+
+  displays the invoice_pay_by_msg or default Please pay by [_1] if empty.
+
+=cut
+
+sub invoice_pay_by_msg {
+  my $self = shift;
+  my $msg = '';
+  my $please_pay_by =
+        $self->conf->config('invoice_pay_by_msg', $self->agentnum)
+        || 'Please pay by [_1]';
+  $msg .= ' - ' . $self->mt($please_pay_by, $self->due_date2str('short')) . ' ';
+
+  $msg;
+}
+
 =item balance_due_msg
 
 =cut
@@ -1947,13 +2038,13 @@ sub balance_due_msg {
   my $msg = $self->mt('Balance Due');
   return $msg unless $self->terms; # huh?
   if ( !$self->conf->exists('invoice_show_prior_due_date')
-       or $self->conf->exists('invoice_sections') ) {
-    # if enabled, the due date is shown with Total New Charges (see 
+       || $self->has_sections ) {
+    # if enabled, the due date is shown with Total New Charges (see
     # _items_total) and not here
     # (yes, or if invoice_sections is enabled; this is just for compatibility)
     if ( $self->due_date ) {
-      $msg .= ' - ' . $self->mt('Please pay by'). ' '.
-        $self->due_date2str('short');
+      $msg .= $self->invoice_pay_by_msg
+       unless $self->conf->config_bool('invoice_omit_due_date',$self->agentnum);
     } elsif ( $self->terms ) {
       $msg .= ' - '. $self->mt($self->terms);
     }
@@ -1976,7 +2067,7 @@ sub balance_due_date {
   $duedate;
 }
 
-sub credit_balance_msg { 
+sub credit_balance_msg {
   my $self = shift;
   $self->mt('Credit Balance Remaining')
 }
@@ -2079,6 +2170,7 @@ Returns an argument list to be passed to L<FS::Misc::send_email>.
 =cut
 
 use MIME::Entity;
+use Encode;
 
 sub generate_email {
 
@@ -2143,7 +2235,7 @@ sub generate_email {
         if $DEBUG;
       @text = $conf->config($tc.'email_pdf_note');
       $html = join('<BR>', @text);
-  
+
     } # else use the plain text invoice
   }
 
@@ -2155,7 +2247,7 @@ sub generate_email {
         if $DEBUG;
 
       # 'print_text' argument is no longer used
-      @text = $self->print_text(\%args);
+      @text = map Encode::encode_utf8($_), $self->print_text(\%args);
 
     } else {
 
@@ -2205,7 +2297,7 @@ sub generate_email {
         'Filename'   => 'logo.png',
         'Content-ID' => "<$content_id>",
       ;
-   
+
       if ( ref($self) eq 'FS::cust_bill' && $conf->exists('invoice-barcode') ) {
         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
         push @related_parts, build MIME::Entity
@@ -2246,11 +2338,11 @@ sub generate_email {
       'Data'        => [ '<html>',
                          '  <head>',
                          '    <title>',
-                         '      '. encode_entities($return{'subject'}), 
+                         '      '. encode_entities($return{'subject'}),
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
-                         $html,
+                         Encode::encode_utf8($html),
                          '  </body>',
                          '</html>',
                        ],
@@ -2301,7 +2393,7 @@ sub generate_email {
       ;
 
     } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
+
       push @otherparts, build MIME::Entity
         'Type'        => 'text/csv',
         'Encoding'    => '7bit',
@@ -2356,7 +2448,7 @@ sub generate_email {
 =item mimebuild_pdf
 
 Returns a list suitable for passing to MIME::Entity->build(), representing
-this invoice as PDF attachment.
+this quotation or invoice as PDF attachment.
 
 =cut
 
@@ -2367,7 +2459,7 @@ sub mimebuild_pdf {
     'Encoding'    => 'base64',
     'Data'        => [ $self->print_pdf(@_) ],
     'Disposition' => 'attachment',
-    'Filename'    => 'invoice-'. $self->invnum. '.pdf',
+    'Filename'    => $self->pdf_filename,
   );
 }
 
@@ -2421,9 +2513,10 @@ sub postal_mail_fsinc {
   my $pages = CAM::PDF->new($file)->numPages;
 
   my $ua = LWP::UserAgent->new(
-    'ssl_opts' => { 
+    'ssl_opts' => {
       verify_hostname => 0,
       SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
+      SSL_version     => 'SSLv3',
     }
   );
   my $response = $ua->request( POST $url, [
@@ -2454,7 +2547,8 @@ sub postal_mail_fsinc {
     'country'          => $bill_location->country,
   ]);
 
-  die "Print connection error: ". $response->message. "\n"
+  die "Print connection error: ". $response->message.
+      ' ('. $response->as_string. ")\n"
     unless $response->is_success;
 
   local $@;
@@ -2476,13 +2570,13 @@ sub postal_mail_fsinc {
 Generate section information for all items appearing on this invoice.
 This will only be called for multi-section invoices.
 
-For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
-related display records (L<FS::cust_bill_pkg_display>) and organize 
-them into two groups ("early" and "late" according to whether they come 
-before or after the total), then into sections.  A subtotal is calculated 
+For each line item (L<FS::cust_bill_pkg> record), this will fetch all
+related display records (L<FS::cust_bill_pkg_display>) and organize
+them into two groups ("early" and "late" according to whether they come
+before or after the total), then into sections.  A subtotal is calculated
 for each section.
 
-Section descriptions are returned in sort weight order.  Each consists 
+Section descriptions are returned in sort weight order.  Each consists
 of a hash containing:
 
 description: the package category name, escaped
@@ -2491,7 +2585,7 @@ tax_section: a flag indicating that the section contains only tax charges
 summarized: same as tax_section, for some reason
 sort_weight: the package category's sort weight
 
-If 'condense' is set on the display record, it also contains everything 
+If 'condense' is set on the display record, it also contains everything
 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
 coderefs to generate parts of the invoice.  This is not advised.
 
@@ -2500,32 +2594,32 @@ sections.
 
 OPTIONS may include:
 
-by_location: a flag to divide the invoice into sections by location.  
-Each section hash will have a 'location' element containing a hashref of 
+by_location: a flag to divide the invoice into sections by location.
+Each section hash will have a 'location' element containing a hashref of
 the location fields (see L<FS::cust_location>).  The section description
-will be the location label, but the template can use any of the location 
+will be the location label, but the template can use any of the location
 fields to create a suitable label.
 
-by_category: a flag to divide the invoice into sections using display 
-records (see L<FS::cust_bill_pkg_display>).  This is the "traditional" 
+by_category: a flag to divide the invoice into sections using display
+records (see L<FS::cust_bill_pkg_display>).  This is the "traditional"
 behavior.  Each section hash will have a 'category' element containing
-the section name from the display record (which probably equals the 
+the section name from the display record (which probably equals the
 category name of the package, but may not in some cases).
 
 summary: a flag indicating that this is a summary-format invoice.
 Turning this on has the following effects:
 - Ignores display items with the 'summary' flag.
 - Places all sections in the "early" group even if they have post_total.
-- Creates sections for all non-disabled package categories, even if they 
+- Creates sections for all non-disabled package categories, even if they
 have no charges on this invoice, as well as a section with no name.
 
 escape: an escape function to use for section titles.
 
-extra_sections: an arrayref of additional sections to return after the 
-sorted list.  If there are any of these, section subtotals exclude 
+extra_sections: an arrayref of additional sections to return after the
+sorted list.  If there are any of these, section subtotals exclude
 usage charges.
 
-format: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
+format: 'latex', 'html', or 'template' (i.e. text).  Not used, but
 passed through to C<_condense_section()>.
 
 =cut
@@ -2534,7 +2628,7 @@ use vars qw(%pkg_category_cache);
 sub _items_sections {
   my $self = shift;
   my %opt = @_;
-  
+
   my $escape = $opt{escape};
   my @extra_sections = @{ $opt{extra_sections} || [] };
 
@@ -2547,12 +2641,12 @@ sub _items_sections {
   my %not_tax = ();
 
   # About tax items + multisection invoices:
-  # If either invoice_*summary option is enabled, AND there is a 
-  # package category with the name of the tax, then there will be 
+  # If either invoice_*summary option is enabled, AND there is a
+  # package category with the name of the tax, then there will be
   # a display record assigning the tax item to that category.
   #
   # However, the taxes are always placed in the "Taxes, Surcharges,
-  # and Fees" section regardless of that.  The only effect of the 
+  # and Fees" section regardless of that.  The only effect of the
   # display record is to create a subtotal for the summary page.
 
   # cache these
@@ -2591,11 +2685,11 @@ sub _items_sections {
           if $cust_bill_pkg->pkgnum  or $cust_bill_pkg->feepart;
 
         # there's actually a very important piece of logic buried in here:
-        # incrementing $late_subtotal{$section} CREATES 
-        # $late_subtotal{$section}.  keys(%late_subtotal) is later used 
+        # incrementing $late_subtotal{$section} CREATES
+        # $late_subtotal{$section}.  keys(%late_subtotal) is later used
         # to define the list of late sections, and likewise keys(%subtotal).
-        # When _items_cust_bill_pkg is called to generate line items for 
-        # real, it will be called with 'section' => $section for each 
+        # When _items_cust_bill_pkg is called to generate line items for
+        # real, it will be called with 'section' => $section for each
         # of these.
         if ( $display->post_total && !$opt{summary} ) {
           if (! $type || $type eq 'S') {
@@ -2615,7 +2709,7 @@ sub _items_sections {
               if $cust_bill_pkg->recur != 0
               || $cust_bill_pkg->recur_show_zero;
           }
-          
+
           if ($type && $type eq 'U') {
             $late_subtotal{$locationnum}{$section} += $usage
               unless scalar(@extra_sections);
@@ -2677,10 +2771,10 @@ sub _items_sections {
           $section->{'locationnum'} = $locationnum;
           my $location = FS::cust_location->by_key($locationnum);
           $section->{'description'} = &{ $escape }($location->location_label);
-          # Better ideas? This will roughly group them by proximity, 
+          # Better ideas? This will roughly group them by proximity,
           # which alpha sorting on any of the address fields won't.
           # Sorting by locationnum is meaningless.
-          # We have to sort on _something_ or the order may change 
+          # We have to sort on _something_ or the order may change
           # randomly from one invoice to the next, which will confuse
           # people.
           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
@@ -2709,10 +2803,10 @@ sub _items_sections {
       } # foreach $sectionname
     } #foreach $locationnum
     push @these, @extra_sections if $post_total == 0;
-    # need an alpha sort for location sections, because postal codes can 
+    # need an alpha sort for location sections, because postal codes can
     # be non-numeric
     $sections[ $post_total ] = [ sort {
-      $opt{'by_location'} ? 
+      $opt{'by_location'} ?
         ($a->{sort_weight} cmp $b->{sort_weight}) :
         ($a->{sort_weight} <=> $b->{sort_weight})
       } @these ];
@@ -2957,8 +3051,8 @@ sub _condensed_total_line_generator {
 
 =item _items_pkg [ OPTIONS ]
 
-Return line item hashes for each package item on this invoice. Nearly 
-equivalent to 
+Return line item hashes for each package item on this invoice. Nearly
+equivalent to
 
 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
 
@@ -3026,7 +3120,7 @@ sub _items_fee {
       }
     } # otherwise include them all in the main section
     # XXX what to do when sectioning by location?
-    
+
     my @ext_desc;
     my %base_invnums; # invnum => invoice date
     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
@@ -3048,11 +3142,15 @@ sub _items_fee {
     my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
     # but not escape the base description line
 
+    my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+      if $options{section_with_taxes};
+
     push @items,
       { feepart     => $cust_bill_pkg->feepart,
         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
         description => $desc,
-        ext_description => \@ext_desc
+        pkg_tax     => \@pkg_tax,
+        ext_description => \@ext_desc,
         # sdate/edate?
       };
   }
@@ -3114,7 +3212,7 @@ sub _taxsort {
 
 sub _items_tax {
   my $self = shift;
-  my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart } 
+  my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
     $self->cust_bill_pkg;
   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
 
@@ -3143,17 +3241,19 @@ escape_function: the function used to escape strings.
 DEPRECATED? (expensive, mostly unused?)
 format_function: the function used to format CDRs.
 
-section: a hashref containing 'category' and/or 'locationnum'; if this 
+section: a hashref containing 'category' and/or 'locationnum'; if this
 is present, only returns line items that belong to that category and/or
 location (whichever is defined).
 
 multisection: a flag indicating that this is a multisection invoice,
 which does something complicated.
 
+section_with_taxes:  Look up and include applied taxes for each record
+
 Returns a list of hashrefs, each of which may contain:
 
-pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
-ext_description, which is an arrayref of detail lines to show below 
+pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
+ext_description, which is an arrayref of detail lines to show below
 the package line.
 
 =cut
@@ -3186,7 +3286,9 @@ sub _items_cust_bill_pkg {
 
   # for location labels: use default location on the invoice date
   my $default_locationnum;
-  if ( $self->custnum ) {
+  if ( $conf->exists('invoice-all_pkg_addresses') ) {
+    $default_locationnum = 0; # treat them all as non-default
+  } elsif ( $self->custnum ) {
     my $h_cust_main;
     my @h_search = FS::h_cust_main->sql_h_search($self->_date);
     $h_cust_main = qsearchs({
@@ -3237,13 +3339,13 @@ sub _items_cust_bill_pkg {
       # this is a location section; skip packages that aren't at this
       # service location.
       next if $cust_bill_pkg->pkgnum == 0; # skips fees...
-      next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum 
+      next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
               != $locationnum;
     }
 
     # Consider display records for this item to determine if it belongs
     # in this section.  Note that if there are no display records, there
-    # will be a default pseudo-record that includes all charge types 
+    # will be a default pseudo-record that includes all charge types
     # and has no section name.
     my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
                                   ? $cust_bill_pkg->cust_bill_pkg_display
@@ -3260,7 +3362,7 @@ sub _items_cust_bill_pkg {
                                 @cust_bill_pkg_display;
     } else {
       # otherwise, process all display records that aren't usage summaries
-      # (I don't think there should be usage summaries if you aren't using 
+      # (I don't think there should be usage summaries if you aren't using
       # category sections, but this is the historical behavior)
       @cust_bill_pkg_display = grep { !$_->summary }
                                 @cust_bill_pkg_display;
@@ -3291,14 +3393,14 @@ sub _items_cust_bill_pkg {
 
         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
           if $DEBUG > 1;
+
         my $cust_pkg = $cust_bill_pkg->cust_pkg;
         my $part_pkg = $cust_pkg->part_pkg;
 
         # which pkgpart to show for display purposes?
         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
 
-        # start/end dates for invoice formats that do nonstandard 
+        # start/end dates for invoice formats that do nonstandard
         # things with them
         my %item_dates = ();
         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
@@ -3307,6 +3409,9 @@ sub _items_cust_bill_pkg {
         # not normally used, but pass this to the template anyway
         $classname = $part_pkg->classname;
 
+        my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+          if $opt{section_with_taxes};
+
         if (    (!$type || $type eq 'S')
              && (    $cust_bill_pkg->setup != 0
                   || $cust_bill_pkg->setup_show_zero
@@ -3319,7 +3424,8 @@ sub _items_cust_bill_pkg {
             if $DEBUG > 1;
 
           # append the word 'Setup' to the setup line if there's going to be
-          # a recur line for the same package (i.e. not a one-time charge) 
+          # a recur line for the same package (i.e. not a one-time charge)
+          # XXX localization
           my $description = $desc;
           $description .= ' Setup'
             if $cust_bill_pkg->recur != 0
@@ -3335,16 +3441,24 @@ sub _items_cust_bill_pkg {
             && ! $cust_bill_pkg->recur_show_zero;
 
           my @d = ();
-          my $svc_label;
+          my @svc_labels = ();
+          my $svc_label = '';
 
-          # always pass the svc_label through to the template, even if 
-          # not displaying it as an ext_description
-          my @svc_labels = map &{$escape_function}($_),
-                      $cust_pkg->h_labels_short($self->_date, undef, 'I');
+          unless ( $part_pkg->hide_svc_detail ) {
 
-          $svc_label = $svc_labels[0];
+            # still pass the svc_label through to the template, even if
+            # not displaying it as an ext_description
+            @svc_labels = map &{$escape_function}($_),
+              $cust_pkg->h_labels_short($self->_date,
+                                        undef,
+                                        'I',
+                                        $self->conf->{locale},
+                                       );
+            $svc_label = $svc_labels[0];
 
-          unless ( $cust_pkg->part_pkg->hide_svc_detail
+          }
+
+          unless ( $part_pkg->hide_svc_detail
                 || $cust_bill_pkg->hidden )
           {
 
@@ -3371,6 +3485,7 @@ sub _items_cust_bill_pkg {
             push @{ $s->{ext_description} }, @d;
           } else {
             $s = {
+              billpkgnum      => $cust_bill_pkg->billpkgnum,
               _is_setup       => 1,
               description     => $description,
               pkgpart         => $pkgpart,
@@ -3382,6 +3497,7 @@ sub _items_cust_bill_pkg {
               ext_description => \@d,
               svc_label       => ($svc_label || ''),
               locationnum     => $cust_pkg->locationnum, # sure, why not?
+              pkg_tax         => \@pkg_tax,
             };
           };
 
@@ -3421,6 +3537,7 @@ sub _items_cust_bill_pkg {
 
           my @d = ();
           my @seconds = (); # for display of usage info
+          my @svc_labels = ();
           my $svc_label = '';
 
           #at least until cust_bill_pkg has "past" ranges in addition to
@@ -3430,9 +3547,13 @@ sub _items_cust_bill_pkg {
           push @dates, $prev->sdate if $prev;
           push @dates, undef if !$prev;
 
-          my @svc_labels = map &{$escape_function}($_),
-                      $cust_pkg->h_labels_short(@dates, 'I');
-          $svc_label = $svc_labels[0];
+          unless ( $part_pkg->hide_svc_detail ) {
+            @svc_labels = map &{$escape_function}($_),
+              $cust_pkg->h_labels_short(@dates,
+                                        'I',
+                                        $self->conf->{locale});
+            $svc_label = $svc_labels[0];
+          }
 
           # show service labels, unless...
                     # the package is set not to display them
@@ -3444,7 +3565,7 @@ sub _items_cust_bill_pkg {
                     # or this is a usage summary line
                 || $is_summary && $type && $type eq 'U'
                     # or this is a usage line and there's a recurring line
-                    # for the package in the same section (which will 
+                    # for the package in the same section (which will
                     # have service labels already)
                 || ($type eq 'U' and defined($r))
               )
@@ -3470,17 +3591,17 @@ sub _items_cust_bill_pkg {
 
             # Display of seconds_since_sqlradacct:
             # On the invoice, when processing @detail_items, look for a field
-            # named 'seconds'.  This will contain total seconds for each 
-            # service, in the same order as @ext_description.  For services 
+            # named 'seconds'.  This will contain total seconds for each
+            # service, in the same order as @ext_description.  For services
             # that don't support this it will show undef.
-            if ( $conf->exists('svc_acct-usage_seconds') 
+            if ( $conf->exists('svc_acct-usage_seconds')
                  and ! $cust_bill_pkg->pkgpart_override ) {
-              foreach my $cust_svc ( 
-                  $cust_pkg->h_cust_svc(@dates, 'I') 
+              foreach my $cust_svc (
+                  $cust_pkg->h_cust_svc(@dates, 'I')
                 ) {
 
-                # eval because not having any part_export_usage exports 
-                # is a fatal error, last_bill/_date because that's how 
+                # eval because not having any part_export_usage exports
+                # is a fatal error, last_bill/_date because that's how
                 # sqlradius_hour billing does it
                 my $sec = eval {
                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
@@ -3505,7 +3626,7 @@ sub _items_cust_bill_pkg {
 
           warn "$me _items_cust_bill_pkg calculating amount\n"
             if $DEBUG > 1;
-  
+
           my $amount = 0;
           if (!$type) {
             $amount = $cust_bill_pkg->recur;
@@ -3514,7 +3635,7 @@ sub _items_cust_bill_pkg {
           } elsif ($type eq 'U') {
             $amount = $cust_bill_pkg->usage;
           }
-  
+
           if ( !$type || $type eq 'R' ) {
 
             warn "$me _items_cust_bill_pkg adding recur\n"
@@ -3530,6 +3651,7 @@ sub _items_cust_bill_pkg {
               push @{ $r->{ext_description} }, @d;
             } else {
               $r = {
+                billpkgnum      => $cust_bill_pkg->billpkgnum,
                 description     => $description,
                 pkgpart         => $pkgpart,
                 pkgnum          => $cust_bill_pkg->pkgnum,
@@ -3541,6 +3663,7 @@ sub _items_cust_bill_pkg {
                 ext_description => \@d,
                 svc_label       => ($svc_label || ''),
                 locationnum     => $cust_pkg->locationnum,
+                pkg_tax         => \@pkg_tax,
               };
               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
             }
@@ -3559,6 +3682,7 @@ sub _items_cust_bill_pkg {
             } elsif ( $amount ) {
               # create a new usage line
               $u = {
+                billpkgnum      => $cust_bill_pkg->billpkgnum,
                 description     => $description,
                 pkgpart         => $pkgpart,
                 pkgnum          => $cust_bill_pkg->pkgnum,
@@ -3568,6 +3692,7 @@ sub _items_cust_bill_pkg {
                 %item_dates,
                 ext_description => \@d,
                 locationnum     => $cust_pkg->locationnum,
+                pkg_tax         => \@pkg_tax,
               };
             } # else this has no usage, so don't create a usage section
           }
@@ -3582,7 +3707,7 @@ sub _items_cust_bill_pkg {
         # items of this kind should normally not have sdate/edate.
         push @b, {
           'description' => $desc,
-          'amount'      => sprintf('%.2f', $cust_bill_pkg->setup 
+          'amount'      => sprintf('%.2f', $cust_bill_pkg->setup
                                            + $cust_bill_pkg->recur)
         };
 
@@ -3595,7 +3720,7 @@ sub _items_cust_bill_pkg {
           # case 2: we are showing a setup line for a package that has
           # no base recurring fee
           or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
-          # case 3: we are showing a recur line for a package that has 
+          # case 3: we are showing a recur line for a package that has
           # a base recurring fee
           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
       ) {
@@ -3612,7 +3737,7 @@ sub _items_cust_bill_pkg {
             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
           }
 
-          # update the active line (before the discount) to show the 
+          # update the active line (before the discount) to show the
           # original price (whether this is a hidden line or not)
 
           $s->{amount} -= $item_discount->{setup_amount} if $s;
@@ -3657,9 +3782,9 @@ sub _items_cust_bill_pkg {
 =item _items_discounts_avail
 
 Returns an array of line item hashrefs representing available term discounts
-for this invoice.  This makes the same assumptions that apply to term 
-discounts in general: that the package is billed monthly, at a flat rate, 
-with no usage charges.  A prorated first month will be handled, as will 
+for this invoice.  This makes the same assumptions that apply to term
+discounts in general: that the package is billed monthly, at a flat rate,
+with no usage charges.  A prorated first month will be handled, as will
 a setup fee if the discount is allowed to apply to setup fees.
 
 =cut
@@ -3667,7 +3792,7 @@ a setup fee if the discount is allowed to apply to setup fees.
 sub _items_discounts_avail {
   my $self = shift;
 
-  #maybe move this method from cust_bill when quotations support discount_plans 
+  #maybe move this method from cust_bill when quotations support discount_plans
   return () unless $self->can('discount_plans');
   my %plans = $self->discount_plans;
 
@@ -3679,7 +3804,7 @@ sub _items_discounts_avail {
     my $plan = $plans{$months};
 
     my $term_total = sprintf('%.2f', $plan->discounted_total);
-    my $percent = sprintf('%.0f', 
+    my $percent = sprintf('%.0f',
                           100 * (1 - $term_total / $plan->base_total) );
     my $permonth = sprintf('%.2f', $term_total / $months);
     my $detail = $self->mt('discount on item'). ' '.
@@ -3692,7 +3817,7 @@ sub _items_discounts_avail {
     +{
       description => $self->mt('Save [_1]% by paying for [_2] months',
                                 $percent, $months),
-      amount      => $self->mt('[_1] ([_2] per month)', 
+      amount      => $self->mt('[_1] ([_2] per month)',
                                 $term_total, $money_char.$permonth),
       ext_description => ($detail || ''),
     }
@@ -3701,4 +3826,68 @@ sub _items_discounts_avail {
 
 }
 
+=item has_sections AGENTNUM
+
+Return true if invoice_sections should be enabled for this bill.
+ (Inherited by both cust_bill and cust_bill_void)
+
+Determination:
+* False if not an invoice
+* True always if conf invoice_sections is enabled
+* True always if sections_by_location is enabled
+* True if conf invoice_sections_multilocation > 1,
+  and location_count >= invoice_sections_multilocation
+* Else, False
+
+=cut
+
+sub has_sections {
+  my ($self, $agentnum) = @_;
+
+  return 0 unless $self->invnum > 0;
+
+  $agentnum ||= $self->agentnum;
+  return 1 if $self->conf->config_bool('invoice_sections', $agentnum);
+  return 1 if $self->conf->exists('sections_by_location', $agentnum);
+
+  my $location_min = $self->conf->config(
+    'invoice_sections_multilocation', $agentnum,
+  );
+
+  return 1
+    if $location_min
+    && $self->location_count >= $location_min;
+
+  0;
+}
+
+
+=item location_count
+
+Return the number of locations billed on this invoice
+
+=cut
+
+sub location_count {
+  my ($self) = @_;
+  return 0 unless $self->invnum;
+
+  # SELECT COUNT( DISTINCT cust_pkg.locationnum )
+  # FROM cust_bill_pkg
+  # LEFT JOIN cust_pkg USING (pkgnum)
+  # WHERE invnum = 278
+  #   AND cust_bill_pkg.pkgnum > 0
+
+  my $result = qsearchs({
+    select    => 'COUNT(DISTINCT cust_pkg.locationnum) as location_count',
+    table     => 'cust_bill_pkg',
+    addl_from => 'LEFT JOIN cust_pkg USING (pkgnum)',
+    extra_sql => 'WHERE invnum = '.dbh->quote( $self->invnum )
+               . '  AND cust_bill_pkg.pkgnum > 0'
+  });
+  ref $result ? $result->location_count : 0;
+}
+
+
+
 1;