summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2015-05-30 15:12:07 -0700
committerMark Wells <mark@freeside.biz>2015-05-30 15:12:07 -0700
commit817c1ce0e1cbcfd1f684222c66f46dd13b2d6dd7 (patch)
tree25fd80fae19bbe1b4ec2c892a35a631cf232d590 /FS
parent3846acae1c2a7ecb275e400cf3802ada6bc89ed2 (diff)
SureTax, #31639, #33015, #34598
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Conf.pm63
-rw-r--r--FS/FS/Cursor.pm32
-rw-r--r--FS/FS/TaxEngine.pm89
-rw-r--r--FS/FS/TaxEngine/cch.pm60
-rw-r--r--FS/FS/TaxEngine/internal.pm19
-rw-r--r--FS/FS/TaxEngine/suretax.pm421
-rw-r--r--FS/FS/Upgrade.pm8
-rw-r--r--FS/FS/cust_bill_pkg.pm2
-rw-r--r--FS/FS/cust_bill_pkg_tax_location.pm13
-rw-r--r--FS/FS/cust_bill_pkg_tax_rate_location.pm14
-rw-r--r--FS/FS/cust_credit.pm2
-rw-r--r--FS/FS/cust_main/Billing.pm2
-rw-r--r--FS/FS/cust_main/Billing_Discount.pm3
-rw-r--r--FS/FS/part_event/Action/fee.pm2
-rw-r--r--FS/FS/part_fee.pm5
-rw-r--r--FS/FS/tax_rate.pm38
-rw-r--r--FS/FS/tax_status.pm6
17 files changed, 633 insertions, 146 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index f80f2d55f..17a7c23ec 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2451,55 +2451,84 @@ and customer address. Include units.',
{
'key' => 'enable_taxclasses',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'Enable per-package tax classes',
'type' => 'checkbox',
},
{
'key' => 'require_taxclasses',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'Require a taxclass to be entered for every package',
'type' => 'checkbox',
},
{
- 'key' => 'enable_taxproducts',
- 'section' => 'billing',
+ 'key' => 'tax_data_vendor',
+ 'section' => 'taxation',
'description' => 'Tax data vendor you are using.',
'type' => 'select',
- 'select_enum' => [ 'cch', 'billsoft', 'avalara' ],
+ 'select_enum' => [ '', 'cch', 'billsoft', 'avalara', 'suretax' ],
},
{
'key' => 'taxdatadirectdownload',
- 'section' => 'billing', #well
- 'description' => 'Enable downloading tax data directly from the vendor site. at least three lines: URL, username, and password.j',
+ 'section' => 'taxation',
+ 'description' => 'Enable downloading tax data directly from CCH. at least three lines: URL, username, and password.j',
'type' => 'textarea',
},
{
'key' => 'ignore_incalculable_taxes',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'Prefer to invoice without tax over not billing at all',
'type' => 'checkbox',
},
{
'key' => 'billsoft-company_code',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'Billsoft tax service company code (3 letters)',
'type' => 'text',
},
{
'key' => 'avalara-taxconfig',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'Avalara tax service configuration. Four lines: company code, account number, license key, test mode (1 to enable).',
'type' => 'textarea',
},
{
+ 'key' => 'suretax-client_number',
+ 'section' => 'taxation',
+ 'description' => 'SureTax tax service client ID.',
+ 'type' => 'text',
+ },
+ {
+ 'key' => 'suretax-validation_key',
+ 'section' => 'taxation',
+ 'description' => 'SureTax validation key (UUID).',
+ 'type' => 'text',
+ },
+ {
+ 'key' => 'suretax-business_unit',
+ 'section' => 'taxation',
+ 'description' => 'SureTax client business unit name; optional.',
+ 'type' => 'text',
+ 'per_agent' => 1,
+ },
+ {
+ 'key' => 'suretax-regulatory_code',
+ 'section' => 'taxation',
+ 'description' => 'SureTax client regulatory status.',
+ 'type' => 'select',
+ 'select_enum' => [ '', 'ILEC', 'IXC', 'CLEC', 'VOIP', 'ISP', 'Wireless' ],
+ 'per_agent' => 1,
+ },
+
+
+ {
'key' => 'welcome_msgnum',
'section' => 'notification',
'description' => 'Template to use for welcome messages when a svc_acct record is created.',
@@ -3678,14 +3707,14 @@ and customer address. Include units.',
{
'key' => 'tax-ship_address',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'By default, tax calculations are done based on the billing address. Enable this switch to calculate tax based on the shipping address instead.',
'type' => 'checkbox',
}
,
{
'key' => 'tax-pkg_address',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'By default, tax calculations are done based on the billing address. Enable this switch to calculate tax based on the package address instead (when present).',
'type' => 'checkbox',
},
@@ -4467,7 +4496,7 @@ and customer address. Include units.',
{
'key' => 'tax_district_method',
- 'section' => 'UI',
+ 'section' => 'taxation',
'description' => 'The method to use to look up tax district codes.',
'type' => 'select',
#'select_hash' => [ FS::Misc::Geo::get_district_methods() ],
@@ -5228,7 +5257,7 @@ and customer address. Include units.',
{
'key' => 'tax-cust_exempt-groups',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'List of grouping possibilities for tax names, for per-customer exemption purposes, one tax name per line. For example, "GST" would indicate the ability to exempt customers individually from taxes named "GST" (but not other taxes).',
'type' => 'textarea',
},
@@ -5242,7 +5271,7 @@ and customer address. Include units.',
{
'key' => 'tax-cust_exempt-groups-num_req',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'When using tax-cust_exempt-groups, control whether individual tax exemption numbers are required for exemption from different taxes.',
'type' => 'select',
'select_hash' => [ '' => 'Not required',
@@ -5270,7 +5299,7 @@ and customer address. Include units.',
{
'key' => 'enable_tax_adjustments',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'Enable the ability to add manual tax adjustments.',
'type' => 'checkbox',
},
@@ -5723,7 +5752,7 @@ and customer address. Include units.',
{
'key' => 'cust_class-tax_exempt',
- 'section' => 'billing',
+ 'section' => 'taxation',
'description' => 'Control the tax exemption flag per customer class rather than per indivual customer.',
'type' => 'checkbox',
},
diff --git a/FS/FS/Cursor.pm b/FS/FS/Cursor.pm
index 67a98eab4..faa15f9f6 100644
--- a/FS/FS/Cursor.pm
+++ b/FS/FS/Cursor.pm
@@ -4,7 +4,7 @@ use strict;
use vars qw($DEBUG $buffer);
use FS::Record;
use FS::UID qw(myconnect driver_name);
-use Scalar::Util qw(refaddr);
+use Scalar::Util qw(refaddr blessed);
$DEBUG = 2;
@@ -29,17 +29,24 @@ while ( my $row = $search->fetch ) {
=over 4
-=item new ARGUMENTS
+=item new ARGUMENTS [, DBH ]
Constructs a cursored search. Accepts all the same arguments as qsearch,
and returns an FS::Cursor object to fetch the rows one at a time.
+DBH may be a database handle; if so, the cursor will be created on that
+connection and have all of its transaction state. Otherwise a new connection
+will be opened for the cursor.
+
=cut
sub new {
my $class = shift;
- my $q = FS::Record::_query(@_); # builds the statement and parameter list
my $dbh;
+ if ( blessed($_[-1]) and $_[-1]->isa('DBI::db') ) {
+ $dbh = pop;
+ }
+ my $q = FS::Record::_query(@_); # builds the statement and parameter list
my $self = {
query => $q,
@@ -59,7 +66,11 @@ sub new {
my $statement;
if ( driver_name() eq 'Pg' ) {
- $self->{dbh} = $dbh = myconnect();
+ if (!$dbh) {
+ $dbh = myconnect();
+ $self->{autoclean} = 1;
+ }
+ $self->{dbh} = $dbh;
$statement = "DECLARE ".$self->{id}." CURSOR FOR ".$q->{statement};
} elsif ( driver_name() eq 'mysql' ) {
# build a cursor from scratch
@@ -144,8 +155,11 @@ sub DESTROY {
return unless $self->{pid} eq $$;
$self->{dbh}->do('CLOSE '. $self->{id})
or die $self->{dbh}->errstr; # clean-up the cursor in Pg
- $self->{dbh}->rollback;
- $self->{dbh}->disconnect;
+ if ($self->{autoclean}) {
+ # the dbh was created just for this cursor, so it has no transaction
+ # state that we care about
+ $self->{dbh}->rollback;
+ }
}
=back
@@ -159,12 +173,6 @@ Replace all uses of qsearch with this.
Still doesn't really support MySQL, but it pretends it does, by simply
running the query and returning records one at a time.
-The cursor will close prematurely if any code issues a rollback/commit. If
-you need protection against this use qsearch or fork and get a new dbh
-handle.
-Normally this issue will represent itself this message.
-ERROR: cursor "cursorXXXXXXX" does not exist.
-
=head1 SEE ALSO
L<FS::Record>
diff --git a/FS/FS/TaxEngine.pm b/FS/FS/TaxEngine.pm
index ac30eb1fc..45601429a 100644
--- a/FS/FS/TaxEngine.pm
+++ b/FS/FS/TaxEngine.pm
@@ -35,10 +35,28 @@ FS::TaxEngine - Base class for tax calculation engines.
=over 4
+=item class
+
+Returns the class name for tax engines, according to the 'tax_data_vendor'
+configuration setting.
+
+=cut
+
+sub class {
+ my $conf = FS::Conf->new;
+ my $subclass = $conf->config('tax_data_vendor') || 'internal';
+ my $class = "FS::TaxEngine::$subclass";
+ local $@;
+ eval "use $class";
+ die "couldn't load $class: $@\n" if $@;
+
+ $class;
+}
+
=item new 'cust_main' => CUST_MAIN, 'invoice_time' => TIME, OPTIONS...
Creates an L<FS::TaxEngine> object. The subclass will be chosen by the
-'enable_taxproducts' configuration setting.
+'tax_data_vendor' configuration setting.
CUST_MAIN and TIME are required. OPTIONS can include:
@@ -54,11 +72,7 @@ sub new {
my %opt = @_;
my $conf = FS::Conf->new;
if ($class eq 'FS::TaxEngine') {
- my $subclass = $conf->config('enable_taxproducts') || 'internal';
- $class .= "::$subclass";
- local $@;
- eval "use $class";
- die "couldn't load $class: $@\n" if $@;
+ $class = $class->class;
}
my $self = { items => [], taxes => {}, conf => $conf, %opt };
bless $self, $class;
@@ -107,6 +121,11 @@ sub calculate_taxes {
my $cust_bill = shift;
my @raw_taxlines = $self->make_taxlines($cust_bill);
+ if ( !@raw_taxlines ) {
+ return;
+ } elsif ( !ref $raw_taxlines[0] ) { # error message
+ return $raw_taxlines[0];
+ }
my @real_taxlines = $self->consolidate_taxlines(@raw_taxlines);
@@ -117,12 +136,13 @@ sub calculate_taxes {
}
sub make_taxlines {
+ # only used by FS::TaxEngine::internal; should just move there
my $self = shift;
my $conf = $self->{conf};
my $cust_bill = shift;
- my @taxlines;
+ my @raw_taxlines;
# For each distinct tax rate definition, calculate the tax and exemptions.
foreach my $taxnum ( keys %{ $self->{taxes} } ) {
@@ -134,16 +154,17 @@ sub make_taxlines {
# the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
# (setup, recurring, usage classes)
- my $taxline = $self->taxline('tax' => $tax_object, 'sales' => $taxables);
- # taxline methods are now required to return real line items
- # with their link records
- die $taxline unless ref($taxline);
+ my @taxlines = $self->taxline('tax' => $tax_object, 'sales' => $taxables);
+ # taxline methods are now required to return the link records alone.
+ # Consolidation will take care of the rest.
+ next if !@taxlines;
+ die $taxlines[0] unless ref($taxlines[0]);
- push @taxlines, $taxline;
+ push @raw_taxlines, @taxlines;
} #foreach $taxnum
- return @taxlines;
+ return @raw_taxlines;
}
sub consolidate_taxlines {
@@ -152,14 +173,16 @@ sub consolidate_taxlines {
my $conf = $self->{conf};
my @raw_taxlines = @_;
+ return if !@raw_taxlines; # shouldn't even be here
+
my @tax_line_items;
# keys are tax names (as printed on invoices / itemdesc )
- # values are arrayrefs of taxlines
+ # values are arrayrefs of tax links ("raw taxlines")
my %taxname;
# collate these by itemdesc
foreach my $taxline (@raw_taxlines) {
- my $taxname = $taxline->itemdesc;
+ my $taxname = $taxline->taxname;
$taxname{$taxname} ||= [];
push @{ $taxname{$taxname} }, $taxline;
}
@@ -168,7 +191,7 @@ sub consolidate_taxlines {
# values are (cumulative) amounts
my %tax_amount;
- my $link_table = $self->info->{link_table};
+ my $link_table = $raw_taxlines[0]->table;
# Preconstruct cust_bill_pkg objects that will become the "final"
# taxlines for each name, so that we can reference them.
@@ -187,32 +210,30 @@ sub consolidate_taxlines {
# create a consolidated tax item with the total amount and all the links
# of all tax items that share that name.
foreach my $taxname ( keys %taxname ) {
- my @tax_links;
+ my $tax_links = $taxname{$taxname};
my $tax_cust_bill_pkg = $real_taxline_named{$taxname};
- $tax_cust_bill_pkg->set( $link_table => \@tax_links );
+ $tax_cust_bill_pkg->set( $link_table => $tax_links );
my $tax_total = 0;
warn "adding $taxname\n" if $DEBUG > 1;
- foreach my $taxitem ( @{ $taxname{$taxname} } ) {
+ foreach my $link ( @$tax_links ) {
# then we need to transfer the amount and the links from the
# line item to the new one we're creating.
- $tax_total += $taxitem->setup;
- foreach my $link ( @{ $taxitem->get($link_table) } ) {
- $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
-
- # if the link represents tax on tax, also fix its taxable pointer
- # to point to the "final" taxline
- my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
- if (my $other_taxname = $taxable_cust_bill_pkg->itemdesc) {
- $link->set('taxable_cust_bill_pkg',
- $real_taxline_named{$other_taxname}
- );
- }
-
- push @tax_links, $link;
+ $tax_total += $link->amount;
+ $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
+
+ # if the link represents tax on tax, also fix its taxable pointer
+ # to point to the "final" taxline
+ my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
+ if ( $taxable_cust_bill_pkg and
+ my $other_taxname = $taxable_cust_bill_pkg->itemdesc) {
+ $link->set('taxable_cust_bill_pkg',
+ $real_taxline_named{$other_taxname}
+ );
}
- } # foreach $taxitem
+
+ } # foreach $link
next unless $tax_total;
# we should really neverround this up...I guess it's okay if taxline
diff --git a/FS/FS/TaxEngine/cch.pm b/FS/FS/TaxEngine/cch.pm
index 4e6dbaf7e..fb3410365 100644
--- a/FS/FS/TaxEngine/cch.pm
+++ b/FS/FS/TaxEngine/cch.pm
@@ -5,6 +5,7 @@ use vars qw( $DEBUG );
use base 'FS::TaxEngine';
use FS::Record qw(dbh qsearch qsearchs);
use FS::Conf;
+use List::Util qw(sum);
=head1 SUMMARY
@@ -131,32 +132,21 @@ sub make_taxlines {
$taxable_location{ $_->billpkgnum } ||= $_->tax_location;
}
- my @taxlines = $tax_rate->taxline_cch( $taxables, $charge_classes );
-
- next if !@taxlines;
- if (!ref $taxlines[0]) {
+ foreach my $link ( $tax_rate->taxline_cch( $taxables, $charge_classes ) ) {
+ if (!ref $link) {
# it's an error string
- warn "error evaluating tax#$taxnum\n";
- return $taxlines[0];
- }
-
- my $billpkgnum = -1; # the current one
- my $fragments; # $item_has_tax{$billpkgnum}{taxnum}
-
- foreach my $taxline (@taxlines) {
- next if $taxline->setup == 0;
+ die "error evaluating tax#$taxnum: $link\n";
+ }
+ next if $link->amount == 0;
- my $link = $taxline->get('cust_bill_pkg_tax_rate_location')->[0];
# store this tax fragment, indexed by taxable item, then by taxnum
- if ( $billpkgnum != $link->taxable_billpkgnum ) {
- $billpkgnum = $link->taxable_billpkgnum;
- $item_has_tax{$billpkgnum} ||= {};
- $fragments = $item_has_tax{$billpkgnum}{$taxnum} ||= [];
- }
+ my $billpkgnum = $link->taxable_billpkgnum;
+ $item_has_tax{$billpkgnum} ||= {};
+ my $fragments = $item_has_tax{$billpkgnum}{$taxnum} ||= [];
- $taxline->set('invnum', $cust_bill->invnum);
- push @$fragments, $taxline; # so we can ToT it
- push @raw_taxlines, $taxline; # so we actually bill it
+ push @raw_taxlines, $link; # this will go into final consolidation
+ push @$fragments, $link; # this will go into a temporary cust_bill_pkg
+ # for ToT calculation
}
} # foreach $taxnum
@@ -167,6 +157,9 @@ sub make_taxlines {
my $this_has_tax = $item_has_tax{$billpkgnum};
my $location = $taxable_location{$billpkgnum};
foreach my $taxnum (keys %$this_has_tax) {
+ # $this_has_tax->{$taxnum} = an arrayref of the tax links for taxdef
+ # $taxnum on taxable item $billpkgnum
+
my $tax_rate = FS::tax_rate->by_key($taxnum);
# find all taxes that apply to it in this location
my @tot = $tax_rate->tax_on_tax( $location );
@@ -177,6 +170,7 @@ sub make_taxlines {
# Calculate ToT separately for each taxable item, and only if _that
# item_ is already taxed under the ToT. This is counterintuitive.
# See RT#5243.
+ my $temp_lineitem;
foreach my $tot (@tot) {
my $totnum = $tot->taxnum;
warn "checking taxnum ".$tot->taxnum.
@@ -185,16 +179,22 @@ sub make_taxlines {
if ( exists $this_has_tax->{ $totnum } ) {
warn "calculating tax on tax: taxnum ".$tot->taxnum." on $taxnum\n"
if $DEBUG;
- my @taxlines = $tot->taxline_cch(
- $this_has_tax->{ $taxnum }, # the first-stage tax (in an arrayref)
- );
- next if (!@taxlines); # it didn't apply after all
- if (!ref($taxlines[0])) {
- warn "error evaluating TOT ($totnum on $taxnum)\n";
- return $taxlines[0];
+ # construct a line item to calculate tax on
+ $temp_lineitem ||= FS::cust_bill_pkg->new({
+ 'pkgnum' => 0,
+ 'invnum' => $cust_bill->invnum,
+ 'setup' => sum(map $_->amount, @{ $this_has_tax->{$taxnum} }),
+ 'recur' => 0,
+ 'itemdesc' => $tax_rate->taxname,
+ 'cust_bill_pkg_tax_rate_location' => $this_has_tax->{$taxnum},
+ });
+ my @new_taxlines = $tot->taxline_cch( [ $temp_lineitem ] );
+ next if (!@new_taxlines); # it didn't apply after all
+ if (!ref($new_taxlines[0])) {
+ die "error evaluating TOT ($totnum on $taxnum): $new_taxlines[0]\n";
}
# add these to the taxline queue
- push @raw_taxlines, @taxlines;
+ push @raw_taxlines, @new_taxlines;
} # if $this_has_tax->{$totnum}
} # foreach my $tot (tax-on-tax rate definition)
} # foreach $taxnum (first-tier rate definition)
diff --git a/FS/FS/TaxEngine/internal.pm b/FS/FS/TaxEngine/internal.pm
index 99535ad38..f45bc0801 100644
--- a/FS/FS/TaxEngine/internal.pm
+++ b/FS/FS/TaxEngine/internal.pm
@@ -60,7 +60,6 @@ sub taxline {
my $taxnum = $tax_object->taxnum;
my $exemptions = $self->{exemptions}->{$taxnum} ||= [];
- my $name = $tax_object->taxname || 'Tax';
my $taxable_cents = 0;
my $tax_cents = 0;
@@ -87,14 +86,7 @@ sub taxline {
push @existing_exemptions, @{ $_->cust_tax_exempt_pkg }
foreach @$taxables;
- my $tax_item = FS::cust_bill_pkg->new({
- 'pkgnum' => 0,
- 'recur' => 0,
- 'sdate' => '',
- 'edate' => '',
- 'itemdesc' => $name,
- });
- my @tax_location;
+ my @tax_links;
foreach my $cust_bill_pkg (@$taxables) {
@@ -274,9 +266,8 @@ sub taxline {
'pkgnum' => $cust_bill_pkg->pkgnum,
'locationnum' => $cust_bill_pkg->cust_pkg->tax_locationnum,
'taxable_cust_bill_pkg' => $cust_bill_pkg,
- 'tax_cust_bill_pkg' => $tax_item,
});
- push @tax_location, $location;
+ push @tax_links, $location;
$taxable_cents += $taxable_charged;
$tax_cents += $this_tax_cents;
@@ -292,7 +283,7 @@ sub taxline {
}
$tax_cents += $extra_cents;
my $i = 0;
- foreach (@tax_location) { # can never require more than a single pass, yes?
+ foreach (@tax_links) { # can never require more than a single pass, yes?
my $cents = $_->get('cents');
if ( $extra_cents > 0 ) {
$cents++;
@@ -300,10 +291,8 @@ sub taxline {
}
$_->set('amount', sprintf('%.2f', $cents/100));
}
- $tax_item->set('setup' => sprintf('%.2f', $tax_cents / 100));
- $tax_item->set('cust_bill_pkg_tax_location', \@tax_location);
- return $tax_item;
+ return @tax_links;
}
sub info {
diff --git a/FS/FS/TaxEngine/suretax.pm b/FS/FS/TaxEngine/suretax.pm
new file mode 100644
index 000000000..327a72843
--- /dev/null
+++ b/FS/FS/TaxEngine/suretax.pm
@@ -0,0 +1,421 @@
+package FS::TaxEngine::suretax;
+
+use strict;
+use base 'FS::TaxEngine';
+use FS::Conf;
+use FS::Record qw(qsearch qsearchs dbh);
+use JSON;
+use XML::Simple qw(XMLin);
+use LWP::UserAgent;
+use HTTP::Request::Common;
+use DateTime;
+
+our $DEBUG = 1; # prints progress messages
+# $DEBUG = 2; # prints decoded request and response (noisy, be careful)
+# $DEBUG = 3; # prints raw response from the API, ridiculously unreadable
+
+our $json = JSON->new->pretty(1);
+
+our %taxproduct_cache;
+
+our $conf;
+
+our $host = 'testapi.taxrating.net';
+# production: 'api.taxrating.net'
+
+FS::UID->install_callback( sub {
+ $conf = FS::Conf->new;
+ # should we enable conf caching here?
+});
+
+# Tax Situs Rules, for determining tax jurisdiction.
+# (may need to be configurable)
+
+# For PSTN calls, use Rule 01, two-out-of-three using NPA-NXX. (The "three"
+# are source number, destination number, and charged party number.)
+our $TSR_CALL_NPANXX = '01';
+
+# For other types of calls (on-network hosted PBX, SIP-addressed calls,
+# other things that don't have an NPA-NXX number), use Rule 11. (See below.)
+our $TSR_CALL_OTHER = '11';
+
+# For regular recurring or one-time charges, use Rule 11. This uses the
+# service zip code for transaction types that are known to require it, and
+# the billing zip code for all other transaction types.
+our $TSR_GENERAL = '11';
+
+# XXX incomplete; doesn't handle international taxes (Rule 14) or point
+# to point private lines (Rule 07).
+
+our %REGCODE = ( # can be selected per agent
+ '' => '99',
+ 'ILEC' => '00',
+ 'IXC' => '01',
+ 'CLEC' => '02',
+ 'VOIP' => '03',
+ 'ISP' => '04',
+ 'Wireless' => '05',
+);
+
+sub info {
+ { batch => 0,
+ override => 0,
+ }
+}
+
+sub add_sale { } # nothing to do here
+
+sub build_request {
+ my ($self, %opt) = @_;
+
+ my $cust_bill = $self->{cust_bill};
+ my $cust_main = $cust_bill->cust_main;
+ my $agentnum = $cust_main->agentnum;
+ my $date = DateTime->from_epoch(epoch => $cust_bill->_date);
+
+ # remember some things that are linked to the customer
+ $self->{taxstatus} = $cust_main->taxstatus
+ or die "Customer #".$cust_main->custnum." has no tax status defined.\n";
+
+ ($self->{bill_zip}, $self->{bill_plus4}) =
+ split('-', $cust_main->bill_location->zip);
+
+ $self->{regcode} = $REGCODE{ $conf->config('suretax-regulatory_code') };
+
+ %taxproduct_cache = ();
+
+ # assemble invoice line items
+ my @lines = map { $self->build_item($_) }
+ $cust_bill->cust_bill_pkg;
+
+ my $ClientNumber = $conf->config('suretax-client_number')
+ or die "suretax-client_number config required.\n";
+ my $ValidationKey = $conf->config('suretax-validation_key')
+ or die "suretax-validation_key config required.\n";
+ my $BusinessUnit = $conf->config('suretax-business_unit', $agentnum) || '';
+
+ return {
+ ClientNumber => $ClientNumber,
+ ValidationKey => $ValidationKey,
+ BusinessUnit => $BusinessUnit,
+ DataYear => '2015', #$date->year,
+ DataMonth => '04', #sprintf('%02d', $date->month),
+ TotalRevenue => sprintf('%.4f', $cust_bill->charged),
+ ReturnFileCode => ($self->{estimate} ? 'Q' : '0'),
+ ClientTracking => $cust_bill->invnum,
+ IndustryExemption => '',
+ ResponseGroup => '13',
+ ResponseType => 'D2',
+ STAN => '',
+ ItemList => \@lines,
+ };
+}
+
+=item build_item CUST_BILL_PKG
+
+Takes a sale item and returns any number of request element hashrefs
+corresponding to it. Yes, any number, because in a rated usage line item
+we have to send each usage detail separately.
+
+=cut
+
+sub build_item {
+ my $self = shift;
+ my $cust_bill_pkg = shift;
+ my $cust_bill = $cust_bill_pkg->cust_bill;
+ my $billpkgnum = $cust_bill_pkg->billpkgnum;
+ my $invnum = $cust_bill->invnum;
+ my $custnum = $cust_bill->custnum;
+
+ # get the part_pkg/fee for this line item, and the relevant part of the
+ # taxproduct cache
+ my $part_item = $cust_bill_pkg->part_X;
+ my $taxproduct_of_class = do {
+ my $part_id = $part_item->table . '#' . $part_item->get($part_item->primary_key);
+ $taxproduct_cache{$part_id} ||= {};
+ };
+
+ my @items;
+ my $recur_without_usage = $cust_bill_pkg->recur;
+
+ my $location = $cust_bill_pkg->tax_location;
+ my ($svc_zip, $svc_plus4) = split('-', $location->zip);
+
+ my $startdate =
+ DateTime->from_epoch( epoch => $cust_bill->_date )->strftime('%m-%d-%Y');
+
+ my %base_item = (
+ 'LineNumber' => '',
+ 'InvoiceNumber' => $billpkgnum,
+ 'CustomerNumber' => $custnum,
+ 'OrigNumber' => '',
+ 'TermNumber' => '',
+ 'BillToNumber' => '',
+ 'Zipcode' => $self->{bill_zip},
+ 'Plus4' => ($self->{bill_plus4} ||= '0000'),
+ 'P2PZipcode' => $svc_zip,
+ 'P2PPlus4' => ($svc_plus4 ||= '0000'),
+ # we don't support Order Placement/Approval zip codes
+ 'Geocode' => '',
+ 'TransDate' => $startdate,
+ 'Revenue' => '',
+ 'Units' => 0,
+ 'UnitType' => '00', # "number of unique lines", the only choice
+ 'Seconds' => 0,
+ 'TaxIncludedCode' => '0',
+ 'TaxSitusRule' => '',
+ 'TransTypeCode' => '',
+ 'SalesTypeCode' => $self->{taxstatus},
+ 'RegulatoryCode' => $self->{regcode},
+ 'TaxExemptionCodeList' => [ ],
+ 'AuxRevenue' => 0, # we don't currently support freight and such
+ 'AuxRevenueType' => '',
+ );
+
+ # some naming conventions:
+ # 'C#####' is a call detail record (using the acctid)
+ # 'S#####' is a cust_bill_pkg setup element (using the billpkgnum)
+ # 'R#####' is a cust_bill_pkg recur element
+ # always set "InvoiceNumber" = the billpkgnum, so we can link it properly
+
+ # cursor all this stuff; data sets can be LARGE
+ # (if it gets really out of hand, we can also incrementally write JSON
+ # to a file)
+
+ my $details = FS::Cursor->new('cust_bill_pkg_detail', {
+ billpkgnum => $cust_bill_pkg->billpkgnum,
+ amount => { op => '>', value => 0 }
+ }, dbh() );
+ while ( my $cust_bill_pkg_detail = $details->fetch ) {
+
+ # look up the tax product for this class
+ my $classnum = $cust_bill_pkg_detail->classnum;
+ my $taxproduct = $taxproduct_of_class->{ $classnum } ||= do {
+ my $part_pkg_taxproduct = $part_item->taxproduct($classnum);
+ $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
+ };
+ die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
+ ", usage class $classnum\n"
+ if !$taxproduct;
+
+ my $cdrs = FS::Cursor->new('cdr', {
+ detailnum => $cust_bill_pkg_detail->detailnum,
+ freesidestatus => 'done',
+ }, dbh() );
+ while ( my $cdr = $cdrs->fetch ) {
+ my $calldate =
+ DateTime->from_epoch( epoch => $cdr->startdate )->strftime('%m-%d-%Y');
+ # determine the tax situs rule; it's different (probably more accurate)
+ # if the call has PSTN phone numbers at both ends
+ my $tsr = $TSR_CALL_OTHER;
+ if ( $cdr->charged_party =~ /^\d{10}$/ and
+ $cdr->src =~ /^\d{10}$/ and
+ $cdr->dst =~ /^\d{10}$/ ) {
+ $tsr = $TSR_CALL_NPANXX;
+ }
+ my %hash = (
+ %base_item,
+ 'LineNumber' => 'C' . $cdr->acctid,
+ 'OrigNumber' => $cdr->src,
+ 'TermNumber' => $cdr->dst,
+ 'BillToNumber' => $cdr->charged_party,
+ 'TransDate' => $calldate,
+ 'Revenue' => $cdr->rated_price, # 4 decimal places
+ 'Units' => 0, # right?
+ 'CallDuration' => $cdr->duration,
+ 'TaxSitusRule' => $tsr,
+ 'TransTypeCode' => $taxproduct,
+ );
+ push @items, \%hash;
+
+ } # while ($cdrs->fetch)
+
+ # decrement the recurring charge
+ $recur_without_usage -= $cust_bill_pkg_detail->amount;
+
+ } # while ($details->fetch)
+
+ # recurring charge
+ if ( $recur_without_usage > 0 ) {
+ my $taxproduct = $taxproduct_of_class->{ 'recur' } ||= do {
+ my $part_pkg_taxproduct = $part_item->taxproduct('recur');
+ $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
+ };
+ die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
+ " recurring charge\n"
+ if !$taxproduct;
+
+ my $tsr = $TSR_GENERAL;
+ my %hash = (
+ %base_item,
+ 'LineNumber' => 'R' . $billpkgnum,
+ 'Revenue' => $recur_without_usage, # 4 decimal places
+ 'Units' => $cust_bill_pkg->units,
+ 'TaxSitusRule' => $tsr,
+ 'TransTypeCode' => $taxproduct,
+ );
+ # API expects all these fields to be _present_, even when they're not
+ # required
+ $hash{$_} = '' foreach(qw(OrigNumber TermNumber BillToNumber));
+ push @items, \%hash;
+ }
+
+ if ( $cust_bill_pkg->setup > 0 ) {
+ my $startdate =
+ DateTime->from_epoch( epoch => $cust_bill->_date )->strftime('%m-%d-%Y');
+ my $taxproduct = $taxproduct_of_class->{ 'setup' } ||= do {
+ my $part_pkg_taxproduct = $part_item->taxproduct('setup');
+ $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
+ };
+ die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
+ " setup charge\n"
+ if !$taxproduct;
+
+ my $tsr = $TSR_GENERAL;
+ my %hash = (
+ %base_item,
+ 'LineNumber' => 'S' . $billpkgnum,
+ 'Revenue' => $cust_bill_pkg->setup, # 4 decimal places
+ 'Units' => $cust_bill_pkg->units,
+ 'TaxSitusRule' => $tsr,
+ 'TransTypeCode' => $taxproduct,
+ );
+ push @items, \%hash;
+ }
+
+ @items;
+}
+
+sub make_taxlines {
+ my $self = shift;
+
+ my @elements;
+
+ my $cust_bill = shift;
+ if (!$cust_bill->invnum) {
+ die "FS::TaxEngine::suretax can't calculate taxes on a non-inserted invoice\n";
+ }
+ $self->{cust_bill} = $cust_bill;
+ my $cust_main = $cust_bill->cust_main;
+ my $country = $cust_main->bill_location->country;
+
+ my $invnum = $cust_bill->invnum;
+ if (FS::cust_bill_pkg->count("invnum = $invnum") == 0) {
+ # don't even bother making the request
+ # (why are we even here, then? invoices with no line items
+ # should not be created)
+ return;
+ }
+
+ # assemble the request hash
+ my $request = $self->build_request;
+
+ warn "sending SureTax request\n" if $DEBUG;
+ my $request_json = $json->encode($request);
+ warn $request_json if $DEBUG > 1;
+
+ # We are targeting the "V05" interface:
+ # - accepts both telecom and general sales transactions
+ # - produces results broken down by "invoice" (Freeside line item)
+ my $ua = LWP::UserAgent->new;
+ my $http_response = $ua->request(
+ POST "https://$host/Services/V05/SureTax.asmx/PostRequest",
+ [ request => $request_json ],
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ 'Accept' => 'application/json',
+ );
+
+ my $raw_response = $http_response->content;
+ warn "received response\n" if $DEBUG;
+ warn $raw_response if $DEBUG > 2;
+ my $response;
+ if ( $raw_response =~ /^<\?xml/ ) {
+ # an error message wrapped in a riddle inside an enigma inside an XML
+ # document...
+ $response = XMLin( $raw_response );
+ $raw_response = $response->{content};
+ }
+ $response = eval { $json->decode($raw_response) }
+ or die "$raw_response\n";
+
+ # documentation implies this might be necessary
+ $response = $response->{'d'} if exists $response->{'d'};
+
+ warn $json->encode($response) if $DEBUG > 1;
+
+ if ( $response->{Successful} ne 'Y' ) {
+ die $response->{HeaderMessage}."\n";
+ } else {
+ my $error = join("\n",
+ map { $_->{"LineNumber"}.': '. $_->{Message} }
+ @{ $response->{ItemMessages} }
+ );
+ die "$error\n" if $error;
+ }
+
+ return if !$response->{GroupList};
+ foreach my $taxable ( @{ $response->{GroupList} } ) {
+ # each member of this array here corresponds to what SureTax calls an
+ # "invoice" and we call a "line item". The invoice number is
+ # cust_bill_pkg.billpkgnum.
+
+ my ($state, $geocode) = split(/\|/, $taxable->{StateCode});
+ foreach my $tax_element ( @{ $taxable->{TaxList} } ) {
+ # create a tax rate location if there isn't one yet
+ my $taxname = $tax_element->{TaxTypeDesc};
+ my $taxauth = substr($tax_element->{TaxTypeCode}, 0, 1);
+ my $tax_rate = FS::tax_rate->new({
+ data_vendor => 'suretax',
+ taxname => $taxname,
+ taxclassnum => '',
+ taxauth => $taxauth, # federal / state / city / district
+ geocode => $geocode, # this is going to disambiguate all
+ # the taxes named "STATE SALES TAX", etc.
+ tax => 0,
+ fee => 0,
+ });
+ my $error = $tax_rate->find_or_insert;
+ die "error inserting tax_rate record for '$taxname': $error\n"
+ if $error;
+ $tax_rate = $tax_rate->replace_old;
+
+ my $tax_rate_location = FS::tax_rate_location->new({
+ data_vendor => 'suretax',
+ geocode => $geocode,
+ state => $state,
+ country => $country,
+ });
+ $error = $tax_rate_location->find_or_insert;
+ die "error inserting tax_rate_location record for '$geocode': $error\n"
+ if $error;
+ $tax_rate_location = $tax_rate_location->replace_old;
+
+ push @elements, FS::cust_bill_pkg_tax_rate_location->new({
+ taxable_billpkgnum => $taxable->{InvoiceNumber},
+ taxnum => $tax_rate->taxnum,
+ taxtype => 'FS::tax_rate',
+ taxratelocationnum => $tax_rate_location->taxratelocationnum,
+ amount => sprintf('%.2f', $tax_element->{TaxAmount}),
+ });
+ }
+ }
+ return @elements;
+}
+
+sub add_taxproduct {
+ my $class = shift;
+ my $desc = shift; # tax code and description, separated by a space.
+ if ($desc =~ s/^(\d{6}+) //) {
+ my $part_pkg_taxproduct = FS::part_pkg_taxproduct->new({
+ 'data_vendor' => 'suretax',
+ 'taxproduct' => $1,
+ 'description' => $desc,
+ });
+ # $obj_or_error
+ return $part_pkg_taxproduct->insert || $part_pkg_taxproduct;
+ } else {
+ return "illegal suretax tax code '$desc'";
+ }
+}
+
+1;
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index b4340d075..ffc04bab7 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -133,11 +133,11 @@ If you need to continue using the old Form 477 report, turn on the
$conf->set($newname, 'location');
}
- # boolean enable_taxproducts is now enable_taxproducts = 'cch'
- if ( $conf->exists('enable_taxproducts') and
- $conf->config('enable_taxproducts') eq '' ) {
+ # boolean enable_taxproducts is now tax_data_vendor = 'cch'
+ if ( $conf->exists('enable_taxproducts') ) {
- $conf->set('enable_taxproducts', 'cch');
+ $conf->delete('enable_taxproducts');
+ $conf->set('tax_data_vendor', 'cch');
}
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
index b6e439552..a5c441008 100644
--- a/FS/FS/cust_bill_pkg.pm
+++ b/FS/FS/cust_bill_pkg.pm
@@ -1275,7 +1275,7 @@ sub upgrade_tax_location {
local $FS::cust_location::import = 1;
my $conf = FS::Conf->new; # h_conf?
- return if $conf->exists('enable_taxproducts'); #don't touch this case
+ return if $conf->config('tax_data_vendor'); #don't touch this case
my $use_ship = $conf->exists('tax-ship_address');
my $use_pkgloc = $conf->exists('tax-pkg_address');
diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm
index 2ffc27357..9a1f22a02 100644
--- a/FS/FS/cust_bill_pkg_tax_location.pm
+++ b/FS/FS/cust_bill_pkg_tax_location.pm
@@ -144,6 +144,19 @@ Returns the cust_bill_pkg object for the I<taxable> charge.
Returns the associated cust_location object
+=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
diff --git a/FS/FS/cust_bill_pkg_tax_rate_location.pm b/FS/FS/cust_bill_pkg_tax_rate_location.pm
index 3e8098c3a..7ae5250e9 100644
--- a/FS/FS/cust_bill_pkg_tax_rate_location.pm
+++ b/FS/FS/cust_bill_pkg_tax_rate_location.pm
@@ -6,6 +6,7 @@ use FS::Record qw( qsearch qsearchs );
use FS::cust_pkg;
use FS::cust_bill_pay_pkg;
use FS::cust_credit_bill_pkg;
+use FS::tax_rate;
=head1 NAME
@@ -130,6 +131,19 @@ Returns the associated cust_bill_pkg object
Returns the associated tax_rate_location object
+=item taxname
+
+Returns the tax name (the itemdesc).
+
+=cut
+
+sub taxname {
+ my $self = shift;
+ my $tax_rate = FS::tax_rate->by_key($self->taxnum)
+ or return '';
+ $tax_rate->taxname;
+}
+
=item desc
Returns a description for this tax line item constituent. Currently this
diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm
index 91bbf790b..f63d86f99 100644
--- a/FS/FS/cust_credit.pm
+++ b/FS/FS/cust_credit.pm
@@ -558,7 +558,7 @@ sub _upgrade_data { # class method
$class->_upgrade_otaker(%opts);
if ( !FS::upgrade_journal->is_done('cust_credit__tax_link')
- and !$conf->exists('enable_taxproducts') ) {
+ and !$conf->config('tax_data_vendor') ) {
# RT#25458: fix credit line item applications that should refer to a
# specific tax allocation
my @cust_credit_bill_pkg = qsearch({
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index 75dca3426..f4c804568 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -1422,7 +1422,7 @@ sub _handle_taxes {
return if ( $self->payby eq 'COMP' ); #dubious
- if ( $conf->exists('enable_taxproducts')
+ if ( $conf->config('enable_taxproducts')
&& ( scalar($part_item->part_pkg_taxoverride)
|| $part_item->has_taxproduct
)
diff --git a/FS/FS/cust_main/Billing_Discount.pm b/FS/FS/cust_main/Billing_Discount.pm
index d437740e3..b2852f6c1 100644
--- a/FS/FS/cust_main/Billing_Discount.pm
+++ b/FS/FS/cust_main/Billing_Discount.pm
@@ -110,6 +110,9 @@ by prepaying the most recent invoice for MONTHS.
=cut
+# XXX this should work by creating a quotation; then we can finally retire
+# the "no_commit" option, which doesn't work with modern tax calculation
+
sub discount_term_values {
my $self = shift;
my $term = shift;
diff --git a/FS/FS/part_event/Action/fee.pm b/FS/FS/part_event/Action/fee.pm
index f1d5891ac..a18cc33d1 100644
--- a/FS/FS/part_event/Action/fee.pm
+++ b/FS/FS/part_event/Action/fee.pm
@@ -40,7 +40,7 @@ sub _calc_fee {
# they're definitely NOT linear and we haven't yet had a reason to
# make that case work.
return $total if $self->option('setuptax') eq 'Y'
- or FS::Conf->new->exists('enable_taxproducts');
+ or FS::Conf->new->config('tax_data_vendor');
# estimate tax rate
# false laziness with xmlhttp-calculate_taxes, cust_main::Billing, etc.
diff --git a/FS/FS/part_fee.pm b/FS/FS/part_fee.pm
index ef14b4f08..0ca52a096 100644
--- a/FS/FS/part_fee.pm
+++ b/FS/FS/part_fee.pm
@@ -523,6 +523,11 @@ sub has_taxproduct {
return ($self->taxproductnum ? 1 : 0);
}
+sub taxproduct { # compat w/ part_pkg
+ my $self = shift;
+ $self->part_pkg_taxproduct;
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm
index 8579020e1..67dd40e83 100644
--- a/FS/FS/tax_rate.pm
+++ b/FS/FS/tax_rate.pm
@@ -386,10 +386,7 @@ Takes an arrayref of L<FS::cust_bill_pkg> objects representing taxable line
items, and an arrayref of charge classes ('setup', 'recur', '' for
unclassified usage, or an L<FS::usage_class> number). Calculates the tax on
each item under this tax definition and returns a list of new
-L<FS::cust_bill_pkg> objects for the taxes charged. Each returned object
-will have a pseudo-field, "cust_bill_pkg_tax_rate_location", containing a
-single L<FS::cust_bill_pkg_tax_rate_location> object linking the tax rate
-back to this tax, and to its originating sale.
+L<FS::cust_bill_pkg_tax_rate_location> objects for the taxes charged.
If the taxable objects are linked to an invoice, this will also calculate
per-customer exemptions (cust_exempt and cust_taxname_exempt) and attach them
@@ -461,7 +458,7 @@ sub taxline_cch {
$self->_fatal_or_null( 'tax with "'. $self->basetype_name. '" basis' );
}
- my @tax_locations;
+ my @tax_links; # for output
my %seen; # locationnum or pkgnum => 1
my $taxable_cents = 0;
@@ -514,7 +511,7 @@ sub taxline_cch {
# yeah, some false laziness with cust_main_county
my $this_tax_cents = int(100 * $taxable_charged * $self->tax);
- my $tax_location = FS::cust_bill_pkg_tax_rate_location->new({
+ my $tax_link = FS::cust_bill_pkg_tax_rate_location->new({
'taxnum' => $self->taxnum,
'taxtype' => ref($self),
'cents' => $this_tax_cents, # not a real field
@@ -524,7 +521,7 @@ sub taxline_cch {
'taxratelocationnum' => $taxratelocationnum,
'taxclass' => $class,
});
- push @tax_locations, $tax_location;
+ push @tax_links, $tax_link;
$taxable_cents += 100 * $taxable_charged;
$tax_cents += $this_tax_cents;
@@ -579,7 +576,7 @@ sub taxline_cch {
return $self->_fatal_or_null( 'unknown unit type in tax'. $self->taxnum );
}
my $this_tax_cents = int($units * $self->fee * 100);
- my $tax_location = FS::cust_bill_pkg_tax_rate_location->new({
+ my $tax_link = FS::cust_bill_pkg_tax_rate_location->new({
'taxnum' => $self->taxnum,
'taxtype' => ref($self),
'cents' => $this_tax_cents,
@@ -587,7 +584,7 @@ sub taxline_cch {
'taxable_cust_bill_pkg' => $cust_bill_pkg,
'taxratelocationnum' => $taxratelocationnum,
});
- push @tax_locations, $tax_location;
+ push @tax_links, $tax_link;
$taxable_units += $units;
$tax_cents += $this_tax_cents;
@@ -614,7 +611,7 @@ sub taxline_cch {
my $extra_cents = sprintf('%.0f', $total_tax_cents - $tax_cents);
$tax_cents += $extra_cents;
my $i = 0;
- foreach (@tax_locations) { # can never require more than a single pass, yes?
+ foreach (@tax_links) { # can never require more than a single pass, yes?
my $cents = $_->get('cents');
if ( $extra_cents > 0 ) {
$cents++;
@@ -623,26 +620,7 @@ sub taxline_cch {
$_->set('amount', sprintf('%.2f', $cents/100));
}
- # just transform each CBPTRL record into a tax line item.
- # calculate_taxes will consolidate them, but before that happens we have
- # to do tax on tax calculation.
- my @tax_items;
- foreach (@tax_locations) {
- next if $_->amount == 0;
- my $tax_item = FS::cust_bill_pkg->new({
- 'pkgnum' => 0,
- 'recur' => 0,
- 'setup' => $_->amount,
- 'sdate' => '', # $_->sdate?
- 'edate' => '',
- 'itemdesc' => $name,
- 'cust_bill_pkg_tax_rate_location' => [ $_ ],
- });
- $_->set('tax_cust_bill_pkg' => $tax_item);
- push @tax_items, $tax_item;
- }
-
- return @tax_items;
+ return @tax_links;
}
sub _fatal_or_null {
diff --git a/FS/FS/tax_status.pm b/FS/FS/tax_status.pm
index f03eeca6a..5f7b50fde 100644
--- a/FS/FS/tax_status.pm
+++ b/FS/FS/tax_status.pm
@@ -149,6 +149,12 @@ sub _upgrade_data {
# P, Q, R: Canada, not yet supported
# MED1/MED2: totally irrelevant to our users
},
+ suretax => {
+ 'R' => 'Residential',
+ 'B' => 'Business',
+ 'I' => 'Industrial',
+ 'L' => 'Lifeline',
+ },
);
=back