summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorMitch Jackson <mitch@freeside.biz>2018-03-29 20:49:28 +0000
committerMitch Jackson <mitch@freeside.biz>2018-03-29 20:49:28 +0000
commit0af65f4c957f0f658d9a539effd5d61375f6a0b9 (patch)
tree34bdf20b685f00cc490abc26bbd0bcb8f6178a77 /FS
parentb91b856f04102ef11918f20171614638882930a9 (diff)
RT# 79636 Taxes per section when using invoice_sections
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Conf.pm7
-rw-r--r--FS/FS/Template_Mixin.pm80
-rw-r--r--FS/FS/cust_bill_pkg.pm65
-rw-r--r--FS/FS/cust_bill_pkg_tax_location.pm14
4 files changed, 160 insertions, 6 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index d02a02608..ed94dd0e5 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -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.',
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index ea753b7ab..99019e91e 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -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,
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
index 284ab3c81..6f93d2d90 100644
--- a/FS/FS/cust_bill_pkg.pm
+++ b/FS/FS/cust_bill_pkg.pm
@@ -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;
-
diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm
index 1717654a2..4da354a6c 100644
--- a/FS/FS/cust_bill_pkg_tax_location.pm
+++ b/FS/FS/cust_bill_pkg_tax_location.pm
@@ -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;
-