From b5f68a61dad0e3d00bab85716dc97bc186f55e48 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 16 Jun 2016 18:17:20 -0700 Subject: [PATCH] fix sales tax rounding in some edge cases, #42263, from #39487 --- FS/FS/TaxEngine/internal.pm | 16 +++++----- FS/t/suite/08-sales_tax.t | 76 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) create mode 100755 FS/t/suite/08-sales_tax.t diff --git a/FS/FS/TaxEngine/internal.pm b/FS/FS/TaxEngine/internal.pm index a9b32d133..db7010c18 100644 --- a/FS/FS/TaxEngine/internal.pm +++ b/FS/FS/TaxEngine/internal.pm @@ -66,7 +66,7 @@ sub taxline { my $taxnum = $tax_object->taxnum; my $exemptions = $self->{exemptions}->{$taxnum} ||= []; - my $taxable_cents = 0; + my $taxable_total = 0; my $tax_cents = 0; my $round_per_line_item = $conf->exists('tax-round_per_line_item'); @@ -302,15 +302,17 @@ sub taxline { }); push @tax_links, $location; - $taxable_cents += $taxable_charged; + $taxable_total += $taxable_charged; $tax_cents += $this_tax_cents; } #foreach $cust_bill_pkg - # calculate tax and rounding error for the whole group - my $extra_cents = sprintf('%.2f', $taxable_cents * $tax_object->tax / 100) - * 100 - $tax_cents; - # make sure we have an integer - $extra_cents = sprintf('%.0f', $extra_cents); + # calculate tax and rounding error for the whole group: total taxable + # amount times tax rate (as cents per dollar), minus the tax already + # charged + # and force 0.5 to round up + my $extra_cents = sprintf('%.0f', + ($taxable_total * $tax_object->tax) - $tax_cents + 0.00000001 + ); # if we're rounding per item, then ignore that and don't distribute any # extra cents. diff --git a/FS/t/suite/08-sales_tax.t b/FS/t/suite/08-sales_tax.t new file mode 100755 index 000000000..bf1ae48c8 --- /dev/null +++ b/FS/t/suite/08-sales_tax.t @@ -0,0 +1,76 @@ +#!/usr/bin/perl + +=head2 DESCRIPTION + +Tests basic sales tax calculations, including consolidation and rounding. +The invoice will have two charges that add up to $50 and two taxes: +- Tax 1, 8.25%, for $4.125 in tax, which will round up. +- Tax 2, 8.245%, for $4.1225 in tax, which will round down. + +Correct: The invoice will have one line item for each of those taxes, with +the correct amount. + +=cut + +use strict; +use Test::More tests => 2; +use FS::Test; +use Date::Parse 'str2time'; +use Date::Format 'time2str'; +use Test::MockTime qw(set_fixed_time); +use FS::cust_main; +use FS::cust_pkg; +use FS::Conf; +my $FS= FS::Test->new; + +# test configuration +my @taxes = ( + [ 'Tax 1', 8.250, 4.13 ], + [ 'Tax 2', 8.245, 4.12 ], +); + +# Create the customer and charge them +my $cust = $FS->new_customer('Basic taxes'); +$cust->bill_location->state('AZ'); # move it away from the default of CA +my $error; +$error = $cust->insert; +BAIL_OUT("can't create test customer: $error") if $error; +$error = $cust->charge( { + amount => 25.00, + pkg => 'Test charge 1', +} ) || +$cust->charge({ + amount => 25.00, + pkg => 'Test charge 2', +}); +BAIL_OUT("can't create test charges: $error") if $error; + +# Create tax defs +foreach my $tax (@taxes) { + my $cust_main_county = FS::cust_main_county->new({ + 'country' => 'US', + 'state' => 'AZ', + 'exempt_amount' => 0.00, + 'taxname' => $tax->[0], + 'tax' => $tax->[1], + }); + $error = $cust_main_county->insert; + BAIL_OUT("can't create tax definitions: $error") if $error; +} + +# Bill the customer +set_fixed_time(str2time('2016-03-10 08:00')); +my @return; +$error = $cust->bill( return_bill => \@return ); +BAIL_OUT("can't bill charges: $error") if $error; +my $cust_bill = $return[0] or BAIL_OUT("no invoice generated"); +# Check amounts +diag("Tax on 25.00 + 25.00"); +foreach my $cust_bill_pkg ($cust_bill->cust_bill_pkg) { + next if $cust_bill_pkg->pkgnum; + my ($tax) = grep { $_->[0] eq $cust_bill_pkg->itemdesc } @taxes; + if ( $tax ) { + ok ( $cust_bill_pkg->setup eq $tax->[2], "Tax at rate $tax->[1]% = $tax->[2]") + or diag("is ". $cust_bill_pkg->setup); + } +} -- 2.11.0