Fixed invoice inconsistencies with various conf flags RT#78190
authorMitch Jackson <mitch@freeside.biz>
Wed, 15 Nov 2017 07:51:40 +0000 (07:51 +0000)
committerMitch Jackson <mitch@freeside.biz>
Thu, 16 Nov 2017 05:30:09 +0000 (05:30 +0000)
Applying different invoicing conf flags manifested different
variations of the same problem.  Addressed by this fix:

 - Incorrect items listed for Previous Balance
 - Incorrect Items listed for applied payments and credits
 - Incorrect subtotals for various sections
 - Invoice amounts, subtotals, balances displayed did not reconcile.
   Because of which data was selected for display, columns could appear
   to have bad math.  No account balances were factually incorrect.
 - Items disappearing from invoices used a payment receipts or
   "statements" giving a false impression of overpayment or credits
 - Applied payments or credits appearing on the wrong statements
 - A single applied credit appearing on up to 3 invoices
 - When viewing older invoices, future payments for future bills
   shown on, and appearing to apply to, the older invoice
 - Inconsistencies of line items and numbers between website, email,
   pdf and txt version invoices.
 - Invoice summary page numbers not matching the invoice
 - Incorrect balances shown on on aging line
 - Update item order on invoice_htmlsummary mason template

Conf flags involved in these issues:

 - disable_previous_balance
 - previous_balance-payments_since
 - previous_balance-summary_only
 - previous_balance-show_on_statements
 - previous_balance-section
 - previous_balance-exclude_from_total
 - invoice_include_aging
 - invoice_show_prior_due_date
 - invoice_usesummary

New invoice template stash variables made available:

 - aged_balance_current
 - aged_balance_30d
 - aged_balance_60d
 - aged_balance_90d

Solved by updating, or creating, FS::cust_bill helper methods that
generate data to be displayed on invoices.  These helper methods
are responsive to various conf flags.  Updated template pipeline to
use these helpers instead of inconsistent sql queries.

Resolves: #78190
See Also: #75709, #76161, #74426

FS/FS/Template_Mixin.pm
FS/FS/cust_bill.pm
conf/invoice_htmlsummary

index 7dc8139..7d92d21 100644 (file)
@@ -29,9 +29,9 @@ use FS::L10N;
 
 $DEBUG = 0;
 $me = '[FS::Template_Mixin]';
 
 $DEBUG = 0;
 $me = '[FS::Template_Mixin]';
-FS::UID->install_callback( sub { 
+FS::UID->install_callback( sub {
   my $conf = new FS::Conf; #global
   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
 } );
 
   $date_format = $conf->config('date_format') || '%x'; #/YY
 } );
 
@@ -121,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.
 
 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.
 
 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
 customize invoice templates for different purposes.
 
@@ -175,7 +175,7 @@ sub print_latex {
   close $lh;
   $params{'logo_file'} = $lh->filename;
 
   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);
         && $self->can('invoice_barcode')
         && $self->invnum ) { # don't try to barcode statements
       my $png_file = $self->invoice_barcode($dir);
@@ -187,7 +187,7 @@ sub print_latex {
       $eps_file = $1;
 
       my $curr_dir = cwd();
       $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)
       # 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)
@@ -200,7 +200,7 @@ sub print_latex {
   }
 
   my @filled_in = $self->print_generic( %params );
   }
 
   my @filled_in = $self->print_generic( %params );
-  
+
   my $fh = new File::Temp( TEMPLATE => $tmp_template,
                            DIR      => $dir,
                            SUFFIX   => '.tex',
   my $fh = new File::Temp( TEMPLATE => $tmp_template,
                            DIR      => $dir,
                            SUFFIX   => '.tex',
@@ -299,7 +299,7 @@ before that line item (quotations only)
 
 =item template
 
 
 =item template
 
-Dprecated.  Used as a suffix for a configuration template.  Please 
+Dprecated.  Used as a suffix for a configuration template.  Please
 don't use this, it deprecated in favor of more flexible alternatives.
 
 =back
 don't use this, it deprecated in favor of more flexible alternatives.
 
 =back
@@ -344,6 +344,8 @@ sub print_generic {
   $templatefile .= "_$template"
     if length($template) && $conf->exists($templatefile."_$template");
 
   $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";
   # the base template
   my @invoice_template = map "$_\n", $conf->config($templatefile)
     or die "cannot load config data $templatefile";
@@ -355,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);
          "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;
 
   warn "$me print_generic creating T:T object\n"
     if $DEBUG > 1;
@@ -374,7 +376,7 @@ sub print_generic {
 
 
   # additional substitution could possibly cause breakage in existing templates
 
 
   # additional substitution could possibly cause breakage in existing templates
-  my %convert_maps = ( 
+  my %convert_maps = (
     'latex' => {
                  'notes'         => sub { map "$_", @_ },
                  'footer'        => sub { map "$_", @_ },
     'latex' => {
                  'notes'         => sub { map "$_", @_ },
                  'footer'        => sub { map "$_", @_ },
@@ -386,7 +388,7 @@ sub print_generic {
     'html'  => {
                  'notes' =>
                    sub {
     '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;
                        s/%%(.*)$/<!-- $1 -->/g;
                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
                        s/\\begin\{enumerate\}/<ol>/g;
@@ -406,7 +408,7 @@ sub print_generic {
                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
                  'returnaddress' =>
                    sub {
                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
                  'returnaddress' =>
                    sub {
-                     map { 
+                     map {
                        s/~/&nbsp;/g;
                        s/\\\\\*?\s*$/<BR>/;
                        s/\\hyphenation\{[\w\s\-]+}//;
                        s/~/&nbsp;/g;
                        s/\\\\\*?\s*$/<BR>/;
                        s/\\hyphenation\{[\w\s\-]+}//;
@@ -420,7 +422,7 @@ sub print_generic {
     'template' => {
                  'notes' =>
                    sub {
     'template' => {
                  'notes' =>
                    sub {
-                     map { 
+                     map {
                        s/%%.*$//g;
                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
                        s/\\begin\{enumerate\}//g;
                        s/%%.*$//g;
                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
                        s/\\begin\{enumerate\}//g;
@@ -438,7 +440,7 @@ sub print_generic {
                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
                  'returnaddress' =>
                    sub {
                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
                  'returnaddress' =>
                    sub {
-                     map { 
+                     map {
                        s/~/ /g;
                        s/\\\\\*?\s*$/\n/;             # dubious
                        s/\\hyphenation\{[\w\s\-]+}//;
                        s/~/ /g;
                        s/\\\\\*?\s*$/\n/;             # dubious
                        s/\\hyphenation\{[\w\s\-]+}//;
@@ -548,7 +550,7 @@ sub print_generic {
     'quotationnum'    => $self->quotationnum,
     'no_date'         => $params{'no_date'},
     '_date'           => ( $params{'no_date'} ? '' : $self->_date ),
     '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'}
                              ? ''
       # templates; see RT#28271
     'date'            => ( $params{'no_date'}
                              ? ''
@@ -581,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'),
     '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)),
     #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)),
@@ -606,7 +608,7 @@ sub print_generic {
 
   #quotations have $name
   $invoice_data{'name'} = $invoice_data{'payname'};
 
   #quotations have $name
   $invoice_data{'name'} = $invoice_data{'payname'};
+
   #localization
   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
   # prototype here to silence warnings
   #localization
   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
   # prototype here to silence warnings
@@ -624,7 +626,7 @@ sub print_generic {
 
   $invoice_data{'bill_period'} = '';
   $invoice_data{'bill_period'} =
 
   $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);
       . " to " .
       $self->time2str_local('%e %h', $max_edate, $format)
     if ($max_edate != 0 && $min_sdate != 999999999999);
@@ -634,7 +636,7 @@ sub print_generic {
     my $pkg_class =
       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
     $invoice_data{finance_section} = $pkg_class->categoryname;
     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
 
   $invoice_data{finance_amount} = '0.00';
   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
 
@@ -651,7 +653,7 @@ 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{'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'};
 
   $invoice_data{'cid'} = $params{'cid'}
     if $params{'cid'};
 
@@ -691,18 +693,34 @@ sub print_generic {
   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
     if $params{'barcode_cid'};
 
   $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
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
   # flag telling this invoice to have a first-page summary
@@ -712,127 +730,100 @@ sub print_generic {
     # XXX should be an FS::cust_bill method to set the defaults, instead
     # of checking the type here
 
     # 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'} = {};
     # 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",
-        );
-
-      # the customer's current balance immediately after generating the last 
-      # bill
-
-      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;
-      }
+    #    my $last_bill = $self->previous_bill;
+    # if ( $last_bill ) {
 
 
-      $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);
+    # 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);
 
 
-      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{'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 {
     } 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{'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'}      = [];
+    }
+
+    # 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->exists('invoice_usesummary', $agentnum) ) {
       $invoice_data{'summarypage'} = $summarypage = 1;
     }
     if ( $conf->exists('invoice_usesummary', $agentnum) ) {
       $invoice_data{'summarypage'} = $summarypage = 1;
     }
@@ -900,9 +891,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
 # 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 : '';
 #    ? $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;
 #    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
 #      ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
 #      : $nbsp;
@@ -950,30 +941,40 @@ sub print_generic {
 
   # default section ('Charges')
   my $default_section = { 'description' => '',
 
   # 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;
                           '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') ) {
   # 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 =  { '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
 
   } else {
     # otherwise put them in the main section
@@ -1006,7 +1007,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
     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' ) {
     # now, treat them as mutually exclusive.
     my %section_method = ( by_category => 1 );
     if ( $conf->config($tc.'sections_method') eq 'location' ) {
@@ -1052,7 +1053,7 @@ sub print_generic {
     my @finance_charges;
     my @charges;
     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
     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...
         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...
@@ -1061,7 +1062,7 @@ sub print_generic {
         push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
       }
     }
         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);
       sprintf('%.2f', sum( @finance_charges ) || 0);
     $default_section->{subtotal} = $other_money_char.
                                     sprintf('%.2f', sum( @charges ) || 0);
@@ -1115,7 +1116,7 @@ sub print_generic {
         #quantity        => 1, # not really correct
         section         => $previous_section, # which might be $default_section
         description     => &$escape_function($line_item->{'description'}),
         #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'},
                              @{ $line_item->{'ext_description'} || [] }
                            ],
         amount          => $money_char . $line_item->{'amount'},
@@ -1130,20 +1131,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, ['',''];
   }
     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)';
   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 ],
        };
        { 'description' => $didsummary_desc,
            'ext_description' => [ $didsummary, $minutes ],
        };
@@ -1229,20 +1230,20 @@ sub print_generic {
         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
       }
       $line_item->{'ext_description'} ||= [];
         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
       }
       $line_item->{'ext_description'} ||= [];
+
       push @detail_items, $line_item;
     }
 
     if ( $section->{'description'} ) {
       push @buf, ( ['','-----------'],
                    [ $section->{'description'}. ' sub-total',
       push @detail_items, $line_item;
     }
 
     if ( $section->{'description'} ) {
       push @buf, ( ['','-----------'],
                    [ $section->{'description'}. ' sub-total',
-                      $section->{'subtotal'} # already formatted this 
+                      $section->{'subtotal'} # already formatted this
                    ],
                    [ '', '' ],
                    [ '', '' ],
                  );
     }
                    ],
                    [ '', '' ],
                    [ '', '' ],
                  );
     }
-  
+
   }
 
   $invoice_data{current_less_finance} =
   }
 
   $invoice_data{current_less_finance} =
@@ -1312,7 +1313,7 @@ sub print_generic {
               ];
 
   }
               ];
 
   }
+
   if ( @items_tax ) {
     my $total = {};
     $total->{'total_item'} = $self->mt('Sub-total');
   if ( @items_tax ) {
     my $total = {};
     $total->{'total_item'} = $self->mt('Sub-total');
@@ -1368,7 +1369,7 @@ sub print_generic {
     }
 
   }
     }
 
   }
-  
+
   if ( $self->can('_items_total') ) { # should always be true now
 
     # even for multisection, need plain text version
   if ( $self->can('_items_total') ) { # should always be true now
 
     # even for multisection, need plain text version
@@ -1399,12 +1400,12 @@ sub print_generic {
     push @buf, [ '', '' ];
 
     # if we're showing previous invoices, also show previous
     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') )
       {
           and $self->can('_items_credits')
           and $self->can('_items_payments') )
       {
-    
+
       # credits
       my $credittotal = 0;
       foreach my $credit (
       # credits
       my $credittotal = 0;
       foreach my $credit (
@@ -1466,7 +1467,7 @@ sub print_generic {
                    ];
       }
       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
                    ];
       }
       $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-    
+
       if ( $multisection ) {
         $adjust_section->{'subtotal'} = $other_money_char.
                                         sprintf('%.2f', $credittotal + $paymenttotal);
       if ( $multisection ) {
         $adjust_section->{'subtotal'} = $other_money_char.
                                         sprintf('%.2f', $credittotal + $paymenttotal);
@@ -1475,21 +1476,21 @@ sub print_generic {
         #in @extra_sections instead of @sections. obviously.
         push @sections, $adjust_section
           unless $adjust_section->{sort_weight};
         #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
         # different rules
       }
 
       # create Balance Due message
-      { 
+      {
         my $total;
         $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
         $total->{'total_amount'} =
           &$embolden_function(
         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->charged +
                                                #    $self->billing_balance
                                                #  :
-                                                   $self->owed + $pr_total
+                                                   $balance_due
                                       )
           );
         if ( $multisection && !$adjust_section->{sort_weight} ) {
                                       )
           );
         if ( $multisection && !$adjust_section->{sort_weight} ) {
@@ -1499,7 +1500,7 @@ sub print_generic {
           push @total_items, $total;
         }
         push @buf,['','-----------'];
           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 ) ];
       }
 
           sprintf("%10.2f", $balance_due ) ];
       }
 
@@ -1519,7 +1520,7 @@ sub print_generic {
           push @total_items, $credit_total;
         }
         push @buf,['','-----------'];
           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 ) ];
       }
     }
           sprintf("%10.2f", -$cust_main->balance ) ];
       }
     }
@@ -1535,7 +1536,7 @@ sub print_generic {
       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
       $total->{'total_amount'} =
         &$embolden_function(
       $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'}. ' '.
         );
       my $last_section = pop @sections;
       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
@@ -1547,7 +1548,7 @@ sub print_generic {
   }
 
   # make a discounts-available section, even without multisection
   }
 
   # 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'),
        and my @discounts_avail = $self->_items_discounts_avail ) {
     my $discount_section = {
       'description' => $self->mt('Discounts Available'),
@@ -1579,7 +1580,7 @@ sub print_generic {
   }
 
   # invoice history "section" (not really a section)
   }
 
   # 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;
   # everything...
   if ( $conf->exists('previous_invoice_history') and $cust_main->isa('FS::cust_main') ) {
     my %history;
@@ -1610,7 +1611,7 @@ sub print_generic {
   }
   $invoice_data{location_info} = \%location_info;
 
   }
   $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'} );
 
   # the invoice variables
   return \%invoice_data if ( $params{'diag'} );
 
@@ -1635,7 +1636,7 @@ sub print_generic {
       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
                        s/--\@\]/$delimiters{$format}[1]/g;
                        $_;
       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
                        s/--\@\]/$delimiters{$format}[1]/g;
                        $_;
-                     } 
+                     }
                  &$convert_map( $conf->config($inc_file, $agentnum) );
 
     }
                  &$convert_map( $conf->config($inc_file, $agentnum) );
 
     }
@@ -1677,7 +1678,7 @@ sub print_generic {
   #setup subroutine for the template
   $invoice_data{invoice_lines} = sub {
     my $lines = shift || scalar(@buf);
   #setup subroutine for the template
   $invoice_data{invoice_lines} = sub {
     my $lines = shift || scalar(@buf);
-    map { 
+    map {
       scalar(@buf)
         ? shift @buf
         : [ '', '' ];
       scalar(@buf)
         ? shift @buf
         : [ '', '' ];
@@ -1722,22 +1723,6 @@ sub template_conf { warn "bare FS::Template_Mixin::template_conf";
   'invoice_';
 }
 
   '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.
 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an postscript invoice, as a scalar.
@@ -1816,23 +1801,23 @@ sub print_html {
   my $self = shift;
   my %params;
   if ( ref($_[0]) ) {
   my $self = shift;
   my %params;
   if ( ref($_[0]) ) {
-    %params = %{ shift() }; 
+    %params = %{ shift() };
   } else {
     %params = @_;
   }
   $params{'format'} = 'html';
   } 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
   $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
 #      # $ % & ~ _ ^ \ { }
 #
 # 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;
 
 sub _latex_escape {
   my $value = shift;
@@ -1857,18 +1842,18 @@ sub _html_escape_nbsp {
 
 sub _translate_old_latex_format {
   warn "_translate_old_latex_format called\n"
 
 sub _translate_old_latex_format {
   warn "_translate_old_latex_format called\n"
-    if $DEBUG; 
+    if $DEBUG;
 
   my @template = ();
   while ( @_ ) {
     my $line = shift;
 
   my @template = ();
   while ( @_ ) {
     my $line = shift;
-  
+
     if ( $line =~ /^%%Detail\s*$/ ) {
     if ( $line =~ /^%%Detail\s*$/ ) {
-  
+
       push @template, q![@--!,
                       q!  foreach my $_tr_line (@detail_items) {!,
                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
       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'}}!,
                       q!        "\\tabularnewline\n~~".!,
                       q!        join( "\\tabularnewline\n~~",!,
                       q!          @{$_tr_line->{'ext_description'}}!,
@@ -1904,9 +1889,9 @@ sub _translate_old_latex_format {
 
     } else {
       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
 
     } else {
       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
-      push @template, $line;  
+      push @template, $line;
     }
     }
-  
+
   }
 
   if ($DEBUG) {
   }
 
   if ($DEBUG) {
@@ -1926,7 +1911,7 @@ sub terms {
 
   #check for an invoice-specific override
   return $self->invoice_terms if $self->invoice_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;
   #check for a customer- specific override
   my $cust_main = $self->cust_main;
   return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
@@ -1980,7 +1965,7 @@ sub balance_due_msg {
   return $msg unless $self->terms; # huh?
   if ( !$self->conf->exists('invoice_show_prior_due_date')
        or $self->conf->exists('invoice_sections') ) {
   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 
+    # 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 ) {
     # _items_total) and not here
     # (yes, or if invoice_sections is enabled; this is just for compatibility)
     if ( $self->due_date ) {
@@ -2012,7 +1997,7 @@ sub balance_due_date {
   $duedate;
 }
 
   $duedate;
 }
 
-sub credit_balance_msg { 
+sub credit_balance_msg {
   my $self = shift;
   $self->mt('Credit Balance Remaining')
 }
   my $self = shift;
   $self->mt('Credit Balance Remaining')
 }
@@ -2180,7 +2165,7 @@ sub generate_email {
         if $DEBUG;
       @text = $conf->config($tc.'email_pdf_note');
       $html = join('<BR>', @text);
         if $DEBUG;
       @text = $conf->config($tc.'email_pdf_note');
       $html = join('<BR>', @text);
-  
+
     } # else use the plain text invoice
   }
 
     } # else use the plain text invoice
   }
 
@@ -2242,7 +2227,7 @@ sub generate_email {
         'Filename'   => 'logo.png',
         'Content-ID' => "<$content_id>",
       ;
         '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
       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
@@ -2283,7 +2268,7 @@ sub generate_email {
       'Data'        => [ '<html>',
                          '  <head>',
                          '    <title>',
       'Data'        => [ '<html>',
                          '  <head>',
                          '    <title>',
-                         '      '. encode_entities($return{'subject'}), 
+                         '      '. encode_entities($return{'subject'}),
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
@@ -2338,7 +2323,7 @@ sub generate_email {
       ;
 
     } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
       ;
 
     } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
+
       push @otherparts, build MIME::Entity
         'Type'        => 'text/csv',
         'Encoding'    => '7bit',
       push @otherparts, build MIME::Entity
         'Type'        => 'text/csv',
         'Encoding'    => '7bit',
@@ -2458,7 +2443,7 @@ sub postal_mail_fsinc {
   my $pages = CAM::PDF->new($file)->numPages;
 
   my $ua = LWP::UserAgent->new(
   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',
       verify_hostname => 0,
       SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
       SSL_version     => 'SSLv3',
@@ -2515,13 +2500,13 @@ sub postal_mail_fsinc {
 Generate section information for all items appearing on this invoice.
 This will only be called for multi-section invoices.
 
 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.
 
 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
 of a hash containing:
 
 description: the package category name, escaped
@@ -2530,7 +2515,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
 
 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.
 
 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
 coderefs to generate parts of the invoice.  This is not advised.
 
@@ -2539,32 +2524,32 @@ sections.
 
 OPTIONS may include:
 
 
 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
 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.
 
 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
 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.
 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.
 
 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.
 
 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
 passed through to C<_condense_section()>.
 
 =cut
@@ -2573,7 +2558,7 @@ use vars qw(%pkg_category_cache);
 sub _items_sections {
   my $self = shift;
   my %opt = @_;
 sub _items_sections {
   my $self = shift;
   my %opt = @_;
-  
+
   my $escape = $opt{escape};
   my @extra_sections = @{ $opt{extra_sections} || [] };
 
   my $escape = $opt{escape};
   my @extra_sections = @{ $opt{extra_sections} || [] };
 
@@ -2586,12 +2571,12 @@ sub _items_sections {
   my %not_tax = ();
 
   # About tax items + multisection invoices:
   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,
   # 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
   # display record is to create a subtotal for the summary page.
 
   # cache these
@@ -2630,11 +2615,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:
           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).
         # 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') {
         # of these.
         if ( $display->post_total && !$opt{summary} ) {
           if (! $type || $type eq 'S') {
@@ -2654,7 +2639,7 @@ sub _items_sections {
               if $cust_bill_pkg->recur != 0
               || $cust_bill_pkg->recur_show_zero;
           }
               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);
           if ($type && $type eq 'U') {
             $late_subtotal{$locationnum}{$section} += $usage
               unless scalar(@extra_sections);
@@ -2716,10 +2701,10 @@ sub _items_sections {
           $section->{'locationnum'} = $locationnum;
           my $location = FS::cust_location->by_key($locationnum);
           $section->{'description'} = &{ $escape }($location->location_label);
           $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.
           # 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) .
           # randomly from one invoice to the next, which will confuse
           # people.
           $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
@@ -2748,10 +2733,10 @@ sub _items_sections {
       } # foreach $sectionname
     } #foreach $locationnum
     push @these, @extra_sections if $post_total == 0;
       } # 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 {
     # 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 ];
         ($a->{sort_weight} cmp $b->{sort_weight}) :
         ($a->{sort_weight} <=> $b->{sort_weight})
       } @these ];
@@ -2996,8 +2981,8 @@ sub _condensed_total_line_generator {
 
 =item _items_pkg [ OPTIONS ]
 
 
 =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 ])
 
 
 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
 
@@ -3065,7 +3050,7 @@ sub _items_fee {
       }
     } # otherwise include them all in the main section
     # XXX what to do when sectioning by location?
       }
     } # 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) {
     my @ext_desc;
     my %base_invnums; # invnum => invoice date
     foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
@@ -3153,7 +3138,7 @@ sub _taxsort {
 
 sub _items_tax {
   my $self = shift;
 
 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, @_);
 
     $self->cust_bill_pkg;
   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
 
@@ -3182,7 +3167,7 @@ escape_function: the function used to escape strings.
 DEPRECATED? (expensive, mostly unused?)
 format_function: the function used to format CDRs.
 
 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).
 
 is present, only returns line items that belong to that category and/or
 location (whichever is defined).
 
@@ -3191,8 +3176,8 @@ which does something complicated.
 
 Returns a list of hashrefs, each of which may contain:
 
 
 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
 the package line.
 
 =cut
@@ -3278,13 +3263,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...
       # 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
               != $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
     # 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
@@ -3301,7 +3286,7 @@ sub _items_cust_bill_pkg {
                                 @cust_bill_pkg_display;
     } else {
       # otherwise, process all display records that aren't usage summaries
                                 @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;
       # category sections, but this is the historical behavior)
       @cust_bill_pkg_display = grep { !$_->summary }
                                 @cust_bill_pkg_display;
@@ -3332,14 +3317,14 @@ sub _items_cust_bill_pkg {
 
         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
           if $DEBUG > 1;
 
         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;
 
         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')
         # things with them
         my %item_dates = ();
         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
@@ -3360,7 +3345,7 @@ sub _items_cust_bill_pkg {
             if $DEBUG > 1;
 
           # append the word 'Setup' to the setup line if there's going to be
             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'
           # XXX localization
           my $description = $desc;
           $description .= ' Setup'
@@ -3382,7 +3367,7 @@ sub _items_cust_bill_pkg {
 
           unless ( $part_pkg->hide_svc_detail ) {
 
 
           unless ( $part_pkg->hide_svc_detail ) {
 
-            # still pass the svc_label through to the template, even if 
+            # 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,
             # not displaying it as an ext_description
             @svc_labels = map &{$escape_function}($_),
               $cust_pkg->h_labels_short($self->_date,
@@ -3499,7 +3484,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
                     # 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))
               )
                     # have service labels already)
                 || ($type eq 'U' and defined($r))
               )
@@ -3525,17 +3510,17 @@ sub _items_cust_bill_pkg {
 
             # Display of seconds_since_sqlradacct:
             # On the invoice, when processing @detail_items, look for a field
 
             # 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.
             # 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 ) {
                  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]);
                 # sqlradius_hour billing does it
                 my $sec = eval {
                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
@@ -3560,7 +3545,7 @@ sub _items_cust_bill_pkg {
 
           warn "$me _items_cust_bill_pkg calculating amount\n"
             if $DEBUG > 1;
 
           warn "$me _items_cust_bill_pkg calculating amount\n"
             if $DEBUG > 1;
-  
+
           my $amount = 0;
           if (!$type) {
             $amount = $cust_bill_pkg->recur;
           my $amount = 0;
           if (!$type) {
             $amount = $cust_bill_pkg->recur;
@@ -3569,7 +3554,7 @@ sub _items_cust_bill_pkg {
           } elsif ($type eq 'U') {
             $amount = $cust_bill_pkg->usage;
           }
           } elsif ($type eq 'U') {
             $amount = $cust_bill_pkg->usage;
           }
-  
+
           if ( !$type || $type eq 'R' ) {
 
             warn "$me _items_cust_bill_pkg adding recur\n"
           if ( !$type || $type eq 'R' ) {
 
             warn "$me _items_cust_bill_pkg adding recur\n"
@@ -3637,7 +3622,7 @@ sub _items_cust_bill_pkg {
         # items of this kind should normally not have sdate/edate.
         push @b, {
           'description' => $desc,
         # 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)
         };
 
                                            + $cust_bill_pkg->recur)
         };
 
@@ -3650,7 +3635,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 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 )
       ) {
           # a base recurring fee
           or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
       ) {
@@ -3667,7 +3652,7 @@ sub _items_cust_bill_pkg {
             $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
           }
 
             $_ = &{$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;
           # original price (whether this is a hidden line or not)
 
           $s->{amount} -= $item_discount->{setup_amount} if $s;
@@ -3712,9 +3697,9 @@ sub _items_cust_bill_pkg {
 =item _items_discounts_avail
 
 Returns an array of line item hashrefs representing available term discounts
 =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
 a setup fee if the discount is allowed to apply to setup fees.
 
 =cut
@@ -3722,7 +3707,7 @@ a setup fee if the discount is allowed to apply to setup fees.
 sub _items_discounts_avail {
   my $self = shift;
 
 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;
 
   return () unless $self->can('discount_plans');
   my %plans = $self->discount_plans;
 
@@ -3734,7 +3719,7 @@ sub _items_discounts_avail {
     my $plan = $plans{$months};
 
     my $term_total = sprintf('%.2f', $plan->discounted_total);
     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'). ' '.
                           100 * (1 - $term_total / $plan->base_total) );
     my $permonth = sprintf('%.2f', $term_total / $months);
     my $detail = $self->mt('discount on item'). ' '.
@@ -3747,7 +3732,7 @@ sub _items_discounts_avail {
     +{
       description => $self->mt('Save [_1]% by paying for [_2] months',
                                 $percent, $months),
     +{
       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 || ''),
     }
                                 $term_total, $money_char.$permonth),
       ext_description => ($detail || ''),
     }
index 36ecbea..5ae4f36 100644 (file)
@@ -11,6 +11,7 @@ use Fcntl qw(:flock); #for spool_csv
 use Cwd;
 use List::Util qw(min max sum);
 use Date::Format;
 use Cwd;
 use List::Util qw(min max sum);
 use Date::Format;
+use DateTime;
 use File::Temp 0.14;
 use HTML::Entities;
 use Storable qw( freeze thaw );
 use File::Temp 0.14;
 use HTML::Entities;
 use Storable qw( freeze thaw );
@@ -105,13 +106,13 @@ Deprecated fields
 =over 4
 
 =item billing_balance - the customer's balance immediately before generating
 =over 4
 
 =item billing_balance - the customer's balance immediately before generating
-this invoice.  DEPRECATED.  Use the L<FS::cust_main/balance_date> method 
+this invoice.  DEPRECATED.  Use the L<FS::cust_main/balance_date> method
 to determine the customer's balance at a specific time.
 
 =item previous_balance - the customer's balance immediately after generating
 the invoice before this one.  DEPRECATED.
 
 to determine the customer's balance at a specific time.
 
 =item previous_balance - the customer's balance immediately after generating
 the invoice before this one.  DEPRECATED.
 
-=item printed - formerly used to track the number of times an invoice had 
+=item printed - formerly used to track the number of times an invoice had
 been printed; no longer used.
 
 =back
 been printed; no longer used.
 
 =back
@@ -163,7 +164,7 @@ sub notice_name {
   $self->conf->config('notice_name') || 'Invoice'
 }
 
   $self->conf->config('notice_name') || 'Invoice'
 }
 
-sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } 
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
@@ -279,15 +280,15 @@ sub void {
 # internal-only and discourage use
 #
 # =item delete
 # internal-only and discourage use
 #
 # =item delete
-# 
+#
 # DO NOT USE THIS METHOD.  Instead, apply a credit against the invoice, or use
 # the B<void> method.
 # DO NOT USE THIS METHOD.  Instead, apply a credit against the invoice, or use
 # the B<void> method.
-# 
+#
 # This is only for internal use by V<void>, which is what you should be using.
 # This is only for internal use by V<void>, which is what you should be using.
-# 
+#
 # DO NOT USE THIS METHOD.  Whatever reason you think you have is almost certainly
 # wrong.  Use B<void>, that's what it is for.  Really.  This means you.
 # DO NOT USE THIS METHOD.  Whatever reason you think you have is almost certainly
 # wrong.  Use B<void>, that's what it is for.  Really.  This means you.
-# 
+#
 # =cut
 
 sub _delete {
 # =cut
 
 sub _delete {
@@ -460,9 +461,30 @@ sub previous_bill {
   $self->get('previous_bill');
 }
 
   $self->get('previous_bill');
 }
 
+=item following_bill
+
+Returns the customer's invoice that follows this one
+
+=cut
+
+sub following_bill {
+  my $self = shift;
+  if (!$self->get('following_bill')) {
+    $self->set('following_bill', qsearchs({
+      table   => 'cust_bill',
+      hashref => {
+        custnum => $self->custnum,
+        invnum  => { op => '>', value => $self->invnum },
+      },
+      order_by => 'ORDER BY invnum ASC LIMIT 1',
+    }));
+  }
+  $self->get('following_bill');
+}
+
 =item previous
 
 =item previous
 
-Returns a list consisting of the total previous balance for this customer, 
+Returns a list consisting of the total previous balance for this customer,
 followed by the previous outstanding invoices (as FS::cust_bill objects also).
 
 =cut
 followed by the previous outstanding invoices (as FS::cust_bill objects also).
 
 =cut
@@ -477,7 +499,7 @@ sub previous {
         qsearch( 'cust_bill', { 'custnum' => $self->custnum,
                                 #'_date'   => { op=>'<', value=>$self->_date },
                                 'invnum'   => { op=>'<', value=>$self->invnum },
         qsearch( 'cust_bill', { 'custnum' => $self->custnum,
                                 #'_date'   => { op=>'<', value=>$self->_date },
                                 'invnum'   => { op=>'<', value=>$self->invnum },
-                              } ) 
+                              } )
     ;
     foreach ( @cust_bill ) { $total += $_->owed; }
     $self->set('previous', [$total, @cust_bill]);
     ;
     foreach ( @cust_bill ) { $total += $_->owed; }
     $self->set('previous', [$total, @cust_bill]);
@@ -618,7 +640,7 @@ sub num_cust_event {
   my $sql =
     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
   my $sql =
     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
-  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
+  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql";
   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
   $sth->fetchrow_arrayref->[0];
 }
   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
   $sth->fetchrow_arrayref->[0];
 }
@@ -638,8 +660,8 @@ Returns a list: an empty list on success or a list of errors.
 sub suspend {
   my $self = shift;
 
 sub suspend {
   my $self = shift;
 
-  grep { $_->suspend(@_) } 
-  grep {! $_->getfield('cancel') } 
+  grep { $_->suspend(@_) }
+  grep {! $_->getfield('cancel') }
   $self->cust_pkg;
 
 }
   $self->cust_pkg;
 
 }
@@ -690,7 +712,7 @@ sub cancel {
 
   grep { $_ }
     map { $_->cancel(%opt) }
 
   grep { $_ }
     map { $_->cancel(%opt) }
-      grep { ! $_->getfield('cancel') } 
+      grep { ! $_->getfield('cancel') }
         @pkgs;
 }
 
         @pkgs;
 }
 
@@ -822,7 +844,7 @@ sub cust_bill_batch {
 
 =item discount_plans
 
 
 =item discount_plans
 
-Returns all discount plans (L<FS::discount_plan>) for this invoice, as a 
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
 hash keyed by term length.
 
 =cut
 hash keyed by term length.
 
 =cut
@@ -865,6 +887,35 @@ sub owed {
   $balance;
 }
 
   $balance;
 }
 
+=item owed_on_invoice
+
+Returns the amount to be displayed as the "Balance Due" on this
+invoice.  Amount returned depends on conf flags for invoicing
+
+See L<FS::cust_bill::owed> for the true amount currently owed
+
+=cut
+
+sub owed_on_invoice {
+  my $self = shift;
+
+  #return $self->owed()
+  #  unless $self->conf->exists('previous_balance-payments_since')
+
+  # Add charges from this invoice
+  my $owed = $self->charged();
+
+  # Add carried balances from previous invoices
+  #   If previous items aren't to be displayed on the invoice,
+  #   _items_previous() is aware of this and responds appropriately.
+  $owed += $_->{amount} for $self->_items_previous();
+
+  # Subtract payments and credits displayed on this invoice
+  $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
+
+  return $owed;
+}
+
 sub owed_pkgnum {
   my( $self, $pkgnum ) = @_;
 
 sub owed_pkgnum {
   my( $self, $pkgnum ) = @_;
 
@@ -927,7 +978,7 @@ sub apply_payments_and_credits {
 
   $self->select_for_update; #mutex
 
 
   $self->select_for_update; #mutex
 
-  my @payments = grep { $_->unapplied > 0 } 
+  my @payments = grep { $_->unapplied > 0 }
                    grep { !$_->no_auto_apply }
                      $self->cust_main->cust_pay;
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
                    grep { !$_->no_auto_apply }
                      $self->cust_main->cust_pay;
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
@@ -956,7 +1007,7 @@ sub apply_payments_and_credits {
           );
       my $max_credit_weight =
         max( map  { $_->part_pkg->credit_weight || 0 }
           );
       my $max_credit_weight =
         max( map  { $_->part_pkg->credit_weight || 0 }
-            grep { $_ } 
+            grep { $_ }
              map  { $_->cust_pkg }
                   @open_lineitems
            );
              map  { $_->cust_pkg }
                   @open_lineitems
            );
@@ -967,7 +1018,7 @@ sub apply_payments_and_credits {
       } else {
         $app = 'credit';
       }
       } else {
         $app = 'credit';
       }
-    
+
     } elsif ( @payments ) {
       $app = 'pay';
     } elsif ( @credits ) {
     } elsif ( @payments ) {
       $app = 'pay';
     } elsif ( @credits ) {
@@ -1041,7 +1092,7 @@ I<from> overrides the default email invoice From: address.
 
 I<amount>: obsolete, does nothing
 
 
 I<amount>: obsolete, does nothing
 
-I<notice_name> overrides "Invoice" as the name of the sent document 
+I<notice_name> overrides "Invoice" as the name of the sent document
 (templates from 10/2009 or newer required).
 
 I<lpr> overrides the system 'lpr' option as the command to print a document
 (templates from 10/2009 or newer required).
 
 I<lpr> overrides the system 'lpr' option as the command to print a document
@@ -1118,7 +1169,7 @@ sub queueable_email {
   $self->set('mode', $opt{mode})
     if $opt{mode};
 
   $self->set('mode', $opt{mode})
     if $opt{mode};
 
-  my %args = map {$_ => $opt{$_}} 
+  my %args = map {$_ => $opt{$_}}
              grep { $opt{$_} }
               qw( from notice_name no_coupon template );
 
              grep { $opt{$_} }
               qw( from notice_name no_coupon template );
 
@@ -1155,7 +1206,7 @@ sub pdf_filename {
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
-Options must be passed as a hashref.  Positional parameters are no longer 
+Options must be passed as a hashref.  Positional parameters are no longer
 allowed.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 allowed.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
@@ -1248,7 +1299,7 @@ sub fax_invoice {
 
 =item batch_invoice [ HASHREF ]
 
 
 =item batch_invoice [ HASHREF ]
 
-Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
+Place this invoice into the open batch (see C<FS::bill_batch>).  If there
 isn't an open batch, one will be created.
 
 HASHREF may contain any options to be passed to C<print_pdf>.
 isn't an open batch, one will be created.
 
 HASHREF may contain any options to be passed to C<print_pdf>.
@@ -1292,7 +1343,7 @@ sub get_open_bill_batch {
   return $batch;
 }
 
   return $batch;
 }
 
-=item ftp_invoice [ TEMPLATENAME ] 
+=item ftp_invoice [ TEMPLATENAME ]
 
 Sends this invoice data via FTP.
 
 
 Sends this invoice data via FTP.
 
@@ -1315,7 +1366,7 @@ sub ftp_invoice {
   );
 }
 
   );
 }
 
-=item spool_invoice [ TEMPLATENAME ] 
+=item spool_invoice [ TEMPLATENAME ]
 
 Spools this invoice data (see L<FS::spool_csv>)
 
 
 Spools this invoice data (see L<FS::spool_csv>)
 
@@ -1364,7 +1415,7 @@ sub send_csv {
   # don't localize dates here, they're a defined format
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
   my $file = "$spooldir/$tracctnum.csv";
   # don't localize dates here, they're a defined format
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
   my $file = "$spooldir/$tracctnum.csv";
-  
+
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
   open(CSV, ">$file") or die "can't open $file: $!";
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
   open(CSV, ">$file") or die "can't open $file: $!";
@@ -1469,7 +1520,7 @@ sub spool_csv {
   }
 
   $file = "$spooldir/$file.csv";
   }
 
   $file = "$spooldir/$file.csv";
-  
+
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
 
   open(CSV, ">>$file") or die "can't open $file: $!";
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
 
   open(CSV, ">>$file") or die "can't open $file: $!";
@@ -1514,7 +1565,7 @@ detail information for this invoice.
 If I<format> is not specified or "default", the fields of the CSV file are as
 follows:
 
 If I<format> is not specified or "default", the fields of the CSV file are as
 follows:
 
-record_type, invnum, custnum, _date, charged, first, last, company, address1, 
+record_type, invnum, custnum, _date, charged, first, last, company, address1,
 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
@@ -1620,7 +1671,7 @@ If I<format> is "billco", the fields of the detail CSV file are as follows:
   9     | Grouping Code              | GROUP     | CHAR |     2
   10    | User Defined               | ACCT CODE | CHAR |    15
 
   9     | Grouping Code              | GROUP     | CHAR |     2
   10    | User Defined               | ACCT CODE | CHAR |    15
 
-If format is 'oneline', there is no detail file.  Each invoice has a 
+If format is 'oneline', there is no detail file.  Each invoice has a
 header line only, with the fields:
 
 Agent number, agent name, customer number, first name, last name, address
 header line only, with the fields:
 
 Agent number, agent name, customer number, first name, last name, address
@@ -1630,21 +1681,21 @@ amount charged, amount due, previous balance, due date.
 and then, for each line item, three columns containing the package number,
 description, and amount.
 
 and then, for each line item, three columns containing the package number,
 description, and amount.
 
-If format is 'bridgestone', there is no detail file.  Each invoice has a 
+If format is 'bridgestone', there is no detail file.  Each invoice has a
 header line with the following fields in a fixed-width format:
 
 Customer number (in display format), date, name (first last), company,
 address 1, address 2, city, state, zip.
 
 This is a mailing list format, and has no per-invoice fields.  To avoid
 header line with the following fields in a fixed-width format:
 
 Customer number (in display format), date, name (first last), company,
 address 1, address 2, city, state, zip.
 
 This is a mailing list format, and has no per-invoice fields.  To avoid
-sending redundant notices, the spooling event should have a "once" or 
+sending redundant notices, the spooling event should have a "once" or
 "once_percust_every" condition.
 
 =cut
 
 sub print_csv {
   my($self, %opt) = @_;
 "once_percust_every" condition.
 
 =cut
 
 sub print_csv {
   my($self, %opt) = @_;
-  
+
   eval "use Text::CSV_XS";
   die $@ if $@;
 
   eval "use Text::CSV_XS";
   die $@ if $@;
 
@@ -1655,6 +1706,9 @@ sub print_csv {
 
   my $time = $opt{'time'} || time;
 
 
   my $time = $opt{'time'} || time;
 
+  $self->set('_template', $opt{template})
+    if exists $opt{template};
+
   my $tracctnum = ''; #leaking out from billco-specific sections :/
   if ( $format eq 'billco' ) {
 
   my $tracctnum = ''; #leaking out from billco-specific sections :/
   if ( $format eq 'billco' ) {
 
@@ -1717,8 +1771,8 @@ sub print_csv {
     );
 
   } elsif ( $format eq 'oneline' ) { #name
     );
 
   } elsif ( $format eq 'oneline' ) { #name
-  
-    my ($previous_balance) = $self->previous; 
+
+    my ($previous_balance) = $self->previous;
     $previous_balance = sprintf('%.2f', $previous_balance);
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
     my @items = map {
     $previous_balance = sprintf('%.2f', $previous_balance);
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
     my @items = map {
@@ -1855,7 +1909,7 @@ sub print_csv {
             my $classnum = $cust_svc->part_svc->classnum;
             my $part_svc_class = FS::part_svc_class->by_key($classnum)
               if $classnum;
             my $classnum = $cust_svc->part_svc->classnum;
             my $part_svc_class = FS::part_svc_class->by_key($classnum)
               if $classnum;
-            $svc_class{$svcpart} = $part_svc_class ? 
+            $svc_class{$svcpart} = $part_svc_class ?
                                    $part_svc_class->classname :
                                    '';
           }
                                    $part_svc_class->classname :
                                    '';
           }
@@ -1894,7 +1948,7 @@ sub print_csv {
     return join('', $header, @details, "\n");
 
   } else { # default
     return join('', $header, @details, "\n");
 
   } else { # default
-  
+
     $csv->combine(
       'cust_bill',
       $self->invnum,
     $csv->combine(
       'cust_bill',
       $self->invnum,
@@ -1954,7 +2008,7 @@ sub print_csv {
 
       my($pkg, $setup, $recur, $sdate, $edate);
       if ( $cust_bill_pkg->pkgnum ) {
 
       my($pkg, $setup, $recur, $sdate, $edate);
       if ( $cust_bill_pkg->pkgnum ) {
-      
+
         ($pkg, $setup, $recur, $sdate, $edate) = (
           $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
         ($pkg, $setup, $recur, $sdate, $edate) = (
           $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
@@ -1963,21 +2017,21 @@ sub print_csv {
           ( $cust_bill_pkg->recur != 0
             ? sprintf("%.2f", $cust_bill_pkg->recur )
             : '' ),
           ( $cust_bill_pkg->recur != 0
             ? sprintf("%.2f", $cust_bill_pkg->recur )
             : '' ),
-          ( $cust_bill_pkg->sdate 
+          ( $cust_bill_pkg->sdate
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
-          ($cust_bill_pkg->edate 
+          ($cust_bill_pkg->edate
             ? time2str("%x", $cust_bill_pkg->edate)
             : '' ),
         );
             ? time2str("%x", $cust_bill_pkg->edate)
             : '' ),
         );
-  
+
       } else { #pkgnum tax
         next unless $cust_bill_pkg->setup != 0;
         $pkg = $cust_bill_pkg->desc;
         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
         ( $sdate, $edate ) = ( '', '' );
       }
       } else { #pkgnum tax
         next unless $cust_bill_pkg->setup != 0;
         $pkg = $cust_bill_pkg->desc;
         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
         ( $sdate, $edate ) = ( '', '' );
       }
-  
+
       $csv->combine(
         'cust_bill_pkg',
         $self->invnum,
       $csv->combine(
         'cust_bill_pkg',
         $self->invnum,
@@ -2096,7 +2150,7 @@ sub batch_card {
   my $cust_main = $self->cust_main;
 
   $options{invnum} = $self->invnum;
   my $cust_main = $self->cust_main;
 
   $options{invnum} = $self->invnum;
-  
+
   $cust_main->batch_card(%options);
 }
 
   $cust_main->batch_card(%options);
 }
 
@@ -2120,7 +2174,7 @@ PNG file name is returned. Otherwise, the PNG image itself is returned.
 
 sub invoice_barcode {
     my ($self, $dir) = (shift,shift);
 
 sub invoice_barcode {
     my ($self, $dir) = (shift,shift);
-    
+
     my $gdbar = new GD::Barcode('Code39',$self->invnum);
        die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
     my $gd = $gdbar->plot(Height => 30);
     my $gdbar = new GD::Barcode('Code39',$self->invnum);
        die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
     my $gd = $gdbar->plot(Height => 30);
@@ -2215,13 +2269,13 @@ sub _items_extra_usage_sections {
       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
         my $amount = $detail->amount;
         next unless $amount && $amount > 0;
       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
         my $amount = $detail->amount;
         next unless $amount && $amount > 0;
+
         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
         $sections{$section}{amount} += $amount;  #subtotal
         $sections{$section}{calls}++;
         $sections{$section}{duration} += $detail->duration;
 
         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
         $sections{$section}{amount} += $amount;  #subtotal
         $sections{$section}{calls}++;
         $sections{$section}{duration} += $detail->duration;
 
-        my $desc = $detail->regionname; 
+        my $desc = $detail->regionname;
         my $description = $desc;
         $description = substr($desc, 0, $maxlength). '...'
           if $format eq 'latex' && length($desc) > $maxlength;
         my $description = $desc;
         $description = substr($desc, 0, $maxlength). '...'
           if $format eq 'latex' && length($desc) > $maxlength;
@@ -2263,7 +2317,7 @@ sub _items_extra_usage_sections {
                               qw( description_generator header_generator total_generator total_line_generator )
                             )
                           : ()
                               qw( description_generator header_generator total_generator total_line_generator )
                             )
                           : ()
-                        ), 
+                        ),
                       };
   }
 
                       };
   }
 
@@ -2310,10 +2364,10 @@ sub _did_summary {
            my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
            my $phone_deleted;
            $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
            my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
            my $phone_deleted;
            $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
-           
+
 # DID either activated or ported in; cannot be both for same DID simultaneously
            if ($inserted >= $start && $inserted <= $end && $phone_inserted
 # DID either activated or ported in; cannot be both for same DID simultaneously
            if ($inserted >= $start && $inserted <= $end && $phone_inserted
-               && (!$phone_inserted->lnp_status 
+               && (!$phone_inserted->lnp_status
                    || $phone_inserted->lnp_status eq ''
                    || $phone_inserted->lnp_status eq 'native')) {
                $num_activated++;
                    || $phone_inserted->lnp_status eq ''
                    || $phone_inserted->lnp_status eq 'native')) {
                $num_activated++;
@@ -2321,21 +2375,21 @@ sub _did_summary {
            else { # this one not so clean, should probably move to (h_)svc_phone
                  local($FS::Record::qsearch_qualify_columns) = 0;
                 my $phone_portedin = qsearchs( 'h_svc_phone',
            else { # this one not so clean, should probably move to (h_)svc_phone
                  local($FS::Record::qsearch_qualify_columns) = 0;
                 my $phone_portedin = qsearchs( 'h_svc_phone',
-                     { 'svcnum' => $h_cust_svc->svcnum, 
-                       'lnp_status' => 'portedin' },  
-                     FS::h_svc_phone->sql_h_searchs($end),  
+                     { 'svcnum' => $h_cust_svc->svcnum,
+                       'lnp_status' => 'portedin' },
+                     FS::h_svc_phone->sql_h_searchs($end),
                    );
                 $num_portedin++ if $phone_portedin;
            }
 
 # DID either deactivated or ported out;        cannot be both for same DID simultaneously
            if($deleted >= $start && $deleted <= $end && $phone_deleted
                    );
                 $num_portedin++ if $phone_portedin;
            }
 
 # DID either deactivated or ported out;        cannot be both for same DID simultaneously
            if($deleted >= $start && $deleted <= $end && $phone_deleted
-               && (!$phone_deleted->lnp_status 
+               && (!$phone_deleted->lnp_status
                    || $phone_deleted->lnp_status ne 'portingout')) {
                $num_deactivated++;
                    || $phone_deleted->lnp_status ne 'portingout')) {
                $num_deactivated++;
-           } 
-           elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
-               && $phone_deleted->lnp_status 
+           }
+           elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+               && $phone_deleted->lnp_status
                && $phone_deleted->lnp_status eq 'portingout') {
                $num_portedout++;
            }
                && $phone_deleted->lnp_status eq 'portingout') {
                $num_portedout++;
            }
@@ -2388,8 +2442,8 @@ sub _items_accountcode_cdr {
         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
             $section->{'header'} = $detail->formatted('format' => $format)
         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
             $section->{'header'} = $detail->formatted('format' => $format)
-                if($detail->detail eq $section->{'header'}); 
-      
+                if($detail->detail eq $section->{'header'});
+
             my $accountcode = $detail->accountcode;
             next unless $accountcode;
 
             my $accountcode = $detail->accountcode;
             next unless $accountcode;
 
@@ -2472,7 +2526,7 @@ sub _items_svc_phone_sections {
       $sections{$phonenum}{calls}++;
       $sections{$phonenum}{duration} += $detail->duration;
 
       $sections{$phonenum}{calls}++;
       $sections{$phonenum}{duration} += $detail->duration;
 
-      my $desc = $detail->regionname; 
+      my $desc = $detail->regionname;
       my $description = $desc;
       $description = substr($desc, 0, $maxlength). '...'
         if $format eq 'latex' && length($desc) > $maxlength;
       my $description = $desc;
       $description = substr($desc, 0, $maxlength). '...'
         if $format eq 'latex' && length($desc) > $maxlength;
@@ -2561,7 +2615,7 @@ sub _items_svc_phone_sections {
                                 total_line_generator
                               )
                           )
                                 total_line_generator
                               )
                           )
-                        ), 
+                        ),
                       };
   }
 
                       };
   }
 
@@ -2580,8 +2634,8 @@ sub _items_svc_phone_sections {
       push @lines, $l;
     }
   }
       push @lines, $l;
     }
   }
-  
-  if($conf->exists('phone_usage_class_summary')) { 
+
+  if($conf->exists('phone_usage_class_summary')) {
       # this only works with Latex
       my @newlines;
       my @newsections;
       # this only works with Latex
       my @newlines;
       my @newsections;
@@ -2607,7 +2661,7 @@ sub _items_svc_phone_sections {
            };
            $calls_detail{'description'} = 'Calls Detail: '
                                                    . $section->{'phonenum'};
            };
            $calls_detail{'description'} = 'Calls Detail: '
                                                    . $section->{'phonenum'};
-           push @newsections, \%calls_detail;  
+           push @newsections, \%calls_detail;
        }
       }
 
        }
       }
 
@@ -2616,7 +2670,7 @@ sub _items_svc_phone_sections {
       foreach my $newsection ( @newsections ) {
        if($newsection->{'post_total'}) { # this means Calls Summary
            foreach my $section ( @sections ) {
       foreach my $newsection ( @newsections ) {
        if($newsection->{'post_total'}) { # this means Calls Summary
            foreach my $section ( @sections ) {
-               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
+               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
                                && !$section->{'post_total'});
                my $newdesc = $section->{'description'};
                my $tn = $section->{'phonenum'};
                                && !$section->{'post_total'});
                my $newdesc = $section->{'description'};
                my $tn = $section->{'phonenum'};
@@ -2666,7 +2720,7 @@ sub _items_svc_phone_sections {
 
 =sub _items_usage_class_summary OPTIONS
 
 
 =sub _items_usage_class_summary OPTIONS
 
-Returns a list of detail items summarizing the usage charges on this 
+Returns a list of detail items summarizing the usage charges on this
 invoice.  Each one will have 'amount', 'description' (the usage charge name),
 and 'usage_classnum'.
 
 invoice.  Each one will have 'amount', 'description' (the usage charge name),
 and 'usage_classnum'.
 
@@ -2713,207 +2767,784 @@ sub _items_usage_class_summary {
   return @l;
 }
 
   return @l;
 }
 
+=sub _items_previous()
+
+  Returns an array of hashrefs, each hashref representing a line-item on
+  the current bill for previous unpaid invoices.
+
+  keys for each previous_item:
+  - amount (see notes)
+  - pkgnum
+  - description
+  - invnum
+  - _date
+
+  Payments and credits shown on this invoice may vary based on configuraiton.
+
+  when conf flag previous_balance-payments_since is set:
+    This method works backwards to rebuild the invoice as a snapshot in time.
+    The invoice displayed will have the balances owed, and payments made,
+    reflecting the state of the account at the time of invoice generation.
+
+=cut
+
 sub _items_previous {
 sub _items_previous {
+
   my $self = shift;
   my $self = shift;
-  my $conf = $self->conf;
-  my $cust_main = $self->cust_main;
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-  my @b = ();
-  foreach ( @pr_cust_bill ) {
-    my $date = $conf->exists('invoice_show_prior_due_date')
-               ? 'due '. $_->due_date2str('short')
-               : $self->time2str_local('short', $_->_date);
-    push @b, {
-      'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
-      #'pkgpart'     => 'N/A',
-      'pkgnum'      => 'N/A',
-      'amount'      => sprintf("%.2f", $_->owed),
-    };
+
+  # simple memoize
+  if ($self->get('_items_previous')) {
+    return sort { $a->{_date} <=> $b->{_date} }
+         values %{ $self->get('_items_previous') };
+  }
+
+  # Gets the customer's current balance and outstanding invoices.
+  my ($prev_balance, @open_invoices) = $self->previous;
+
+  my %invoices = map {
+    $_->invnum => $self->__items_previous_map_invoice($_)
+  } @open_invoices;
+
+  # Which credits and payments displayed on the bill will vary based on
+  # conf flag previous_balance-payments_since.
+  my @credits  = $self->_items_credits();
+  my @payments = $self->_items_payments();
+
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    # For each credit or payment, determine which invoices it was applied to.
+    # Manipulate data displayed so the invoice displayed appears as a
+    # snapshot in time... with previous balances and balance owed displayed
+    # as they were at the time of invoice creation.
+
+    my @credits_postbill = $self->_items_credits_postbill();
+    my @payments_postbill = $self->_items_payments_postbill();
+
+    my %pmnt_dupechk;
+    my %cred_dupechk;
+
+    # Each section below follows this pattern on a payment/credit
+    #
+    # - Dupe check, avoid adjusting for the same item twice
+    # - If invoice being adjusted for isn't in our list, add it
+    # - Adjust the invoice balance to refelct balnace without the
+    #   credit or payment applied
+    #
+
+    # Working with payments displayed on this bill
+    for my $pmt_hash (@payments) {
+      my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
+      for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
+        next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
+        $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
+
+        my $invnum = $cust_bill_pay->invnum;
+
+        $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+          unless exists $invoices{$invnum};
+
+        $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
+      }
+    }
+
+    # Working with credits displayed on this bill
+    for my $cred_hash (@credits) {
+      my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
+      for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
+        next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
+        $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
+
+        my $invnum = $cust_credit_bill->invnum;
+
+        $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+          unless exists $invoices{$invnum};
+
+        $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
+      }
+    }
+
+    # Working with both credits and payments which are not displayed
+    # on this bill, but which have affected this bill's balances
+    for my $postbill (@payments_postbill, @credits_postbill) {
+
+      if ($postbill->{billpaynum}) {
+        next if exists $pmnt_dupechk{$postbill->{billpaynum}};
+        $pmnt_dupechk{$postbill->{billpaynum}} = 1;
+      } elsif ($postbill->{creditbillnum}) {
+        next if exists $cred_dupechk{$postbill->{creditbillnum}};
+        $cred_dupechk{$postbill->{creditbillnum}} = 1;
+      } else {
+        die "Missing creditbillnum or billpaynum";
+      }
+
+      my $invnum = $postbill->{invnum};
+
+      $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+        unless exists $invoices{$invnum};
+
+      $invoices{$invnum}->{amount} += $postbill->{amount};
+    }
+
+    # Make sure current invoice doesn't appear in previous items
+    delete $invoices{$self->invnum}
+      if exists $invoices{$self->invnum};
+
   }
   }
-  @b;
 
 
-  #{
-  #    'description'     => 'Previous Balance',
-  #    #'pkgpart'         => 'N/A',
-  #    'pkgnum'          => 'N/A',
-  #    'amount'          => sprintf("%10.2f", $pr_total ),
-  #    'ext_description' => [ map {
-  #                                 "Invoice ". $_->invnum.
-  #                                 " (". time2str("%x",$_->_date). ") ".
-  #                                 sprintf("%10.2f", $_->owed)
-  #                         } @pr_cust_bill ],
+  # Make sure amount is formatted as a dollar string
+  # (Formatting should happen on the template side, but is not?)
+  $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
+    for keys %invoices;
+
+  $self->set('_items_previous', \%invoices);
+  return sort { $a->{_date} <=> $b->{_date} } values %invoices;
 
 
-  #};
 }
 
 }
 
+=sub _items_previous_total
+
+  Return sum of amounts from all items returned by _items_previous
+  Results will vary based on invoicing conf flags
+
+=cut
+
+sub _items_previous_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_previous();
+  return $tot;
+}
+
+sub __items_previous_get_invoice {
+  # Helper function for _items_previous
+  #
+  # Read a record from cust_bill, return a hash of it's information
+  my ($self, $invnum) = @_;
+  die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
+
+  my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
+  return $self->__items_previous_map_invoice($cust_bill);
+}
+
+sub __items_previous_map_invoice {
+  # Helper function for _items_previous
+  #
+  # Transform a cust_bill object into a simple hash reference of the type
+  # required by _items_previous
+  my ($self, $cust_bill) = @_;
+  die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
+
+  my $date = $self->conf->exists('invoice_show_prior_due_date')
+           ? 'due '.$cust_bill->due_date2str('short')
+           : $self->time2str_local('short', $cust_bill->_date);
+
+  return {
+    invnum => $cust_bill->invnum,
+    amount => $cust_bill->owed,
+    pkgnum => 'N/A',
+    _date  => $cust_bill->_date,
+    description => join(' ',
+      $self->mt('Previous Balance, Invoice #'),
+      $cust_bill->invnum,
+      "($date)"
+    ),
+  }
+}
+
+=sub _items_credits()
+
+  Return array of hashrefs containing credits to be shown as line-items
+  when rendering this bill.
+
+  keys for each credit item:
+  - crednum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
+
+  This method has three ways it selects which credits to display on
+  this bill:
+
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+     Returns credits that have been applied to this bill only
+
+  2) Case:
+       Conf flag set for 'previous_balance-payments_since'
+
+     List all credits that have been recorded during the time period
+     between the timestamps of the last invoice and this invoice
+
+  3) Case:
+        Conf flag set for 'previous_balance-payments_since'
+        $opt{'template'} eq 'statement'
+
+    List all payments that have been recorded between the timestamps
+    of the previous invoice and the following invoice.
+
+     This is used to give the customer a receipt for a payment
+     in the form of their last bill with the payment amended.
+
+     I am concerned with this implementation, but leaving in place as is
+     If this option is selected, while viewing an older bill, the old bill
+     will show ALL future credits for future bills, but no charges for
+     future bills.  Somebody could be misled into believing they have a
+     large account credit when they don't.  Also, interrupts the chain of
+     invoices as an account history... the customer could have two invoices
+     in their fileing cabinet, for two different dates, both with a line item
+     for the same duplicate credit.  The accounting is technically accurate,
+     but somebody could easily become confused and think two credits were
+     made, when really those two line items on two different bills represent
+     only a single credit
+
+=cut
+
 sub _items_credits {
 sub _items_credits {
-  my( $self, %opt ) = @_;
-  my $trim_len = $opt{'trim_len'} || 40;
-
-  my @b;
-  #credits
-  my @objects;
-  if ( $self->conf->exists('previous_balance-payments_since') ) {
-    if ( $opt{'template'} eq 'statement' ) {
-      # then the current bill is a "statement" (i.e. an invoice sent as
-      # a payment receipt)
-      # and in that case we want to see payments on or after THIS invoice
-      @objects = qsearch('cust_credit', {
-          'custnum' => $self->custnum,
-          '_date'   => {op => '>=', value => $self->_date},
-      });
+
+  my $self= shift;
+
+  # Simple memoize
+  return @{$self->get('_items_credits')} if $self->get('_items_credits');
+
+  my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
+  my $trim_len = $opt{template} || $self->get('trim_len') || 40;
+
+  my @return;
+  my @cust_credit_objs;
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    if ($template eq 'statement') {
+      # Case 3 (see above)
+      # Return credits timestamped between the previous and following bills
+
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
+
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
+
+      my %query = (
+        table => 'cust_credit',
+        hashref => {
+          custnum => $self->custnum,
+          _date => { op => '>=', value => $date_start },
+        },
+      );
+      $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+      @cust_credit_objs = qsearch(\%query);
+
     } else {
     } else {
-      my $date = 0;
-      $date = $self->previous_bill->_date if $self->previous_bill;
-      @objects = qsearch('cust_credit', {
-          'custnum' => $self->custnum,
-          '_date'   => {op => '>=', value => $date},
+      # Case 2 (see above)
+      # Return credits timestamps between this and the previous bills
+
+      my $date_start = 0;
+      my $date_end = $self->_date;
+
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
+      }
+
+      @cust_credit_objs = qsearch({
+        table => 'cust_credit',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
       });
     }
       });
     }
+
   } else {
   } else {
-    @objects = $self->cust_credited;
+    # Case 1 (see above)
+    # Return only credits that have been applied to this bill
+
+    @cust_credit_objs = $self->cust_credited;
+
   }
 
   }
 
-  foreach my $obj ( @objects ) {
+  # Translate objects into hashrefs
+  foreach my $obj ( @cust_credit_objs ) {
     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
+    my %r_obj = (
+      amount       => sprintf('%.2f',$cust_credit->amount),
+      crednum      => $cust_credit->crednum,
+      _date        => $cust_credit->_date,
+      creditreason => $cust_credit->reason,
+    );
 
     my $reason = substr($cust_credit->reason, 0, $trim_len);
     $reason .= '...' if length($reason) < length($cust_credit->reason);
 
     my $reason = substr($cust_credit->reason, 0, $trim_len);
     $reason .= '...' if length($reason) < length($cust_credit->reason);
-    $reason = " ($reason) " if $reason;
-
-    push @b, {
-      #'description' => 'Credit ref\#'. $_->crednum.
-      #                 " (". time2str("%x",$_->cust_credit->_date) .")".
-      #                 $reason,
-      'description' => $self->mt('Credit applied').' '.
-                       $self->time2str_local('short', $obj->_date). $reason,
-      'amount'      => sprintf("%.2f",$obj->amount),
-    };
+    $reason = "($reason)" if $reason;
+
+    $r_obj{description} = join(' ',
+      $self->mt('Credit applied'),
+      $self->time2str_local('short', $cust_credit->_date),
+      $reason,
+    );
+
+    push @return, \%r_obj;
   }
   }
+  $self->set('_items_credits',\@return);
+  @return;
+}
+
+=sub _items_credits_total
+
+  Return the total of al items from _items_credits
+  Will vary based on invoice display conf flag
 
 
-  @b;
+=cut
+
+sub _items_credits_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_credits();
+  return $tot;
+}
+
+
+
+=sub _items_credits_postbill()
+
+  Returns an array of hashrefs for credits where
+  - Credit issued after this invoice
+  - Credit applied to an invoice before this invoice
+
+  Returned hashrefs are of the format returned by _items_credits()
+
+=cut
+
+sub _items_credits_postbill {
+  my $self = shift;
+
+  my @cust_credit_bill = qsearch({
+    table   => 'cust_credit_bill',
+    select  => join(', ',qw(
+      cust_credit_bill.creditbillnum
+      cust_credit_bill._date
+      cust_credit_bill.invnum
+      cust_credit_bill.amount
+    )),
+    addl_from => ' LEFT JOIN cust_credit'.
+                 ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
+    extra_sql => ' WHERE cust_credit.custnum     = '.$self->custnum.
+                 ' AND   cust_credit_bill._date  > '.$self->_date.
+                 ' AND   cust_credit_bill.invnum < '.$self->invnum.' ',
+#! did not investigate why hashref doesn't work for this join query
+#    hashref => {
+#      'cust_credit.custnum'     => {op => '=', value => $self->custnum},
+#      'cust_credit_bill._date'  => {op => '>', value => $self->_date},
+#      'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
+#    },
+  });
 
 
+  return map {{
+    _date         => $_->_date,
+    invnum        => $_->invnum,
+    amount        => $_->amount,
+    creditbillnum => $_->creditbillnum,
+  }} @cust_credit_bill;
 }
 
 }
 
+=sub _items_payments_postbill()
+
+  Returns an array of hashrefs for payments where
+  - Payment occured after this invoice
+  - Payment applied to an invoice before this invoice
+
+  Returned hashrefs are of the format returned by _items_payments()
+
+=cut
+
+sub _items_payments_postbill {
+  my $self = shift;
+
+  my @cust_bill_pay = qsearch({
+    table    => 'cust_bill_pay',
+    select => join(', ',qw(
+      cust_bill_pay.billpaynum
+      cust_bill_pay._date
+      cust_bill_pay.invnum
+      cust_bill_pay.amount
+    )),
+    addl_from => ' LEFT JOIN cust_bill'.
+                 ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
+    extra_sql => ' WHERE cust_bill.custnum     = '.$self->custnum.
+                 ' AND   cust_bill_pay._date   > '.$self->_date.
+                 ' AND   cust_bill_pay.invnum  < '.$self->invnum.' ',
+  });
+
+  return map {{
+    _date      => $_->_date,
+    invnum     => $_->invnum,
+    amount     => $_->amount,
+    billpaynum => $_->billpaynum,
+  }} @cust_bill_pay;
+}
+
+=sub _items_payments()
+
+  Return array of hashrefs containing payments to be shown as line-items
+  when rendering this bill.
+
+  keys for each payment item:
+  - paynum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
+
+  This method has three ways it selects which payments to display on
+  this bill:
+
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+     Returns payments that have been applied to this bill only
+
+  2) Case:
+       Conf flag set for 'previous_balance-payments_since'
+
+     List all payments that have been recorded between the timestamps
+     of the previous invoice and this invoice
+
+  3) Case:
+        Conf flag set for 'previous_balance-payments_since'
+        $opt{'template'} eq 'statement'
+
+     List all payments that have been recorded between the timestamps
+     of the previous invoice and the following invoice.
+
+     I am concerned with this implementation, but leaving in place as is
+     If this option is selected, while viewing an older bill, the old bill
+     will show ALL future payments for future bills, but no charges for
+     future bills.  Somebody could be misled into believing they have a
+     large account credit when they don't.  Also, interrupts the chain of
+     invoices as an account history... the customer could have two invoices
+     in their fileing cabinet, for two different dates, both with a line item
+     for the same duplicate payment.  The accounting is technically accurate,
+     but somebody could easily become confused and think two payments were
+     made, when really those two line items on two different bills represent
+     only a single payment.
+
+=cut
+
 sub _items_payments {
 sub _items_payments {
+
   my $self = shift;
   my $self = shift;
+
+  # Simple memoize
+  return @{$self->get('_items_payments')} if $self->get('_items_payments');
+
   my %opt = @_;
   my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
+
+  my @return;
+  my @cust_pay_objs;
+
+  my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    if ($template eq 'statement') {
+print "\nCASE 3\n";
+      # Case 3 (see above)
+      # Return payments timestamped between the previous and following bills
+
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
+
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
+
+      my %query = (
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => { op => '>=', value => $date_start },
+        },
+      );
+      $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+      @cust_pay_objs = qsearch(\%query);
 
 
-  my @b;
-  my $detailed = $self->conf->exists('invoice_payment_details');
-  my @objects;
-  if ( $self->conf->exists('previous_balance-payments_since') ) {
-    # then show payments dated on/after the previous bill...
-    if ( $opt{'template'} eq 'statement' ) {
-      # then the current bill is a "statement" (i.e. an invoice sent as
-      # a payment receipt)
-      # and in that case we want to see payments on or after THIS invoice
-      @objects = qsearch('cust_pay', {
-          'custnum' => $self->custnum,
-          '_date'   => {op => '>=', value => $self->_date},
-      });
     } else {
     } else {
-      # the normal case: payments on or after the previous invoice
-      my $date = 0;
-      $date = $self->previous_bill->_date if $self->previous_bill;
-      @objects = qsearch('cust_pay', {
-        'custnum' => $self->custnum,
-        '_date'   => {op => '>=', value => $date},
+      # Case 2 (see above)
+      # Return payments timestamped between this and the previous bill
+print "\nCASE 2\n";
+      my $date_start = 0;
+      my $date_end = $self->_date;
+
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
+      }
+
+      @cust_pay_objs = qsearch({
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
       });
       });
-      # and before the current bill...
-      @objects = grep { $_->_date < $self->_date } @objects;
     }
     }
+
   } else {
   } else {
-    @objects = $self->cust_bill_pay;
+    # Case 1 (see above)
+    # Return payments applied only to this bill
+
+    @cust_pay_objs = $self->cust_bill_pay;
+
   }
 
   }
 
-  foreach my $obj (@objects) {
+  $self->set(
+    '_items_payments',
+    [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
+  );
+  return @{ $self->get('_items_payments') };
+}
+
+=sub _items_payments_total
+
+  Return a total of all records returned by _items_payments
+  Results vary based on invoicing conf flags
+
+=cut
+
+sub _items_payments_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_payments();
+  return $tot;
+}
+
+sub __items_payments_make_hashref {
+  # Transform a FS::cust_pay object into a simple hashref for invoice
+  my ($self, @cust_pay_objs) = @_;
+  my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+  my @return;
+
+  for my $obj (@cust_pay_objs) {
+
+    # In case we're passed FS::cust_bill_pay (or something else?)
+    # Below, we use $obj to render amount rather than $cust_apy.
+    #   If we were passed cust_bill_pay objs, then:
+    #   $obj->amount represents the amount applied to THIS invoice
+    #   $cust_pay->amount represents the total payment, which may have
+    #       been applied accross several invoices.
+    # If we were passed cust_bill_pay objects, then the conf flag
+    # previous_balance-payments_since is NOT set, so we should not
+    # present any payments not applied to this invoice.
     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
-    my $desc = $self->mt('Payment received').' '.
-               $self->time2str_local('short', $cust_pay->_date );
-    $desc .= $self->mt(' via ') .
-             $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
-      if $detailed;
-
-    push @b, {
-      'description' => $desc,
-      'amount'      => sprintf("%.2f", $obj->amount )
-    };
-  }
 
 
-  @b;
+    my %r_obj = (
+      _date   => $cust_pay->_date,
+      amount  => sprintf("%.2f", $obj->amount),
+      paynum  => $cust_pay->paynum,
+      payinfo => $cust_pay->payby_payinfo_pretty(),
+      description => join(' ',
+        $self->mt('Payment received'),
+        $self->time2str_local('short', $cust_pay->_date),
+      ),
+    );
 
 
-}
+    if ($c_invoice_payment_details) {
+      $r_obj{description} = join(' ',
+        $r_obj{description},
+        $self->mt('via'),
+        $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
+      );
+    }
+
+    push @return, \%r_obj;
+  }
+  return @return;
+}
+
+=sub _items_total()
+
+  Generate the line-items to be shown on the bill in the "Totals" section
+
+  Returns a list of hashrefs, each with the keys:
+  - total_item: description field
+  - total_amount: dollar-formatted number amount
+
+  Information presented by this method varies based on Conf
+
+  Conf previous_balance-payments_due
+  - default, flag not set
+      Only transactions that were applied to this bill bill be
+      displayed and calculated intothe total.  If items exist in
+      the past-due section, those items will disappear from this
+      invoice if they have been paid off.
+
+  - previous_balance-payments_due flag is set
+      Transactions occuring after the timestsamp of this
+      invoice are not reflected on invoice line items
+
+      Only payments/credits applied between the previous invoice
+      and this one are displayed and calculated into the total
+
+  - previous_balance-payments_due && $opt{template} eq 'statement'
+      Same as above, except payments/credits occuring before the date
+      of the following invoice are also displayed and calculated into
+      the total
+
+  Conf previous_balance-exclude_from_total
+  - default, flag not set
+      The "Totals" section contains a single line item.
+      The dollar amount of this line items is a sum of old and new charges
+  - previous_balance-exclude_from_total flag is set
+      The "Totals" section contains two line items.
+      One for previous balance, one for new charges
+  !NOTE: Avent virtualization flag 'disable_previous_balance' can
+      override the global conf flag previous_balance-exclude_from_total
+
+  Conf invoice_show_prior_due_date
+  - default, flag not set
+    Total line item in the "Totals" section does not mention due date
+  - invoice_show_prior_due_date flag is set
+    Total line item in the "Totals" section includes either the due
+    date of the invoice, or the specified invoice terms
+    ? Not sure why this is called "Prior" due date, since we seem to be
+      displaying THIS due date...
+=cut
 
 sub _items_total {
   my $self = shift;
   my $conf = $self->conf;
 
 
 sub _items_total {
   my $self = shift;
   my $conf = $self->conf;
 
-  my @items;
-  my ($pr_total) = $self->previous;
-  my ($previous_charges_desc, $new_charges_desc, $new_charges_amount);
+  my $c_multi_line_total = 0;
+  $c_multi_line_total    = 1
+    if $conf->exists('previous_balance-exclude_from_total')
+    && $self->enable_previous();
 
 
-  if ( $conf->exists('previous_balance-exclude_from_total') ) {
-    # if enabled, specifically add a line for the previous balance total
-    $previous_charges_desc = $self->mt(
-      $conf->config('previous_balance-text') || 'Previous Balance'
-    );
+  my @line_items;
+  my $invoice_charges  = $self->charged();
 
 
-    # then return separate lines for previous balance and total new charges
-    if ( $pr_total ) {
-      push @items,
-        { total_item    => $previous_charges_desc,
-          total_amount  => sprintf('%.2f',$pr_total)
-        };
-    }
-  }
+  # _items_previous() is aware of conf flags
+  my $previous_balance = 0;
+  $previous_balance += $_->{amount} for $self->_items_previous();
+
+  my $total_charges;
+  my $total_descr;
+
+  if ( $previous_balance && $c_multi_line_total ) {
+    # previous balance, new charges on separate lines
 
 
-  if (   $conf->exists('previous_balance-exclude_from_total')
-      or !$self->enable_previous ) {
-    # show new charges only
+    push @line_items, {
+      total_amount => sprintf('%.2f',$previous_balance),
+      total_item   => $self->mt(
+        $conf->config('previous_balance-text') || 'Previous Balance'
+      ),
+    };
 
 
-    $new_charges_desc = $self->mt(
+    $total_charges = $invoice_charges;
+    $total_descr   = $self->mt(
       $conf->config('previous_balance-text-total_new_charges')
       $conf->config('previous_balance-text-total_new_charges')
-       || 'Total New Charges'
+      || 'Total New Charges'
     );
 
     );
 
-    $new_charges_amount = $self->charged;
-
   } else {
   } else {
-    # show new charges + previous invoice total
-
-    $new_charges_desc = $self->mt('Total Charges');
-    if ( $self->enable_previous ) {
-      $new_charges_amount = sprintf('%.2f', $self->charged + $pr_total);
-    } else {
-      $new_charges_amount = sprintf('%.2f', $self->charged);
-    }
-
+    # previous balance and new charges combined into a single total line
+    $total_charges = $invoice_charges + $previous_balance;
+    $total_descr = $self->mt('Total Charges');
   }
 
   if ( $conf->exists('invoice_show_prior_due_date') ) {
     # then the due date should be shown with Total New Charges,
     # and should NOT be shown with the Balance Due message.
   }
 
   if ( $conf->exists('invoice_show_prior_due_date') ) {
     # then the due date should be shown with Total New Charges,
     # and should NOT be shown with the Balance Due message.
+
     if ( $self->due_date ) {
     if ( $self->due_date ) {
-      # localize the "Please pay by" message and the date itself
-      # (grammar issues with this, yeah)
-      $new_charges_desc .= ' - ' . $self->mt('Please pay by') . ' ' .
-                           $self->due_date2str('short');
+      $total_descr = join(' ',
+        $total_descr,
+        '-',
+        $self->mt('Please pay by'),
+        $self->due_date2str('short')
+      );
     } elsif ( $self->terms ) {
     } elsif ( $self->terms ) {
-      # phrases like "due on receipt" should be localized
-      $new_charges_desc .= ' - ' . $self->mt($self->terms);
+      $total_descr = join(' ',
+        $total_descr,
+        '-',
+        $self->mt($self->terms)
+      );
     }
   }
 
     }
   }
 
-  push @items,
-    { total_item    => $new_charges_desc,
-      total_amount  => $new_charges_amount,
-    };
+  push @line_items, {
+    total_amount => sprintf('%.2f', $total_charges),
+    total_item   => $total_descr,
+  };
 
 
-  @items;
+  return @line_items;
 }
 
 }
 
+=item _items_aging_balances
+
+  Returns an array of aged balance amounts from a given epoch timestamp.
+
+  The time of day is ignored for this calculation, so that slight differences
+  on the generation time of an invoice doesn't determine which column an
+  aged balance falls into.
+
+  Will not include any balances dated after the given timestamp in
+  the calculated totals
 
 
+  usage:
+  @aged_balances = $b->_items_aging_balances( $b->_date )
+
+  @aged_balances = (
+    under30d,
+    30d-60d,
+    60d-90d,
+    over90d
+  )
+
+=cut
+
+sub _items_aging_balances {
+  my ($self, $basetime) = @_;
+  die "Incorrect usage of _items_aging_balances()" unless ref $self;
+
+  $basetime = $self->_date unless $basetime;
+  my @aging_balances = (0, 0, 0, 0);
+  my @open_invoices = $self->_items_previous();
+  my $d30 = 2592000; # 60 * 60 * 24 * 30,
+  my $d60 = 5184000; # 60 * 60 * 24 * 60,
+  my $d90 = 7776000; # 60 * 60 * 24 * 90
+
+  # Move the clock back on our given day to 12:00:01 AM
+  my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
+  my $dt_12am = DateTime->new(
+    year   => $dt_basetime->year,
+    month  => $dt_basetime->month,
+    day    => $dt_basetime->day,
+    hour   => 0,
+    minute => 0,
+    second => 1,
+  )->epoch();
+
+  # set our epoch breakpoints
+  $_ = $dt_12am - $_ for $d30, $d60, $d90;
+
+  # grep the aged balances
+  for my $oinv (@open_invoices) {
+    if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
+      # If post invoice dated less than 30days ago
+      $aging_balances[0] += $oinv->{amount};
+    } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
+      # If past invoice dated between 30-60 days ago
+      $aging_balances[1] += $oinv->{amount};
+    } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
+      # If past invoice dated between 60-90 days ago
+      $aging_balances[2] += $oinv->{amount};
+    } else {
+      # If past invoice dated 90+ days ago
+      $aging_balances[3] += $oinv->{amount};
+    }
+  }
+
+  return map{ sprintf('%.2f',$_) } @aging_balances;
+}
 
 =item call_details [ OPTION => VALUE ... ]
 
 
 =item call_details [ OPTION => VALUE ... ]
 
@@ -2933,7 +3564,7 @@ sub call_details {
       my $row = shift;
 
       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
       my $row = shift;
 
       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
-      
+
     };
   }
 
     };
   }
 
@@ -3022,7 +3653,7 @@ sub process_re_X {
 
 }
 
 
 }
 
-# this is called from search/cust_bill.html and given all its search 
+# this is called from search/cust_bill.html and given all its search
 # parameters, so it needs to perform the same search.
 
 sub re_X {
 # parameters, so it needs to perform the same search.
 
 sub re_X {
@@ -3040,7 +3671,7 @@ sub re_X {
   delete $query->{'count_query'};
   delete $query->{'count_addl'};
 
   delete $query->{'count_query'};
   delete $query->{'count_addl'};
 
-  $query->{debug} = 1; # was in here before, is obviously useful  
+  $query->{debug} = 1; # was in here before, is obviously useful
 
   my @cust_bill = qsearch( $query );
 
 
   my @cust_bill = qsearch( $query );
 
@@ -3090,8 +3721,8 @@ Returns an SQL fragment to retreive the amount owed (charged minus credited and
 
 sub owed_sql {
   my ($class, $start, $end) = @_;
 
 sub owed_sql {
   my ($class, $start, $end) = @_;
-  'charged - '. 
-    $class->paid_sql($start, $end). ' - '. 
+  'charged - '.
+    $class->paid_sql($start, $end). ' - '.
     $class->credited_sql($start, $end);
 }
 
     $class->credited_sql($start, $end);
 }
 
@@ -3180,4 +3811,3 @@ documentation.
 =cut
 
 1;
 =cut
 
 1;
-
index 47bdbfb..249db9b 100644 (file)
@@ -9,29 +9,20 @@
       <table class="invoice_summary">
         <tr><th colspan=2><br></th></tr>
         <tr>
       <table class="invoice_summary">
         <tr><th colspan=2><br></th></tr>
         <tr>
-          <td><b><u><br>Summary of Previous Balance and Payments<br></u></b></td>
+          <td><b><u><br>Summary of Previous Balance<br></u></b></td>
           <td></td>
         </tr>
         <tr>
           <td><b>Previous Balance</b></td>
           <td></td>
         </tr>
         <tr>
           <td><b>Previous Balance</b></td>
-          <td align="right"><b><%= $dollar.$true_previous_balance %></b></td>
-        </tr>
-        <tr>
-          <td><b>Payments</b></td>
-          <th align="right"><b><%= $dollar.$balance_adjustments %></b></th>
-        </tr>
-        <tr>
-          <td><b>Balance Outstanding</b></td>
-          <td align="right"><b><%= $dollar.sprintf('%.2f', $true_previous_balance - $balance_adjustments) %></b></td>
+          <td align=right><b><%= "${dollar}${true_previous_balance}" %></b></td>
         </tr>
         <tr><th colspan=2><br></th></tr>
         <tr><td colspan=2><br></td></tr>
         <tr>
           <td><b><u>Summary of New Charges</u></b></td>
         </tr>
         <tr><th colspan=2><br></th></tr>
         <tr><td colspan=2><br></td></tr>
         <tr>
           <td><b><u>Summary of New Charges</u></b></td>
-          <td></td>
         </tr>
         <tr><td colspan=2><br></td></tr>
         </tr>
         <tr><td colspan=2><br></td></tr>
-        <%= 
+        <%=
           my $last = $summary_subtotals[-1];
           foreach my $section (@summary_subtotals) {
             $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
           my $last = $summary_subtotals[-1];
           foreach my $section (@summary_subtotals) {
             $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
           <td align="right"><b><%= $dollar.$current_less_finance %></b></td>
         </tr>
         <tr><th colspan=2><br></th></tr>
           <td align="right"><b><%= $dollar.$current_less_finance %></b></td>
         </tr>
         <tr><th colspan=2><br></th></tr>
+        <tr>
+          <td><b><u><br>Summary of Payments and Credits<br></u></b></td>
+          <td></td>
+        </tr>
+        <tr>
+          <td><b>Payments and Credits</b></td>
+          <td align="right"><b>-<%= $dollar.$balance_adjustments %></b></td>
+        </tr>
+        <tr><th colspan=2><br></th></tr>
         <tr><td colspan=2><br></td></tr>
         <tr>
           <td><b><u>Invoice Summary</u></b></td>
         <tr><td colspan=2><br></td></tr>
         <tr>
           <td><b><u>Invoice Summary</u></b></td>
-          <td></td>
         </tr>
         <tr><td colspan=2><br></td></tr>
         <tr>
           <td><b>Previous Past Due Charges</b></td>
         </tr>
         <tr><td colspan=2><br></td></tr>
         <tr>
           <td><b>Previous Past Due Charges</b></td>
-          <td align="right"><b><%= $dollar.sprintf('%.2f', $true_previous_balance - $balance_adjustments) %></b></td>
+          <td align="right"><b><%= $dollar.$true_previous_balance %></b></td>
         </tr>
         <tr>
           <td><b>Finance charges on overdue amount</b></td>
         </tr>
         <tr>
           <td><b>Finance charges on overdue amount</b></td>
         </tr>
         <tr>
           <td><b>New Charges</b></td>
         </tr>
         <tr>
           <td><b>New Charges</b></td>
-          <th align="right"><b><%= $dollar.$current_less_finance %></b></th>
+          <td align="right"><b><%= $dollar.$current_less_finance %></b></td>
         </tr>
         </tr>
-
-        <%= 
-          
-          #false laziness w/invoice_latexsummary and above
+        <%=
           foreach my $section ( grep $_->{adjust_section}, @sections) {
             $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
             $OUT .= qq(<th align="right"><b>). $section->{'subtotal'}. "</b></th></tr>";
           }
         %>
           foreach my $section ( grep $_->{adjust_section}, @sections) {
             $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
             $OUT .= qq(<th align="right"><b>). $section->{'subtotal'}. "</b></th></tr>";
           }
         %>
-
+        <tr>
+          <td><b>Payments and Credits</b></td>
+          <th align="right"><b>-<%= $dollar.sprintf('%.2f', $balance_adjustments) %></b></th>
+        </tr>
         <tr>
           <td><b>Total Amount Due</b></td>
           <td align="right"><b><%= $dollar.sprintf('%.2f', $balance) %></b></td>
         <tr>
           <td><b>Total Amount Due</b></td>
           <td align="right"><b><%= $dollar.sprintf('%.2f', $balance) %></b></td>