summaryrefslogtreecommitdiff
path: root/FS/FS
diff options
context:
space:
mode:
authorjeff <jeff>2008-04-01 00:54:44 +0000
committerjeff <jeff>2008-04-01 00:54:44 +0000
commit4104f4e3d1b387296b16b4a035b4b7f42e0c5977 (patch)
treefcb03d2c518cc91df33059675764d548fedc9e7a /FS/FS
parent12eb930abf31078c68dbf7eb94865faa1c59fc9e (diff)
checkpoint of new tax rating system
Diffstat (limited to 'FS/FS')
-rw-r--r--FS/FS/Conf.pm7
-rw-r--r--FS/FS/Schema.pm111
-rw-r--r--FS/FS/cust_tax_location.pm208
-rw-r--r--FS/FS/part_pkg.pm30
-rw-r--r--FS/FS/part_pkg_taxoverride.pm119
-rw-r--r--FS/FS/part_pkg_taxproduct.pm124
-rw-r--r--FS/FS/part_pkg_taxrate.pm336
-rw-r--r--FS/FS/tax_class.pm249
-rw-r--r--FS/FS/tax_rate.pm605
9 files changed, 1789 insertions, 0 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 94f2f05..d1f64b2 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -1406,6 +1406,13 @@ worry that config_items is freeside-specific and icky.
},
{
+ 'key' => 'enable_taxproducts',
+ 'section' => 'billing',
+ 'description' => 'Enable per-package mapping to new style tax classes',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'welcome_email',
'section' => '',
'description' => 'Template file for welcome email. Welcome emails are sent to the customer email invoice destination(s) each time a svc_acct record is created. See the <a href="http://search.cpan.org/~mjd/Text-Template/lib/Text/Template.pm">Text::Template</a> documentation for details on the template substitution language. The following variables are available<ul><li><code>$username</code> <li><code>$password</code> <li><code>$first</code> <li><code>$last</code> <li><code>$pkg</code></ul>',
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 9548aa7..e431b07 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -303,6 +303,7 @@ sub tables_hashref {
my @date_type = ( 'int', 'NULL', '' );
my @perl_type = ( 'text', 'NULL', '' );
my @money_type = ( 'decimal', '', '10,2' );
+ my @money_typen = ( 'decimal', 'NULL', '10,2' );
my $username_len = 32; #usernamemax config file
@@ -665,6 +666,68 @@ sub tables_hashref {
'index' => [ [ 'county' ], [ 'state' ], [ 'country' ] ],
},
+ 'tax_rate' => {
+ 'columns' => [
+ 'taxnum', 'serial', '', '', '', '',
+ 'geocode', 'varchar', 'NULL', $char_d, '', '',#cch provides 10 char
+ 'data_vendor', 'varchar', 'NULL', $char_d, '', '',#auto update source
+ 'location', 'varchar', 'NULL', $char_d, '', '',#provided by tax authority
+ 'taxclassnum', 'int', '', '', '', '',
+ 'effective_date', @date_type, '', '',
+ 'tax', 'real', '', '', '', '', # tax %
+ 'excessrate', 'real', 'NULL','', '', '', # second tax %
+ 'taxbase', @money_typen, '', '', # amount at first tax rate
+ 'taxmax', @money_typen, '', '', # maximum about at both rates
+ 'usetax', 'real', 'NULL', '', '', '', # tax % when non-local
+ 'useexcessrate', 'real', 'NULL', '', '', '', # second tax % when non-local
+ 'unittype', 'int', 'NULL', '', '', '', # for fee
+ 'fee', 'real', 'NULL', '', '', '', # amount tax per unit
+ 'excessfee', 'real', 'NULL', '', '', '', # second amount tax per unit
+ 'feebase', 'real', 'NULL', '', '', '', # units taxed at first rate
+ 'feemax', 'real', 'NULL', '', '', '', # maximum number of unit taxed
+ 'maxtype', 'int', 'NULL', '', '', '', # indicator of how thresholds accumulate
+ 'taxname', 'varchar', 'NULL', $char_d, '', '', # may appear on invoice
+ 'taxauth', 'int', 'NULL', '', '', '', # tax authority
+ 'basetype', 'int', 'NULL', '', '', '', # indicator of basis for tax
+ 'passtype', 'int', 'NULL', '', '', '', # indicator declaring how item should be shown
+ 'passflag', 'char', 'NULL', 1, '', '', # Y = required to list as line item, N = Prohibited
+ 'setuptax', 'char', 'NULL', 1, '', '', # Y = setup tax exempt
+ 'recurtax', 'char', 'NULL', 1, '', '', # Y = recur tax exempt
+ 'manual', 'char', 'NULL', 1, '', '', # Y = manually edited
+ ],
+ 'primary_key' => 'taxnum',
+ 'unique' => [],
+ 'index' => [ ['taxclassnum'], ['data_vendor', 'geocode'] ],
+ },
+
+ 'cust_tax_location' => {
+ 'columns' => [
+ 'custlocationnum', 'serial', '', '', '', '',
+ 'data_vendor', 'varchar', 'NULL', $char_d, '', '', # update source
+ 'zip', 'char', '', 5, '', '',
+ 'state', 'char', '', 2, '', '',
+ 'plus4hi', 'char', '', 4, '', '',
+ 'plus4lo', 'char', '', 4, '', '',
+ 'default_location','char', 'NULL', 1, '', '', # Y = default for zip
+ 'geocode', 'varchar', '', 20, '', '',
+ ],
+ 'primary_key' => 'custlocationnum',
+ 'unique' => [],
+ 'index' => [ [ 'zip', 'plus4lo', 'plus4hi' ] ],
+ },
+
+ 'tax_class' => {
+ 'columns' => [
+ 'taxclassnum', 'serial', '', '', '', '',
+ 'data_vendor', 'varchar', 'NULL', $char_d, '', '',
+ 'taxclass', 'varchar', '', $char_d, '', '',
+ 'description', 'varchar', '', 2*$char_d, '', '',
+ ],
+ 'primary_key' => 'taxclassnum',
+ 'unique' => [ [ 'data_vendor', 'taxclass' ] ],
+ 'index' => [],
+ },
+
'cust_pay_pending' => {
'columns' => [
'paypendingnum','serial', '', '', '', '',
@@ -933,6 +996,7 @@ sub tables_hashref {
'disabled', 'char', 'NULL', 1, '', '',
'taxclass', 'varchar', 'NULL', $char_d, '', '',
'classnum', 'int', 'NULL', '', '', '',
+ 'taxproductnum', 'int', 'NULL', '', '', '',
'pay_weight', 'real', 'NULL', '', '', '',
'credit_weight', 'real', 'NULL', '', '', '',
'agentnum', 'int', 'NULL', '', '', '',
@@ -953,6 +1017,51 @@ sub tables_hashref {
'index' => [],
},
+ 'part_pkg_taxproduct' => {
+ 'columns' => [
+ 'taxproductnum', 'serial', '', '', '', '',
+ 'data_vendor', 'varchar', 'NULL', $char_d, '', '',
+ 'taxproduct', 'varchar', '', $char_d, '', '',
+ 'description', 'varchar', '', 2*$char_d, '', '',
+ ],
+ 'primary_key' => 'taxproductnum',
+ 'unique' => [ [ 'data_vendor', 'taxproduct' ] ],
+ 'index' => [],
+ },
+
+ 'part_pkg_taxrate' => {
+ 'columns' => [
+ 'pkgtaxratenum', 'serial', '', '', '', '',
+ 'data_vendor', 'varchar', 'NULL', $char_d, '', '', # update source
+ 'geocode', 'varchar', 'NULL', $char_d, '', '', # cch provides 10
+ 'taxproductnum', 'int', '', '', '', '',
+ 'city', 'varchar', 'NULL', $char_d, '', '', # tax_location?
+ 'county', 'varchar', 'NULL', $char_d, '', '',
+ 'state', 'varchar', 'NULL', $char_d, '', '',
+ 'local', 'varchar', 'NULL', $char_d, '', '',
+ 'country', 'char', 'NULL', 2, '', '',
+ 'taxclassnumtaxed', 'int', 'NULL', '', '', '',
+ 'taxcattaxed', 'varchar', 'NULL', $char_d, '', '',
+ 'taxclassnum', 'int', 'NULL', '', '', '',
+ 'effdate', @date_type, '', '',
+ 'taxable', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'pkgtaxratenum',
+ 'unique' => [],
+ 'index' => [ [ 'data_vendor', 'geocode', 'taxproductnum' ] ],
+ },
+
+ 'part_pkg_taxoverride' => {
+ 'columns' => [
+ 'taxoverridenum', 'serial', '', '', '', '',
+ 'pkgpart', 'serial', '', '', '', '',
+ 'taxnum', 'serial', '', '', '', '',
+ ],
+ 'primary_key' => 'taxoverridenum',
+ 'unique' => [],
+ 'index' => [ [ 'pkgpart' ], [ 'taxnum' ] ],
+ },
+
# 'part_title' => {
# 'columns' => [
# 'titlenum', 'int', '', '',
@@ -1366,6 +1475,7 @@ sub tables_hashref {
'routernum', 'serial', '', '', '', '',
'routername', 'varchar', '', $char_d, '', '',
'svcnum', 'int', 'NULL', '', '', '',
+ 'agentnum', 'int', 'NULL', '', '', '',
],
'primary_key' => 'routernum',
'unique' => [],
@@ -1389,6 +1499,7 @@ sub tables_hashref {
'routernum', 'int', '', '', '', '',
'ip_gateway', 'varchar', '', 15, '', '',
'ip_netmask', 'int', '', '', '', '',
+ 'agentnum', 'int', 'NULL', '', '', '',
],
'primary_key' => 'blocknum',
'unique' => [ [ 'blocknum', 'routernum' ] ],
diff --git a/FS/FS/cust_tax_location.pm b/FS/FS/cust_tax_location.pm
new file mode 100644
index 0000000..11faa3f
--- /dev/null
+++ b/FS/FS/cust_tax_location.pm
@@ -0,0 +1,208 @@
+package FS::cust_tax_location;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs dbh );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::cust_tax_location - Object methods for cust_tax_location records
+
+=head1 SYNOPSIS
+
+ use FS::cust_tax_location;
+
+ $record = new FS::cust_tax_location \%hash;
+ $record = new FS::cust_tax_location { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_tax_location object represents a mapping between a customer and
+a tax location. FS::cust_tax_location inherits from FS::Record. The
+following fields are currently supported:
+
+=over 4
+
+=item custlocationnum
+
+primary key
+
+=item data_vendor
+
+a tax data vendor
+
+=item zip
+
+=item state
+
+=item plus4hi
+
+the upper bound of the last 4 zip code digits
+
+=item plus4lo
+
+the lower bound of the last 4 zip code digits
+
+=item default_location
+
+'Y' when this record represents the default for zip
+
+=item geocode - the foreign key into FS::part_pkg_tax_rate and FS::tax_rate
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new cust_tax_location. To add the cust_tax_location to the database,
+see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_tax_location'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid cust_tax_location. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('custlocationnum')
+ || $self->ut_text('data_vendor')
+ || $self->ut_number('zip')
+ || $self->ut_text('state')
+ || $self->ut_number('plus4hi')
+ || $self->ut_number('plus4lo')
+ || $self->ut_enum('default', [ '', ' ', 'Y' ] )
+ || $self->ut_number('geocode')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+
+sub batch_import {
+ my $param = shift;
+
+ my $fh = $param->{filehandle};
+ my $format = $param->{'format'};
+
+ my @fields;
+ if ( $format eq 'cch' ) {
+ @fields = qw( zip state plus4lo plus4hi geocode default );
+ } elsif ( $format eq 'extended' ) {
+ die "unimplemented\n";
+ @fields = qw( );
+ } else {
+ die "unknown format $format";
+ }
+
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+
+ my $imported = 0;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $line;
+ while ( defined($line=<$fh>) ) {
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ my @columns = $csv->fields();
+
+ my %cust_tax_location = ( 'data_vendor' => $format );;
+ foreach my $field ( @fields ) {
+ $cust_tax_location{$field} = shift @columns;
+ }
+
+ my $cust_tax_location = new FS::cust_tax_location( \%cust_tax_location );
+ my $error = $cust_tax_location->insert;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert cust_tax_location for $line: $error";
+ }
+
+ $imported++;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
+=back
+
+=head1 BUGS
+
+The author should be informed of any you find.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 84502b7..dc0a4d5 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -14,6 +14,8 @@ use FS::type_pkgs;
use FS::part_pkg_option;
use FS::pkg_class;
use FS::agent;
+use FS::part_pkg_taxoverride;
+use FS::part_pkg_taxproduct;
@ISA = qw( FS::m2m_Common FS::Record ); # FS::option_Common ); # this can use option_Common
# when all the plandata bs is
@@ -726,6 +728,34 @@ sub option {
'';
}
+=item part_pkg_taxoverride
+
+Returns all options as FS::part_pkg_taxoverride objects (see
+L<FS::part_pkg_taxoverride>).
+
+=cut
+
+sub part_pkg_taxoverride {
+ my $self = shift;
+ qsearch('part_pkg_taxoverride', { 'pkgpart' => $self->pkgpart } );
+}
+
+=item taxproduct_description
+
+Returns the description of the associated tax product for this package
+definition (see L<FS::part_pkg_taxproduct>).
+
+=cut
+
+sub taxproduct_description {
+ my $self = shift;
+ my $part_pkg_taxproduct =
+ qsearchs( 'part_pkg_taxproduct',
+ { 'taxproductnum' => $self->taxproductnum }
+ );
+ $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
+}
+
=item _rebless
Reblesses the object into the FS::part_pkg::PLAN class (if available), where
diff --git a/FS/FS/part_pkg_taxoverride.pm b/FS/FS/part_pkg_taxoverride.pm
new file mode 100644
index 0000000..656fe53
--- /dev/null
+++ b/FS/FS/part_pkg_taxoverride.pm
@@ -0,0 +1,119 @@
+package FS::part_pkg_taxoverride;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::part_pkg_taxoverride - Object methods for part_pkg_taxoverride records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_taxoverride;
+
+ $record = new FS::part_pkg_taxoverride \%hash;
+ $record = new FS::part_pkg_taxoverride { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_taxoverride object represents a manual mapping of a
+package to tax rates. FS::part_pkg_taxoverride inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item taxoverridenum
+
+Primary key
+
+=item pkgpart
+
+The package definition id
+
+=item taxnum
+
+The tax rate definition id
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new tax override. To add the tax product to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'part_pkg_taxoverride'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid tax product. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('taxoverridenum')
+ || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_foreign_key('taxnum', 'tax_rate', 'taxnum')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=cut
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg_taxproduct.pm b/FS/FS/part_pkg_taxproduct.pm
new file mode 100644
index 0000000..000d0d4
--- /dev/null
+++ b/FS/FS/part_pkg_taxproduct.pm
@@ -0,0 +1,124 @@
+package FS::part_pkg_taxproduct;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::part_pkg_taxproduct - Object methods for part_pkg_taxproduct records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_taxproduct;
+
+ $record = new FS::part_pkg_taxproduct \%hash;
+ $record = new FS::part_pkg_taxproduct { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_taxproduct object represents a tax product.
+FS::part_pkg_taxproduct inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item taxproductnum
+
+Primary key
+
+=item data_vendor
+
+Tax data vendor
+
+=item taxproduct
+
+Tax product id from the vendor
+
+=item description
+
+A human readable description of the id in taxproduct
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new tax product. To add the tax product to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'part_pkg_taxproduct'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid tax product. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('taxproductnum')
+ || $self->ut_textn('data_vendor')
+ || $self->ut_text('taxproduct')
+ || $self->ut_textn('description')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=cut
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg_taxrate.pm b/FS/FS/part_pkg_taxrate.pm
new file mode 100644
index 0000000..aa1c3df
--- /dev/null
+++ b/FS/FS/part_pkg_taxrate.pm
@@ -0,0 +1,336 @@
+package FS::part_pkg_taxrate;
+
+use strict;
+use vars qw( @ISA );
+use Date::Parse;
+use FS::UID qw(dbh);
+use FS::Record qw( qsearch qsearchs );
+use FS::part_pkg_taxproduct;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::part_pkg_taxrate - Object methods for part_pkg_taxrate records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_taxrate;
+
+ $record = new FS::part_pkg_taxrate \%hash;
+ $record = new FS::part_pkg_taxrate { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_taxrate object maps packages onto tax rates.
+FS::part_pkg_taxrate inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item pkgtaxratenum
+
+Primary key
+
+=item data_vendor
+
+Tax data vendor
+
+=item geocode
+
+Tax vendor location code
+
+=item taxproductnum
+
+Class of package for tax purposes, Index into FS::part_pkg_taxproduct
+
+=item city
+
+city
+
+=item county
+
+county
+
+=item state
+
+state
+
+=item local
+
+local
+
+=item country
+
+country
+
+=item taxclassnum
+
+Class of tax index into FS::tax_taxclass and FS::tax_rate
+
+=item taxclassnumtaxed
+
+Class of tax taxed by this entry.
+
+=item taxable
+
+taxable
+
+=item effdate
+
+effdate
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new customer (location), package, tax rate mapping. To add the
+mapping to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'part_pkg_taxrate'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid tax rate mapping. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('pkgtaxratenum')
+ || $self->ut_textn('data_vendor')
+ || $self->ut_textn('geocode')
+ || $self->
+ ut_foreign_key('taxproductnum', 'part_pkg_taxproduct', 'taxproductnum')
+ || $self->ut_textn('city')
+ || $self->ut_textn('county')
+ || $self->ut_textn('state')
+ || $self->ut_textn('local')
+ || $self->ut_text('country')
+ || $self->ut_foreign_keyn('taxclassnumtaxed', 'tax_class', 'taxclassnum')
+ || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
+ || $self->ut_numbern('effective_date')
+ || $self->ut_enum('taxable', [ 'Y', '' ])
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item batch_import
+
+Loads part_pkg_taxrate records from an external CSV file. If there is
+an error, returns the error, otherwise returns false.
+
+=cut
+
+sub batch_import {
+ my $param = shift;
+
+ my $fh = $param->{filehandle};
+ my $format = $param->{'format'};
+
+ my @fields;
+ my $hook;
+ if ( $format eq 'cch' ) {
+ @fields = qw( city county state local geocode group groupdesc item
+ itemdesc provider customer taxtypetaxed taxcattaxed
+ taxable taxtype taxcat effdate rectype );
+
+ $hook = sub {
+ my $hash = shift;
+
+ unless ( $hash->{'rectype'} eq 'R' or $hash->{'rectype'} eq 'T' ) {
+ delete($hash->{$_}) for (keys %$hash);
+ return;
+ }
+
+ my %providers = ( '00' => 'Regulated LEC',
+ '01' => 'Regulated IXC',
+ '02' => 'Unregulated LEC',
+ '03' => 'Unregulated IXC',
+ '04' => 'ISP',
+ '05' => 'Wireless',
+ );
+
+ my %customers = ( '00' => 'Residential',
+ '01' => 'Commercial',
+ '02' => 'Industrial',
+ '09' => 'Lifeline',
+ '10' => 'Senior Citizen',
+ );
+
+ my $taxproduct =
+ join(':', map{ $hash->{$_} } qw(group item provider customer ) );
+
+ my %part_pkg_taxproduct = ( 'data_vendor' => 'cch',
+ 'taxproduct' => $taxproduct,
+ );
+
+ my $part_pkg_taxproduct = qsearchs( 'part_pkg_taxproduct',
+ { %part_pkg_taxproduct }
+ );
+ unless ($part_pkg_taxproduct) {
+ $part_pkg_taxproduct{'description'} =
+ join(' : ', map{ $hash->{$_} } qw(groupdesc itemdesc),
+ $providers{$hash->{'provider'}} || 'Unknown',
+ $customers{$hash->{'customer'}} || 'Unknown',
+ );
+ $part_pkg_taxproduct = new FS::part_pkg_taxproduct \%part_pkg_taxproduct;
+ my $error = $part_pkg_taxproduct->insert;
+ return "Error inserting tax product (part_pkg_taxproduct): $error"
+ if $error;
+
+ }
+ $hash->{'taxproductnum'} = $part_pkg_taxproduct->taxproductnum;
+
+ delete($hash->{$_})
+ for qw(group groupdesc item itemdesc provider customer rectype );
+
+ my %map = ( 'taxclassnum' => [ 'taxtype', 'taxcat' ],
+ 'taxclassnumtaxed' => [ 'taxtypetaxed', 'taxcattaxed' ],
+ );
+
+ for my $item (keys %map) {
+ my $tax_class =
+ qsearchs( 'tax_class',
+ { data_vendor => 'cch',
+ 'taxclass' => join(':', map($hash->{$_}, @{$map{$item}})),
+ }
+ );
+ $hash->{$item} = $tax_class->taxclassnum
+ if $tax_class;
+
+ delete($hash->{$_}) foreach @{$map{$item}};
+ }
+
+ $hash->{'effdate'} = str2time($hash->{'effdate'});
+
+ $hash->{'effdate'} = str2time($hash->{'effdate'});
+ $hash->{'country'} = 'US'; # CA is available
+
+ delete($hash->{'taxable'}) if ($hash->{'taxable'} eq 'N');
+
+ '';
+ };
+
+ } elsif ( $format eq 'extended' ) {
+ die "unimplemented\n";
+ @fields = qw( );
+ $hook = sub {};
+ } else {
+ die "unknown format $format";
+ }
+
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+
+ my $imported = 0;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $line;
+ while ( defined($line=<$fh>) ) {
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ my @columns = $csv->fields();
+
+ my %part_pkg_taxrate = ( 'data_vendor' => $format );
+ foreach my $field ( @fields ) {
+ $part_pkg_taxrate{$field} = shift @columns;
+ }
+ my $error = &{$hook}(\%part_pkg_taxrate);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ next unless scalar(keys %part_pkg_taxrate);
+
+
+ my $part_pkg_taxrate = new FS::part_pkg_taxrate( \%part_pkg_taxrate );
+ $error = $part_pkg_taxrate->insert;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert part_pkg_taxrate for $line: $error";
+ }
+
+ $imported++;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
+
diff --git a/FS/FS/tax_class.pm b/FS/FS/tax_class.pm
new file mode 100644
index 0000000..0a939ad
--- /dev/null
+++ b/FS/FS/tax_class.pm
@@ -0,0 +1,249 @@
+package FS::tax_class;
+
+use strict;
+use vars qw( @ISA );
+use FS::UID qw(dbh);
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::tax_class - Object methods for tax_class records
+
+=head1 SYNOPSIS
+
+ use FS::tax_class;
+
+ $record = new FS::tax_class \%hash;
+ $record = new FS::tax_class { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::tax_class object represents a tax class. FS::tax_class
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item taxclassnum
+
+Primary key
+
+=item data_vendor
+
+Vendor of the tax data
+
+=item taxclass
+
+Tax class
+
+=item description
+
+Human readable description of the tax class
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new tax class. To add the tax class to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'tax_class'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid tax class. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('taxclassnum')
+ || $self->ut_text('taxclass')
+ || $self->ut_textn('data_vendor')
+ || $self->ut_textn('description')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item batch_import
+
+Loads part_pkg_taxrate records from an external CSV file. If there is
+an error, returns the error, otherwise returns false.
+
+=cut
+
+sub batch_import {
+ my $param = shift;
+
+ my $fh = $param->{filehandle};
+ my $format = $param->{'format'};
+
+ my @fields;
+ my $hook;
+ my $endhook;
+ my $data = {};
+ my $imported = 0;
+
+ if ( $format eq 'cch' ) {
+ @fields = qw( table name pos number length value description );
+
+ $hook = sub {
+ my $hash = shift;
+
+ if ($hash->{'table'} eq 'DETAIL') {
+ push @{$data->{'taxcat'}}, [ $hash->{'value'}, $hash->{'description'} ]
+ if $hash->{'name'} eq 'TAXCAT';
+
+ push @{$data->{'taxtype'}}, [ $hash->{'value'}, $hash->{'description'} ]
+ if $hash->{'name'} eq 'TAXTYPE';
+ }
+
+ delete($hash->{$_})
+ for qw( data_vendor table name pos number length value description );
+
+ '';
+
+ };
+
+ $endhook = sub {
+ foreach my $type (@{$data->{'taxtype'}}) {
+ foreach my $cat (@{$data->{'taxcat'}}) {
+ my $tax_class =
+ new FS::tax_class( { 'data_vendor' => 'cch',
+ 'taxclass' => $type->[0].':'.$cat->[0],
+ 'description' => $type->[1].':'.$cat->[1],
+ } );
+ my $error = $tax_class->insert;
+ return $error if $error;
+ $imported++;
+ }
+ }
+ '';
+ };
+
+ } elsif ( $format eq 'extended' ) {
+ die "unimplemented\n";
+ @fields = qw( );
+ $hook = sub {};
+ } else {
+ die "unknown format $format";
+ }
+
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $line;
+ while ( defined($line=<$fh>) ) {
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ my @columns = $csv->fields();
+
+ my %tax_class = ( 'data_vendor' => $format );
+ foreach my $field ( @fields ) {
+ $tax_class{$field} = shift @columns;
+ }
+ my $error = &{$hook}(\%tax_class);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ next unless scalar(keys %tax_class);
+
+ my $tax_class = new FS::tax_class( \%tax_class );
+ $error = $tax_class->insert;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert tax_class for $line: $error";
+ }
+
+ $imported++;
+ }
+
+ my $error = &{$endhook}();
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert tax_class for $line: $error";
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
+
diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm
new file mode 100644
index 0000000..38e5343
--- /dev/null
+++ b/FS/FS/tax_rate.pm
@@ -0,0 +1,605 @@
+package FS::tax_rate;
+
+use strict;
+use vars qw( @ISA @EXPORT_OK $conf $DEBUG $me
+ %tax_unittypes %tax_maxtypes %tax_basetypes %tax_authorities
+ %tax_passtypes
+ @tax_rate %tax_rate $countyflag );
+use Exporter;
+use Date::Parse;
+use Tie::IxHash;
+use FS::Record qw( qsearchs qsearch dbh );
+use FS::tax_class;
+
+@ISA = qw( FS::Record );
+@EXPORT_OK = qw( regionselector );
+
+$DEBUG = 1;
+$me = '[FS::tax_rate]';
+
+@tax_rate = ();
+$countyflag = '';
+
+#ask FS::UID to run this stuff for us later
+$FS::UID::callback{'FS::tax_rate'} = sub {
+ $conf = new FS::Conf;
+};
+
+=head1 NAME
+
+FS::tax_rate - Object methods for tax_rate objects
+
+=head1 SYNOPSIS
+
+ use FS::tax_rate;
+
+ $record = new FS::tax_rate \%hash;
+ $record = new FS::tax_rate { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+ ($county_html, $state_html, $country_html) =
+ FS::tax_rate::regionselector( $county, $state, $country );
+
+=head1 DESCRIPTION
+
+An FS::tax_rate object represents a tax rate, defined by locale.
+FS::tax_rate inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item taxnum
+
+primary key (assigned automatically for new tax rates)
+
+=item geocode
+
+a geographic location code provided by a tax data vendor
+
+=item data_vendor
+
+the tax data vendor
+
+=item location
+
+a location code provided by a tax authority
+
+=item taxclassnum
+
+a foreign key into FS::tax_class - the type of tax
+referenced but FS::part_pkg_taxrate
+
+=item effective_date
+
+the time after which the tax applies
+
+=item tax
+
+percentage
+
+=item excessrate
+
+second bracket percentage
+
+=item taxbase
+
+the amount to which the tax applies (first bracket)
+
+=item taxmax
+
+a cap on the amount of tax if a cap exists
+
+=item usetax
+
+percentage on out of jurisdiction purchases
+
+=item useexcessrate
+
+second bracket percentage on out of jurisdiction purchases
+
+=item unittype
+
+one of the values in %tax_unittypes
+
+=item fee
+
+amount of tax per unit
+
+=item excessfee
+
+second bracket amount of tax per unit
+
+=item feebase
+
+the number of units to which the fee applies (first bracket)
+
+=item feemax
+
+the most units to which fees apply (first and second brackets)
+
+=item maxtype
+
+a value from %tax_maxtypes indicating how brackets accumulate (i.e. monthly, per invoice, etc)
+
+=item taxname
+
+if defined, printed on invoices instead of "Tax"
+
+=item taxauth
+
+a value from %tax_authorities
+
+=item basetype
+
+a value from %tax_basetypes indicating the tax basis
+
+=item passtype
+
+a value from %tax_passtypes indicating how the tax should displayed to the customer
+
+=item passflag
+
+'Y', 'N', or blank indicating the tax can be passed to the customer
+
+=item setuptax
+
+if 'Y', this tax does not apply to setup fees
+
+=item recurtax
+
+if 'Y', this tax does not apply to recurring fees
+
+=item manual
+
+if 'Y', has been manually edited
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new tax rate. To add the tax rate to the database, see L<"insert">.
+
+=cut
+
+sub table { 'tax_rate'; }
+
+=item insert
+
+Adds this tax rate to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this tax rate from the database. If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid tax rate. If there is an error,
+returns the error, otherwise returns false. Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ foreach (qw( taxbase taxmax )) {
+ $self->$_(0) unless $self->$_;
+ }
+
+ $self->ut_numbern('taxnum')
+ || $self->ut_text('geocode')
+ || $self->ut_textn('data_vendor')
+ || $self->ut_textn('location')
+ || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
+ || $self->ut_numbern('effective_date')
+ || $self->ut_float('tax')
+ || $self->ut_floatn('excessrate')
+ || $self->ut_money('taxbase')
+ || $self->ut_money('taxmax')
+ || $self->ut_floatn('usetax')
+ || $self->ut_floatn('useexcessrate')
+ || $self->ut_numbern('unittype')
+ || $self->ut_floatn('fee')
+ || $self->ut_floatn('excessfee')
+ || $self->ut_floatn('feemax')
+ || $self->ut_numbern('maxtype')
+ || $self->ut_textn('taxname')
+ || $self->ut_numbern('taxauth')
+ || $self->ut_numbern('basetype')
+ || $self->ut_numbern('passtype')
+ || $self->ut_enum('passflag', [ '', 'Y', 'N' ])
+ || $self->ut_enum('setuptax', [ '', 'Y' ] )
+ || $self->ut_enum('recurtax', [ '', 'Y' ] )
+ || $self->ut_enum('manual', [ '', 'Y' ] )
+ || $self->SUPER::check
+ ;
+
+}
+
+=item taxclass_description
+
+Returns the human understandable value associated with the related
+FS::tax_class.
+
+=cut
+
+sub taxclass_description {
+ my $self = shift;
+ my $tax_class = qsearchs('tax_class', {'taxclassnum' => $self->taxclassnum });
+ $tax_class ? $tax_class->description : '';
+}
+
+=item unittype_name
+
+Returns the human understandable value associated with the unittype column
+
+=cut
+
+%tax_unittypes = ( '0' => 'access line',
+ '1' => 'minute',
+ '2' => 'account',
+);
+
+sub unittype_name {
+ my $self = shift;
+ $tax_unittypes{$self->unittype};
+}
+
+=item maxtype_name
+
+Returns the human understandable value associated with the maxtype column
+
+=cut
+
+%tax_maxtypes = ( '0' => 'receipts per invoice',
+ '1' => 'receipts per item',
+ '2' => 'total utility charges per utility tax year',
+ '3' => 'total charges per utility tax year',
+ '4' => 'receipts per access line',
+ '9' => 'monthly receipts per location',
+);
+
+sub maxtype_name {
+ my $self = shift;
+ $tax_maxtypes{$self->maxtype};
+}
+
+=item basetype_name
+
+Returns the human understandable value associated with the basetype column
+
+=cut
+
+%tax_basetypes = ( '0' => 'sale price',
+ '1' => 'gross receipts',
+ '2' => 'sales taxable telecom revenue',
+ '3' => 'minutes carried',
+ '4' => 'minutes billed',
+ '5' => 'gross operating revenue',
+ '6' => 'access line',
+ '7' => 'account',
+ '8' => 'gross revenue',
+ '9' => 'portion gross receipts attributable to interstate service',
+ '10' => 'access line',
+ '11' => 'gross profits',
+ '12' => 'tariff rate',
+ '14' => 'account',
+);
+
+sub basetype_name {
+ my $self = shift;
+ $tax_basetypes{$self->basetype};
+}
+
+=item taxauth_name
+
+Returns the human understandable value associated with the taxauth column
+
+=cut
+
+%tax_authorities = ( '0' => 'federal',
+ '1' => 'state',
+ '2' => 'county',
+ '3' => 'city',
+ '4' => 'local',
+ '5' => 'county administered by state',
+ '6' => 'city administered by state',
+ '7' => 'city administered by county',
+ '8' => 'local administered by state',
+ '9' => 'local administered by county',
+);
+
+sub taxauth_name {
+ my $self = shift;
+ $tax_authorities{$self->taxauth};
+}
+
+=item passtype_name
+
+Returns the human understandable value associated with the passtype column
+
+=cut
+
+%tax_passtypes = ( '0' => 'separate tax line',
+ '1' => 'separate surcharge line',
+ '2' => 'surcharge not separated',
+ '3' => 'included in base rate',
+);
+
+sub passtype_name {
+ my $self = shift;
+ $tax_passtypes{$self->passtype};
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
+
+=cut
+
+sub regionselector {
+ my ( $selected_county, $selected_state, $selected_country,
+ $prefix, $onchange, $disabled ) = @_;
+
+ $prefix = '' unless defined $prefix;
+
+ $countyflag = 0;
+
+# unless ( @tax_rate ) { #cache
+ @tax_rate = qsearch('tax_rate', {} );
+ foreach my $c ( @tax_rate ) {
+ $countyflag=1 if $c->county;
+ #push @{$tax_rate{$c->country}{$c->state}}, $c->county;
+ $tax_rate{$c->country}{$c->state}{$c->county} = 1;
+ }
+# }
+ $countyflag=1 if $selected_county;
+
+ my $script_html = <<END;
+ <SCRIPT>
+ function opt(what,value,text) {
+ var optionName = new Option(text, value, false, false);
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+ function ${prefix}country_changed(what) {
+ country = what.options[what.selectedIndex].text;
+ for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
+ what.form.${prefix}state.options[i] = null;
+END
+ #what.form.${prefix}state.options[0] = new Option('', '', false, true);
+
+ foreach my $country ( sort keys %tax_rate ) {
+ $script_html .= "\nif ( country == \"$country\" ) {\n";
+ foreach my $state ( sort keys %{$tax_rate{$country}} ) {
+ ( my $dstate = $state ) =~ s/[\n\r]//g;
+ my $text = $dstate || '(n/a)';
+ $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
+ }
+ $script_html .= "}\n";
+ }
+
+ $script_html .= <<END;
+ }
+ function ${prefix}state_changed(what) {
+END
+
+ if ( $countyflag ) {
+ $script_html .= <<END;
+ state = what.options[what.selectedIndex].text;
+ country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
+ for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
+ what.form.${prefix}county.options[i] = null;
+END
+
+ foreach my $country ( sort keys %tax_rate ) {
+ $script_html .= "\nif ( country == \"$country\" ) {\n";
+ foreach my $state ( sort keys %{$tax_rate{$country}} ) {
+ $script_html .= "\nif ( state == \"$state\" ) {\n";
+ #foreach my $county ( sort @{$tax_rate{$country}{$state}} ) {
+ foreach my $county ( sort keys %{$tax_rate{$country}{$state}} ) {
+ my $text = $county || '(n/a)';
+ $script_html .=
+ qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
+ }
+ $script_html .= "}\n";
+ }
+ $script_html .= "}\n";
+ }
+ }
+
+ $script_html .= <<END;
+ }
+ </SCRIPT>
+END
+
+ my $county_html = $script_html;
+ if ( $countyflag ) {
+ $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
+ $county_html .= '</SELECT>';
+ } else {
+ $county_html .=
+ qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
+ }
+
+ my $state_html = qq!<SELECT NAME="${prefix}state" !.
+ qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
+ foreach my $state ( sort keys %{ $tax_rate{$selected_country} } ) {
+ my $text = $state || '(n/a)';
+ my $selected = $state eq $selected_state ? 'SELECTED' : '';
+ $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
+ }
+ $state_html .= '</SELECT>';
+
+ $state_html .= '</SELECT>';
+
+ my $country_html = qq!<SELECT NAME="${prefix}country" !.
+ qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
+ my $countrydefault = $conf->config('countrydefault') || 'US';
+ foreach my $country (
+ sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
+ keys %tax_rate
+ ) {
+ my $selected = $country eq $selected_country ? ' SELECTED' : '';
+ $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
+ }
+ $country_html .= '</SELECT>';
+
+ ($county_html, $state_html, $country_html);
+
+}
+
+sub batch_import {
+ my $param = shift;
+
+ my $fh = $param->{filehandle};
+ my $format = $param->{'format'};
+
+ my @fields;
+ my $hook;
+ if ( $format eq 'cch' ) {
+ @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
+ excessrate effective_date taxauth taxtype taxcat taxname
+ usetax useexcessrate fee unittype feemax maxtype passflag
+ passtype basetype );
+ $hook = sub {
+ my $hash = shift;
+
+ $hash->{'effective_date'} = str2time($hash->{'effective_date'});
+
+ my $taxclassid =
+ join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
+
+ my %tax_class = ( 'data_vendor' => 'cch',
+ 'taxclass' => $taxclassid,
+ );
+
+ my $tax_class = qsearchs( 'tax_class', \%tax_class );
+ return "Error inserting tax rate: no tax class $taxclassid"
+ unless $tax_class;
+
+ $hash->{'taxclassnum'} = $tax_class->taxclassnum;
+
+ foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
+ delete($hash->{$_});
+ }
+
+ my %passflagmap = ( '0' => '',
+ '1' => 'Y',
+ '2' => 'N',
+ );
+ $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
+ if exists $passflagmap{$hash->{'passflag'}};
+
+ foreach (keys %$hash) {
+ $hash->{$_} = substr($hash->{$_}, 0, 80)
+ if length($hash->{$_}) > 80;
+ }
+
+ };
+
+ } elsif ( $format eq 'extended' ) {
+ die "unimplemented\n";
+ @fields = qw( );
+ $hook = sub {};
+ } else {
+ die "unknown format $format";
+ }
+
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+
+ my $imported = 0;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $line;
+ while ( defined($line=<$fh>) ) {
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ warn "$me batch_import: $imported\n"
+ if (!($imported % 100) && $DEBUG);
+
+ my @columns = $csv->fields();
+
+ my %tax_rate = ( 'data_vendor' => $format );
+ foreach my $field ( @fields ) {
+ $tax_rate{$field} = shift @columns;
+ }
+ my $error = &{$hook}(\%tax_rate);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ my $tax_rate = new FS::tax_rate( \%tax_rate );
+ $error = $tax_rate->insert;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert tax_rate for $line: $error";
+ }
+
+ $imported++;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
+=back
+
+=head1 BUGS
+
+regionselector? putting web ui components in here? they should probably live
+somewhere else...
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+