RT# 79636 Taxes per section when using invoice_sections
authorMitch Jackson <mitch@freeside.biz>
Thu, 29 Mar 2018 20:49:28 +0000 (20:49 +0000)
committerMitch Jackson <mitch@freeside.biz>
Thu, 29 Mar 2018 20:49:28 +0000 (20:49 +0000)
FS/FS/Conf.pm
FS/FS/Template_Mixin.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_tax_location.pm

index d02a026..ed94dd0 100644 (file)
@@ -1738,6 +1738,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'invoice_sections_with_taxes',
+    'section'     => 'invoicing',
+    'description' => 'Include taxes within each section of mutli-section invoices.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'summary_subtotals_method',
     'section'     => 'invoicing',
     'description' => 'How to group line items when calculating summary subtotals.  By default, it will be the same method used for grouping invoice sections.',
index ea753b7..99019e9 100644 (file)
@@ -938,6 +938,8 @@ sub print_generic {
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $self->has_sections;
   $invoice_data{'multisection'} = $multisection;
+  my $section_with_taxes = 1
+    if $conf->exists('invoice_sections_with_taxes');
   my $late_sections;
   my $extra_sections = [];
   my $extra_lines = ();
@@ -1200,6 +1202,9 @@ sub print_generic {
     warn "$me   searching for line items\n"
       if $DEBUG > 1;
 
+    my %section_tax_lines;
+    my %seen_tax_lines;
+
     foreach my $line_item ( $self->_items_pkg(%options),
                             $self->_items_fee(%options) ) {
 
@@ -1223,10 +1228,55 @@ sub print_generic {
         $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
       }
       $line_item->{'ext_description'} ||= [];
+
+      if ( $section_with_taxes && ref $line_item->{pkg_tax} ) {
+        for my $line_tax ( @{$ line_item->{pkg_tax} } ) {
+
+          # It is rarely possible for the same tax record to be presented here
+          # multiple times.  See cust_bill_pkg::_pkg_tax_list for more info
+          next if $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} };
+          $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} } = 1;
+
+          $section_tax_lines{ $line_tax->{taxname} } += $line_tax->{amount};
+        }
+      }
+
       push @detail_items, $line_item;
     }
 
+    # If conf flag invoice_sections_with_taxes:
+    # - Add @detail_items for taxes into each section
+    # - Update section subtotal to include taxes
+    if ( $section_with_taxes && %section_tax_lines ) {
+      for my $taxname ( keys %section_tax_lines ) {
+
+        push @detail_items, {
+          section => $section,
+          amount  => sprintf($money_char."%.2f",$section_tax_lines{$taxname}),
+          description => &$escape_function($taxname),
+        };
+
+        # Append taxes to total.  If line format resembles "$5.00 to $12.00"
+        # append to the second value.
+        if ($section->{subtotal} =~ /to/) {
+          my @subtotal = split /\s/, $section->{subtotal};
+          $subtotal[2] =~ s/[^\d\.]//g;
+          $subtotal[2] = sprintf(
+            $money_char."%.2f",
+            ( $subtotal[2] + $section_tax_lines{$taxname} )
+          );
+          $section->{subtotal} = join ' ', @subtotal;
+        } else {
+        $section->{subtotal} =~ s/[^\d\.]//g;
+          $section->{subtotal} = sprintf(
+            $money_char . "%.2f",
+            ( $section->{subtotal} + $section_tax_lines{$taxname} )
+          );
+        }
+
+      }
+    }
+
     if ( $section->{'description'} ) {
       push @buf, ( ['','-----------'],
                    [ $section->{'description'}. ' sub-total',
@@ -1327,11 +1377,19 @@ sub print_generic {
         $tax_section->{'description'} = $self->mt($tax_description);
         $tax_section->{'summarized'} = '';
 
-        # append it if it's not already there
-        if ( !grep $tax_section, @sections ) {
+        if ( $conf->exists('invoice_sections_with_taxes')) {
+
+          # remove tax section if taxes are itemized within other sections
+          @sections = grep{ $_ ne $tax_section } @sections;
+
+        } elsif ( !grep $tax_section, @sections ) {
+
+          # append it if it's not already there
           push @sections, $tax_section;
           push @summary_subtotals, $tax_section;
+
         }
+
       }
 
     } else {
@@ -3072,11 +3130,15 @@ sub _items_fee {
     my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
     # but not escape the base description line
 
+    my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+      if $self->conf->exists('invoice_sections_with_taxes');
+
     push @items,
       { feepart     => $cust_bill_pkg->feepart,
         amount      => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
         description => $desc,
-        ext_description => \@ext_desc
+        pkg_tax     => \@pkg_tax,
+        ext_description => \@ext_desc,
         # sdate/edate?
       };
   }
@@ -3177,6 +3239,8 @@ which does something complicated.
 preref_callback: coderef run for each line item, code should return HTML to be
 displayed before that line item (quotations only)
 
+section_with_taxes:  Look up and include applied taxes for each record
+
 Returns a list of hashrefs, each of which may contain:
 
 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and 
@@ -3312,6 +3376,9 @@ sub _items_cust_bill_pkg {
                           'no_usage'        => $opt{'no_usage'},
                         );
 
+      my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+        if $self->conf->exists('invoice_sections_with_taxes');
+
       if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
         # XXX this should be pulled out into quotation_pkg
 
@@ -3337,6 +3404,7 @@ sub _items_cust_bill_pkg {
             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
             'quantity'    => $cust_bill_pkg->quantity,
+            'pkg_tax'     => \@pkg_tax,
             'ext_description' => \@details,
             'preref_html' => ( $opt{preref_callback}
                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
@@ -3352,6 +3420,7 @@ sub _items_cust_bill_pkg {
             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
             'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
             'quantity'    => $cust_bill_pkg->quantity,
+            'pkg_tax'     => \@pkg_tax,
             'ext_description' => \@details,
            'preref_html'  => ( $opt{preref_callback}
                                  ? &{ $opt{preref_callback} }( $cust_bill_pkg )
@@ -3457,6 +3526,7 @@ sub _items_cust_bill_pkg {
               setup_show_zero => $cust_bill_pkg->setup_show_zero,
               unit_amount     => $cust_bill_pkg->unitsetup,
               quantity        => $cust_bill_pkg->quantity,
+              pkg_tax         => \@pkg_tax,
               ext_description => \@d,
               svc_label       => ($svc_label || ''),
               locationnum     => $cust_pkg->locationnum, # sure, why not?
@@ -3617,6 +3687,7 @@ sub _items_cust_bill_pkg {
                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
                 unit_amount     => $unit_amount,
                 quantity        => $cust_bill_pkg->quantity,
+                pkg_tax         => \@pkg_tax,
                 %item_dates,
                 ext_description => \@d,
                 svc_label       => ($svc_label || ''),
@@ -3645,6 +3716,7 @@ sub _items_cust_bill_pkg {
                 amount          => $amount,
                 usage_item      => 1,
                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
+                pkg_tax         => \@pkg_tax,
                 %item_dates,
                 ext_description => \@d,
                 locationnum     => $cust_pkg->locationnum,
index 284ab3c..6f93d2d 100644 (file)
@@ -1804,6 +1804,70 @@ sub upgrade_tax_location {
   '';
 }
 
+sub _pkg_tax_list {
+  # Return an array of hashrefs for each cust_bill_pkg_tax_location
+  # applied to this bill for this cust_bill_pkg.pkgnum.
+  #
+  # ! Important Note:
+  #   In some situations, this list will contain more tax records than the
+  #   ones directly related to $self->billpkgnum.  The returned list contains
+  #   all records, for this bill, charged against this billpkgnum's pkgnum.
+  #
+  #   One must keep this in mind when using data returned by this method.
+  #
+  #   An unaddressed deficiency in the cust_bill_pkg_tax_location model makes
+  #   this necessary:  When a linked-hidden package generates a tax/fee as a row
+  #   in cust_bill_pkg_tax_location, there is not enough information to surmise
+  #   with specificity which billpkgnum row represents the direct parent of the
+  #   the linked-hidden package's tax row.  The closest we can get to this
+  #   backwards reassociation is to use the pkgnum.  Therefore, when multiple
+  #   billpkgnum's appear with the same pkgnum, this method is going to return
+  #   the tax records for ALL of those billpkgnum's, not just $self->billpkgnum.
+  #
+  #   This could be addressed with an update to the model, and to the billing
+  #   routine that generates rows into cust_bill_pkg_tax_location.  Perhaps a
+  #   column, link_billpkgnum or parent_billpkgnum, recording the link. I'm not
+  #   doing that now, because there would be no possible repair of data stored
+  #   historically prior to such a fix.  I need _pkg_tax_list() to not be
+  #   broken for already-generated bills.
+  #
+  #   Any code you write relying on _pkg_tax_list() MUST be aware of, and
+  #   account for, the possible return of duplicated tax records returned
+  #   when method is called on multiple cust_bill_pkg_tax_location rows.
+  #   Duplicates can be identified by billpkgtaxlocationnum column.
+
+  my $self = shift;
+  return unless $self->pkgnum;
+
+  map +{
+      billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
+      billpkgnum            => $_->billpkgnum,
+      taxnum                => $_->taxnum,
+      amount                => $_->amount,
+      taxname               => $_->taxname,
+  },
+  qsearch({
+    table  => 'cust_bill_pkg_tax_location',
+    addl_from => '
+      LEFT JOIN cust_bill_pkg
+             ON cust_bill_pkg.billpkgnum
+         = cust_bill_pkg_tax_location.taxable_billpkgnum
+    ',
+    select => join( ', ', (qw|
+      cust_bill_pkg.billpkgnum
+      cust_bill_pkg_tax_location.billpkgtaxlocationnum
+      cust_bill_pkg_tax_location.taxnum
+      cust_bill_pkg_tax_location.amount
+    |)),
+    extra_sql =>
+      ' WHERE '.
+      ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
+      ' AND '.
+      ' cust_bill_pkg_tax_location.pkgnum = ' . dbh->quote( $self->pkgnum ),
+  });
+
+}
+
 sub _upgrade_data {
   # Create a queue job to run upgrade_tax_location from January 1, 2012 to 
   # the present date.
@@ -1862,4 +1926,3 @@ from the base documentation.
 =cut
 
 1;
-
index 1717654..4da354a 100644 (file)
@@ -160,6 +160,19 @@ sub cust_location {
   qsearchs( 'cust_location', { 'locationnum' => $self->locationnum }  );
 }
 
+=item taxname
+
+Returns the tax name (for populating the itemdesc field).
+
+=cut
+
+sub taxname {
+  my $self = shift;
+  my $cust_main_county = FS::cust_main_county->by_key($self->taxnum)
+    or return '';
+  $cust_main_county->taxname || 'Tax';
+}
+
 =item desc
 
 Returns a description for this tax line item constituent.  Currently this
@@ -495,4 +508,3 @@ L<FS::Record>, schema.html from the base documentation.
 =cut
 
 1;
-