From 4104f4e3d1b387296b16b4a035b4b7f42e0c5977 Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 1 Apr 2008 00:54:44 +0000 Subject: [PATCH] checkpoint of new tax rating system --- FS/FS/Conf.pm | 7 + FS/FS/Schema.pm | 111 ++++++ FS/FS/cust_tax_location.pm | 208 ++++++++++ FS/FS/part_pkg.pm | 30 ++ FS/FS/part_pkg_taxoverride.pm | 119 ++++++ FS/FS/part_pkg_taxproduct.pm | 124 ++++++ FS/FS/part_pkg_taxrate.pm | 336 +++++++++++++++++ FS/FS/tax_class.pm | 249 ++++++++++++ FS/FS/tax_rate.pm | 605 ++++++++++++++++++++++++++++++ FS/MANIFEST | 15 + FS/t/cust_tax_location.t | 5 + FS/t/part_pkg_taxoverride.t | 5 + FS/t/part_pkg_taxproduct.t | 5 + FS/t/part_pkg_taxrate.t | 5 + FS/t/tax_class.t | 5 + FS/t/tax_rate.t | 5 + htetc/handler.pl | 8 +- httemplate/browse/tax_rate.cgi | 205 ++++++++++ httemplate/edit/part_pkg.cgi | 42 ++- httemplate/edit/part_pkg_taxoverride.html | 61 +++ httemplate/edit/part_pkg_taxproduct.html | 42 +++ httemplate/edit/process/part_pkg.cgi | 13 +- httemplate/edit/process/tax_class.html | 49 +++ httemplate/edit/process/tax_rate.html | 13 + httemplate/edit/tax_class.html | 36 ++ httemplate/edit/tax_rate.html | 105 ++++++ httemplate/elements/menu.html | 1 + httemplate/misc/process/recharge_svc.new | 85 +++++ httemplate/misc/process/tax-import.cgi | 58 +++ httemplate/misc/tax-import.cgi | 91 +++++ 30 files changed, 2640 insertions(+), 3 deletions(-) create mode 100644 FS/FS/cust_tax_location.pm create mode 100644 FS/FS/part_pkg_taxoverride.pm create mode 100644 FS/FS/part_pkg_taxproduct.pm create mode 100644 FS/FS/part_pkg_taxrate.pm create mode 100644 FS/FS/tax_class.pm create mode 100644 FS/FS/tax_rate.pm create mode 100644 FS/t/cust_tax_location.t create mode 100644 FS/t/part_pkg_taxoverride.t create mode 100644 FS/t/part_pkg_taxproduct.t create mode 100644 FS/t/part_pkg_taxrate.t create mode 100644 FS/t/tax_class.t create mode 100644 FS/t/tax_rate.t create mode 100755 httemplate/browse/tax_rate.cgi create mode 100644 httemplate/edit/part_pkg_taxoverride.html create mode 100644 httemplate/edit/part_pkg_taxproduct.html create mode 100644 httemplate/edit/process/tax_class.html create mode 100644 httemplate/edit/process/tax_rate.html create mode 100644 httemplate/edit/tax_class.html create mode 100644 httemplate/edit/tax_rate.html create mode 100755 httemplate/misc/process/recharge_svc.new create mode 100644 httemplate/misc/process/tax-import.cgi create mode 100644 httemplate/misc/tax-import.cgi diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 94f2f0579..d1f64b208 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 Text::Template documentation for details on the template substitution language. The following variables are available', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 9548aa760..e431b0764 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 000000000..11faa3f1f --- /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 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, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 84502b745..dc0a4d58a 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). + +=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). + +=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 000000000..656fe53e2 --- /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 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, 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 000000000..000d0d46b --- /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 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, 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 000000000..aa1c3df76 --- /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 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, 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 000000000..0a939adf6 --- /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 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, 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 000000000..38e53434e --- /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 = < + 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 .= <= 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 + + my $county_html = $script_html; + if ( $countyflag ) { + $county_html .= qq!'; + } else { + $county_html .= + qq!!; + } + + my $state_html = qq!'; + + $state_html .= ''; + + my $country_html = qq!'; + + ($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, L, L, schema.html from the base +documentation. + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 635bc04c0..23df38570 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -48,6 +48,7 @@ FS/UID.pm FS/Msgcat.pm FS/Pony.pm FS/acct_snarf.pm +FS/addr_block.pm FS/agent.pm FS/agent_type.pm FS/cust_bill.pm @@ -167,6 +168,7 @@ FS/cust_tax_exempt.pm FS/cust_tax_exempt_pkg.pm FS/clientapi_session.pm FS/clientapi_session_field.pm +t/addr_block.t t/agent.t t/agent_type.t t/AccessRight.t @@ -286,6 +288,7 @@ t/rate_prefix.t t/radius_usergroup.t t/reg_code.t t/reg_code_pkg.t +t/router.t t/session.t t/svc_acct.t t/svc_acct_pop.t @@ -394,3 +397,15 @@ FS/cust_pay_pending.pm t/cust_pay_pending.t FS/part_pkg_taxclass.pm t/part_pkg_taxclass.t +FS/tax_rate.pm +t/tax_rate.t +FS/tax_class.pm +t/tax_class.t +FS/cust_tax_location.pm +t/cust_tax_location.t +FS/part_pkg_taxproduct.pm +t/part_pkg_taxproduct.t +FS/part_pkg_taxoverride.pm +t/part_pkg_taxoverride.t +FS/part_pkg_taxrate.pm +t/part_pkg_taxrate.t diff --git a/FS/t/cust_tax_location.t b/FS/t/cust_tax_location.t new file mode 100644 index 000000000..83a1362c7 --- /dev/null +++ b/FS/t/cust_tax_location.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_tax_location; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_pkg_taxoverride.t b/FS/t/part_pkg_taxoverride.t new file mode 100644 index 000000000..d3b385d9c --- /dev/null +++ b/FS/t/part_pkg_taxoverride.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_pkg_taxoverride; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_pkg_taxproduct.t b/FS/t/part_pkg_taxproduct.t new file mode 100644 index 000000000..a0aaa1d1d --- /dev/null +++ b/FS/t/part_pkg_taxproduct.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_pkg_taxproduct; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_pkg_taxrate.t b/FS/t/part_pkg_taxrate.t new file mode 100644 index 000000000..6e5bee0aa --- /dev/null +++ b/FS/t/part_pkg_taxrate.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_pkg_taxrate; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/tax_class.t b/FS/t/tax_class.t new file mode 100644 index 000000000..ddd8d9f04 --- /dev/null +++ b/FS/t/tax_class.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::tax_class; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/tax_rate.t b/FS/t/tax_rate.t new file mode 100644 index 000000000..d498812d3 --- /dev/null +++ b/FS/t/tax_rate.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::tax_rate; +$loaded=1; +print "ok 1\n"; diff --git a/htetc/handler.pl b/htetc/handler.pl index caa266df0..d2fe52df0 100644 --- a/htetc/handler.pl +++ b/htetc/handler.pl @@ -149,9 +149,9 @@ sub handler use FS::cust_credit_bill; use FS::cust_main qw(smart_search); use FS::cust_main_county; - use FS::part_pkg_taxclass; use FS::cust_pay; use FS::cust_pkg; + use FS::part_pkg_taxclass; use FS::cust_pkg_reason; use FS::cust_refund; use FS::cust_svc; @@ -207,6 +207,12 @@ sub handler use FS::reason_type; use FS::reason; use FS::cust_main_note; + use FS::tax_class; + use FS::cust_tax_location; + use FS::part_pkg_taxproduct; + use FS::part_pkg_taxoverride; + use FS::part_pkg_taxrate; + use FS::tax_rate; if ( %%%RT_ENABLED%%% ) { eval ' diff --git a/httemplate/browse/tax_rate.cgi b/httemplate/browse/tax_rate.cgi new file mode 100755 index 000000000..b401b3786 --- /dev/null +++ b/httemplate/browse/tax_rate.cgi @@ -0,0 +1,205 @@ +<% include( 'elements/browse.html', + 'title' => "Tax Rates $title", + 'name_singular' => 'tax rate', + 'menubar' => \@menubar, + 'html_init' => $html_init, + 'query' => { + 'table' => 'tax_rate', + 'hashref' => $hashref, + 'order_by' => 'ORDER BY geocode, taxclassnum', + }, + 'count_query' => $count_query, + 'header' => \@header, + 'header2' => \@header2, + 'fields' => \@fields, + 'align' => $align, + 'color' => \@color, + 'cell_style' => \@cell_style, + 'links' => \@links, + 'link_onclicks' => \@link_onclicks, + ) +%> +<%once> + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $exempt_sub = sub { + my $tax_rate = shift; + + my @exempt = (); + push @exempt, + sprintf("$money_char%.2f per month", $tax_rate->exempt_amount ) + if $tax_rate->exempt_amount > 0; + + push @exempt, 'Setup fee' + if $tax_rate->setuptax =~ /^Y$/i; + + push @exempt, 'Recurring fee' + if $tax_rate->recurtax =~ /^Y$/i; + + [ map [ {'data'=>$_} ], @exempt ]; +}; + +my $oldrow; +my $cell_style; +my $cell_style_sub = sub { + my $row = shift; + if ( $oldrow ne $row ) { + if ( $oldrow ) { + if ( $oldrow->country ne $row->country ) { + $cell_style = 'border-top:1px solid #000000'; + } elsif ( $oldrow->state ne $row->state ) { + $cell_style = 'border-top:1px solid #cccccc'; #default? + } elsif ( $oldrow->state eq $row->state ) { + #$cell_style = 'border-top:dashed 1px dark gray'; + $cell_style = 'border-top:1px dashed #cccccc'; + } + } + $oldrow = $row; + } + return $cell_style; +}; + +my $select_link = [ 'javascript:void(0);', sub { ''; } ]; + +my $select_onclick = sub { + my $row = shift; + my $taxnum = $row->taxnum; + my $color = '#333399'; + qq!overlib( OLiframeContent('${p}edit/tax_rate.html?$taxnum', 540, 420, 'edit_tax_rate_popup' ), CAPTION, 'Edit tax rate', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK, BGCOLOR, '$color', CGCOLOR, '$color' ); return false;!; +}; + +my $separate_taxclasses_link = sub { + my( $row ) = @_; + my $taxnum = $row->taxnum; + my $url = "${p}edit/process/tax_rate-expand.cgi?taxclassnum=1;taxnum=$taxnum"; + + qq!!; +}; + + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my @menubar; + +my $html_init = + "Click on geocodes to specify rates for a new area."; +$html_init .= "
Click on separate taxclasses to specify taxes per taxclass."; +$html_init .= '

'; + +$html_init .= qq( + + + + +); + +my $title = ''; +my $select_word = 'edit'; + +my $geocode = ''; +if ( $cgi->param('geocode') =~ /^(\w+)$/ ) { + $geocode = $1; + $title = "$geocode"; +} +$cgi->delete('geocode'); + +$title = " for $title" if $title; + +my $taxclassnum = ''; +if ( $cgi->param('taxclassnum') =~ /^(\d+)$/ ) { + $taxclassnum = $1; + my $tax_class = qsearchs('tax_class', {'taxclassnum' => $taxclassnum}); + if ($tax_class) { + $title .= " for ". $tax_class->taxclass. + " (". $tax_class->description. ") tax class"; + }else{ + $taxclassnum = ''; + } +} +$cgi->delete('taxclassnum'); + +if ( $geocode || $taxclassnum ) { + push @menubar, 'View all tax rates' => $p.'browse/tax_rate.cgi'; +} + +$cgi->param('dummy', 1); + +#restore this so pagination works +$cgi->param('geocode', $geocode) if $geocode; +$cgi->param('taxclassnum', $taxclassnum ) if $taxclassnum; + +my $hashref = {}; +my $count_query = 'SELECT COUNT(*) FROM tax_rate'; +if ( $geocode ) { + $hashref->{'geocode'} = $geocode; + $count_query .= ' WHERE geocode = '. dbh->quote($geocode); +} +if ( $taxclassnum ) { + $hashref->{'taxclassnum'} = $taxclassnum; + $count_query .= ( $count_query =~ /WHERE/i ? ' AND ' : ' WHERE ' ). + ' taxclassnum = '. dbh->quote($taxclassnum); +} + + +$cell_style = ''; + +my @header = ( 'Location Code', ); +my @header2 = ( '', ); +my @links = ( '', ); +my @link_onclicks = ( '', ); +my $align = 'l'; + +my @fields = ( + 'geocode', +); + +my @color = ( + '000000', +); + +push @header, qq!Tax class (
add new)!; +push @header2, '(per-tax classification)'; +push @fields, sub { $_[0]->taxclass_description || '(all) '. + &{$separate_taxclasses_link}($_[0], 'Separate Taxclasses'). + 'separate taxclasses
' + }; +push @color, sub { shift->taxclass ? '000000' : '999999' }; +push @links, ''; +push @link_onclicks, ''; +$align .= 'l'; + +push @header, 'Tax name', + 'Rate', #'Tax', + 'Exemptions', + ; + +push @header2, '(printed on invoices)', + '', + '', + ; + +push @fields, + sub { shift->taxname || 'Tax' }, + sub { shift->tax. '% ('. $select_word. ')' }, + $exempt_sub, +; + +push @color, + sub { shift->taxname ? '000000' : '666666' }, + sub { shift->tax ? '000000' : '666666' }, + '000000', +; + +$align .= 'lrl'; + +my @cell_style = map $cell_style_sub, (1..scalar(@header)); + +push @links, '', $select_link, ''; +push @link_onclicks, '', $select_onclick, ''; + + diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi index b1851a7ba..84f7498e1 100755 --- a/httemplate/edit/part_pkg.cgi +++ b/httemplate/edit/part_pkg.cgi @@ -3,6 +3,11 @@ )) %> % #), ' onLoad="visualize()"'); + + + + + <% include('/elements/error.html') %>
@@ -81,6 +86,34 @@ Tax information % } +% if ( $conf->exists('enable_taxproducts') ) { + + + <% ntable("#cccccc", 2) %> + + Tax product + + + + + + + + + + <% $tax_override ? 'Edit tax overrides' : 'Override taxes' %> + + + + + + +% } else { + + + +% } +
@@ -234,7 +267,7 @@ Line-item revenue recognition %#} else { %# push @fixups, 'taxclass'; #hidden %#} -%my @form_elements = ( 'classnum', 'taxclass', 'agent_type' ); +%my @form_elements = ( 'classnum', 'taxclass', 'agent_type', 'tax_override' ); % %my @form_radio = ( 'pkg_svc_primary' ); % @@ -252,6 +285,7 @@ Line-item revenue recognition % 'form_elements' => \@form_elements, % 'form_text' => [ qw(pkg comment promo_code clone pkgnum pkgpart), % qw(pay_weight credit_weight), #keys(%weight), +% qw(taxproductnum), % @fixups, % ], % 'form_checkbox' => [ qw(setuptax recurtax disabled) ], @@ -409,12 +443,14 @@ my ($query) = $cgi->keywords; my $conf = new FS::Conf; my $part_pkg = ''; my @agent_type = (); +my $tax_override; my @all_agent_types = map {$_->typenum} qsearch('agent_type',{}); if ( $cgi->param('error') ) { $part_pkg = new FS::part_pkg ( { map { $_, scalar($cgi->param($_)) } fields('part_pkg') } ); (@agent_type) = $cgi->param('agent_type'); + $tax_override = $cgi->param('tax_override'); } my $action = ''; @@ -430,6 +466,9 @@ if ( $cgi->param('clone') ) { } elsif ( $query && $query =~ /^(\d+)$/ ) { (@agent_type) = map {$_->typenum} qsearch('type_pkgs',{'pkgpart'=>$1}) unless $part_pkg; + $tax_override = + join (",", map {$_->taxnum} qsearch('part_pkg_taxoverride',{'pkgpart'=>$1})) + unless $part_pkg; $part_pkg ||= qsearchs('part_pkg',{'pkgpart'=>$1}); $pkgpart = $part_pkg->pkgpart; } else { @@ -447,5 +486,6 @@ unless ( $part_pkg->plan ) { #backwards-compat } $action ||= $part_pkg->pkgpart ? 'Edit' : 'Add'; my $hashref = $part_pkg->hashref; +my $taxproduct_description = $part_pkg->taxproduct_description; diff --git a/httemplate/edit/part_pkg_taxoverride.html b/httemplate/edit/part_pkg_taxoverride.html new file mode 100644 index 000000000..61cca1fbc --- /dev/null +++ b/httemplate/edit/part_pkg_taxoverride.html @@ -0,0 +1,61 @@ +<%doc> + + The crappy version + + +<% include('/elements/header-popup.html', 'Select tax product') %> + + + +<% ntable("#cccccc", 2) %> + + Tax override + + <% include( '/elements/select-table.html', + 'table' => 'tax_rate', + 'name_col' => 'taxname', + 'curr_value' => \@curr_value, + 'element_etc' => "id='taxoverride_popup_select'", + 'multiple' => '1', + ) + %> + + + +

+
+ +<% include('/elements/footer.html') %> + +<%init> + +my $conf = new FS::Conf; + +my @curr_value; +my ( $query ) = $cgi->keywords; +$query =~ /^([\d,]+)$/; +push @curr_value, split ',', $1 + if $1; + +unless (scalar(@curr_value)) { + #push @curr_value, map {$_=>taxnum} $part_pkg->tax_rate; +} + + diff --git a/httemplate/edit/part_pkg_taxproduct.html b/httemplate/edit/part_pkg_taxproduct.html new file mode 100644 index 000000000..033c37f56 --- /dev/null +++ b/httemplate/edit/part_pkg_taxproduct.html @@ -0,0 +1,42 @@ +<% include('/elements/header-popup.html', 'Select tax product') %> + + + +<% ntable("#cccccc", 2) %> + + Tax product + + <% include( '/elements/select-table.html', + 'empty_label' => '(select product)', + 'table' => 'part_pkg_taxproduct', + 'name_col' => 'description', + 'curr_value' => $curr_value, + 'element_etc' => "id='taxproduct_popup_select'", + ) + %> + + + +

+
+ +<% include('/elements/footer.html') %> + +<%init> + +my $conf = new FS::Conf; + +my ( $query ) = $cgi->keywords; +$query =~ /^(\d+)$/; +my $curr_value = $1; + + diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi index 36debfce0..eac20af57 100755 --- a/httemplate/edit/process/part_pkg.cgi +++ b/httemplate/edit/process/part_pkg.cgi @@ -53,6 +53,9 @@ $error = "At least one agent type must be specified." !$pkgpart && $conf->exists('agent-defaultpkg') ); +$cgi->param('tax_override') =~ /^([\d,]+)$/; +my (@tax_overrides) = (grep "$_", split (",", $1)); + my $new = new FS::part_pkg ( { map { $_ => scalar($cgi->param($_)); @@ -103,11 +106,19 @@ if ( $error ) { } unless ( $error || $conf->exists('agent_defaultpkg') ) { - my $error = $new->process_m2m( + $error = $new->process_m2m( 'link_table' => 'type_pkgs', 'target_table' => 'agent_type', 'params' => \@agents, ); } +unless ( $error ) { + $error = $new->process_m2m( + 'link_table' => 'part_pkg_taxoverride', + 'target_table' => 'tax_rate', + 'params' => \@tax_overrides, + ); +} + diff --git a/httemplate/edit/process/tax_class.html b/httemplate/edit/process/tax_class.html new file mode 100644 index 000000000..339c9083e --- /dev/null +++ b/httemplate/edit/process/tax_class.html @@ -0,0 +1,49 @@ +% if ( $error ) { +% $cgi->param('error', $error); +<% $cgi->redirect(popurl(2). "tax_class.html?". $cgi->query_string ) %> +%} else { +<% $cgi->redirect(popurl(3). "browse/tax_rate.cgi?taxclassnum=". uri_escape($tax_class->taxclassnum) ) %> +%} +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $tax_class = new FS::tax_class { + 'taxclass' => $cgi->param('taxclass'), + 'description' => $cgi->param('description'), +}; + +#maybe this whole thing should be in a transaction. at some point, no biggie +#none of the follow-up stuff will fail unless there's a more serious problem +#than a hanging record in tax_class... + +my $error = $tax_class->insert; + +# all of this is highly dubious at the moment + +#unless ( $error ) { +# #auto-add the new taxclass to any regions that have taxclasses already +# +# my $sth = dbh->prepare(" +# SELECT geocode FROM tax_rate +# WHERE taxclass IS NOT NULL AND taxclass != '' +# GROUP BY geocode +# ") or die dbh->errstr; +# $sth->execute or die $sth->errstr; +# +# while ( my $row = $sth->fetchrow_hashref ) { +# warn "inserting for $row"; +# my $cust_main_county = new FS::tax_rate { +# 'geocode' => $row->{geocode}, +# 'tax' => 0, +# 'taxclassnum' => $tax_class->taxclassnum, +# }; +# $error = $cust_main_county->insert; +# #last if $error; +# die $error if $error; +# } +# +#} + + diff --git a/httemplate/edit/process/tax_rate.html b/httemplate/edit/process/tax_rate.html new file mode 100644 index 000000000..933bf07d6 --- /dev/null +++ b/httemplate/edit/process/tax_rate.html @@ -0,0 +1,13 @@ +<% include( 'elements/process.html', + 'table' => 'tax_rate', + 'popup_reload' => 'Tax changed', #a popup "parent reload" for now + #someday change the individual element and go away instead + ) +%> +<%init> + +my $conf = new FS::Conf; +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + + diff --git a/httemplate/edit/tax_class.html b/httemplate/edit/tax_class.html new file mode 100644 index 000000000..d3e2e821f --- /dev/null +++ b/httemplate/edit/tax_class.html @@ -0,0 +1,36 @@ +<% include('/elements/header.html', "$action taxclass") %> + +<% include('/elements/error.html') %> + +
+ + + + +Tax class
+Description + +

+ + +
+ +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $taxclass = ''; +my $description = ''; +if ( $cgi->param('error') ) { + $taxclass = $cgi->param('taxclass'); + $description = $cgi->param('description'); +} + +my $action = 'Add'; + +my $p1 = popurl(1); + + diff --git a/httemplate/edit/tax_rate.html b/httemplate/edit/tax_rate.html new file mode 100644 index 000000000..e1d8d4f93 --- /dev/null +++ b/httemplate/edit/tax_rate.html @@ -0,0 +1,105 @@ +<% include('elements/edit.html', + 'popup' => 1, + 'name' => 'Tax rate', #Edit tax rate + 'table' => 'tax_rate', + 'labels' => $labels, + 'fields' => \@fields, + ) +%> +<%once> + +my $conf = new FS::Conf; + + + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $taxnum; +if ( $cgi->param('error') ) { + $cgi->param('taxnum') =~ /^(\d+)$/ or die 'error, but no taxnum'; + $taxnum = $1; +} else { + my($query) = $cgi->keywords; + $query =~ /^(\d+)$/ or die 'no taxnum'; + $taxnum = $1; +} + +my $tax_rate = qsearchs('tax_rate', { 'taxnum' => $taxnum }) + or die "unknown taxnum $1"; + +my $labels = { 'taxnum' => 'Tax', + 'data_vendor' => 'Data vendor', + 'geocode' => 'Vendor location code', + 'location' => 'Tax auth loc code', + 'taxclass_description' => 'Tax class', + 'taxname' => 'Tax name', + 'effective_date' => 'Effective date', + 'tax' => 'Tax rate (1st bracket)', + 'excessrate' => 'Tax rate (2nd bracket)', + 'taxbase' => 'First bracket', + 'taxmax' => 'Max tax', + 'usetax' => 'Use tax rate (1st bracket)', + 'useexcessrate' => 'Use tax rate (2nd bracket)', + 'unittype_name' => 'Units', + 'fee' => 'Fee per unit (1st bracket)', + 'excessfee' => 'Fee per unit (2st bracket)', + 'feebase' => 'Units in first bracket', + 'feemax' => 'Max Units', + 'maxtype_name' => 'Threshold accumulation', + 'taxauth_name', => 'Tax authority', + 'basetype_name' => 'Basis', + 'passtype_name' => 'Passthru', + 'passflag' => 'Passable', + 'setuptax' => 'This tax not applicable to setup fees', + 'recurtax' => 'This tax not applicable to recurring fees', + }; + +my @fields = ( + { type=>'tablebreak-tr-title', value=>'Location' }, + { field=>'data_vendor', type=>'hidden',}, + { field=>'geocode', type=>'fixed' }, + { field=>'taxclassnum', type=>'hidden' } , + { field=>'taxclass_description', type=>'fixed' } , + { field=>'taxname', type=>'text' } , + { field=>'effective_date', type=>'fixed' } , + { field=>'location', type=>'text' }, + { type=>'tablebreak-tr-title', value=>'Money based rates' }, + { field=>'tax', type=>'percentage' } , + { field=>'excessrate', type=>'percentage' } , + { field=>'taxbase', type=>'money' } , + { field=>'taxmax', type=>'money' } , + { field=>'usetax', type=>'percentage' } , + { field=>'useexcessrate', type=>'percentage' } , + { type=>'tablebreak-tr-title', value=>'Service based rates' }, + { field=>'unittype', type=>'hidden' } , + { field=>'unittype_name', type=>'fixed' } , + { field=>'fee', type=>'money' } , + { field=>'excessfee', type=>'money' } , + { field=>'feebase', type=>'text' } , + { field=>'feemax', type=>'text' } , + { type=>'tablebreak-tr-title', value=>'Taxation rules' }, + { field=>'maxtype', type=>'hidden' } , + { field=>'maxtype_name', type=>'fixed' } , + { field=>'taxauth', type=>'hidden' } , + { field=>'taxauth_name', type=>'fixed' } , + { field=>'basetype', type=>'hidden' } , + { field=>'basetype_name', type=>'fixed' } , + { field=>'passtype', type=>'hidden' } , + { field=>'passtype_name', type=>'fixed' } , + { field=>'passflag', type=>'fixed' } , + { field=>'setuptax', type=>'checkbox' } , + { field=>'recurtax', type=>'checkbox' } , + { field=>'manual', type=>'hidden', value=>'Y' } , +); + +#push @fields, +# { type=>'tablebreak-tr-title', value=>'Exemptions' }, +# { field=>'setuptax', type=>'checkbox', value=>'Y', }, +# { field=>'recurtax', type=>'checkbox', value=>'Y', }, +# { field=>'exempt_amount', type=>'money', }, +#; + + diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 047671ae5..4e4d68719 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -217,6 +217,7 @@ $report_menu{'SQL Query'} = [ $fsurl.'search/report_sql.html', 'SQL Query' ] if $curuser->access_right('Raw SQL'); tie my %tools_importing, 'Tie::IxHash', + 'Import tax rates from CSV files' => [ $fsurl.'misc/tax-import.cgi', '' ], 'Import customers from CSV file' => [ $fsurl.'misc/cust_main-import.cgi', '' ], 'Import payments from CSV file' => [ $fsurl.'misc/cust_pay-import.cgi', '' ], diff --git a/httemplate/misc/process/recharge_svc.new b/httemplate/misc/process/recharge_svc.new new file mode 100755 index 000000000..bc916e5da --- /dev/null +++ b/httemplate/misc/process/recharge_svc.new @@ -0,0 +1,85 @@ +% +% +%#untaint svcnum +%my $svcnum = $cgi->param('svcnum'); +%$svcnum =~ /^(\d+)$/ || die "Illegal svcnum"; +%$svcnum = $1; +% +%#untaint prepaid +%my $prepaid = $cgi->param('prepaid'); +%$prepaid =~ /^(\w*)$/; +%$prepaid = $1; + +%#untaint payby +%my $payby = $cgi->param('payby'); +%$payby =~ /^([A-Z]*)$/; +%$payby = $1; +% +%my $error = ''; +%my $svc_acct = qsearchs( 'svc_acct', {'svcnum'=>$svcnum} ); +%$error = "Can't recharge service $svcnum. " unless $svc_acct; +% +%my $cust_main = $svc_acct->cust_svc->cust_pkg->cust_main; +% +%my $oldAutoCommit = $FS::UID::AutoCommit; +%local $FS::UID::AutoCommit = 0; +%my $dbh = dbh; +% +% +%unless ($error) { +% +% my ($amount, $seconds, $up, $down, $total) = (0, 0, 0, 0, 0); +% #should probably use payby.pm but whatever +% if ($payby eq 'PREP') { +% $error = $cust_main->get_prepay($prepaid, \$amount, \$seconds, \$up, \$down, \$total) +% || $svc_acct->increment_seconds($seconds) +% || $svc_acct->increment_upbytes($up) +% || $svc_acct->increment_downbytes($down) +% || $svc_acct->increment_totalbytes($total) +% || $cust_main->insert_cust_pay_prepay( $amount, $prepaid ); +% } elsif ( $payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP)$/ ) { +% my $part_pkg = $svc_acct->cust_svc->cust_pkg->part_pkg; +% $amount = $part_pkg->option('recharge_amount', 1); +% my %rhash = map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) } +% qw ( recharge_seconds recharge_upbytes recharge_downbytes +% recharge_totalbytes ); +% +% my $description = "Recharge"; +% $description .= " $rhash{seconds}s" if $rhash{seconds}; +% $description .= " $rhash{upbytes} up" if $rhash{upbytes}; +% $description .= " $rhash{downbytes} down" if $rhash{downbytes}; +% $description .= " $rhash{totalbytes} total" if $rhash{totalbytes}; +% +% $error = $cust_main->charge($amount, "Recharge " . $svc_acct->label, +% $description, $part_pkg->taxclass); +% +% $error ||= $svc_acct->recharge(\%rhash); +% +% my $old_balance = $cust_main->balance; +% $error ||= $cust_main->bill; +% $error ||= $cust_main->apply_payments_and_credits; +% my $bill_error = $cust_main->collect('realtime' => 1) unless $error; +% $error ||= "Failed to collect - $bill_error" +% if $cust_main->balance > $old_balance && $cust_main->balance > 0 +% && $payby ne 'BILL'; +% +% } else { +% $error = "fatal error - unknown payby: $payby"; +% } +%} +% +%if ($error) { +% $cgi->param('error', $error); +% $dbh->rollback if $oldAutoCommit; +% print $cgi->redirect(popurl(2). "recharge_svc.html?". $cgi->query_string ); +%} +%$dbh->commit or die $dbh->errstr if $oldAutoCommit; +% +<% header("Package recharged") %> + + +<%init> +my $conf = new FS::Conf; + diff --git a/httemplate/misc/process/tax-import.cgi b/httemplate/misc/process/tax-import.cgi new file mode 100644 index 000000000..77fba61f5 --- /dev/null +++ b/httemplate/misc/process/tax-import.cgi @@ -0,0 +1,58 @@ +% if ( $error ) { +% warn $error; +% errorpage($error); +% } else { + <% include('/elements/header.html','Import successful') %> + <% include('/elements/footer.html') %> +% } +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Import'); + +my $cfh = $cgi->upload('codefile'); +my $zfh = $cgi->upload('plus4file'); +my $tfh = $cgi->upload('txmatrix'); +my $dfh = $cgi->upload('detail'); +#warn $cgi; +#warn $fh; + +my $oldAutoCommit = $FS::UID::AutoCommit; +local $FS::UID::AutoCommit = 0; +my $dbh = dbh; + +my $error = defined($cfh) + ? FS::tax_class::batch_import( { + filehandle => $cfh, + 'format' => scalar($cgi->param('format')), + } ) + : 'No code file'; + +$error ||= defined($zfh) + ? FS::cust_tax_location::batch_import( { + filehandle => $zfh, + 'format' => scalar($cgi->param('format')), + } ) + : 'No plus4 file'; + +$error ||= defined($tfh) + ? FS::part_pkg_taxrate::batch_import( { + filehandle => $tfh, + 'format' => scalar($cgi->param('format')), + } ) + : 'No tax matrix file'; + +$error ||= defined($dfh) + ? FS::tax_rate::batch_import( { + filehandle => $dfh, + 'format' => scalar($cgi->param('format')), + } ) + : 'No tax detail file'; + +if ($error) { + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; +}else{ + $dbh->commit or die $dbh->errstr if $oldAutoCommit; +} + + diff --git a/httemplate/misc/tax-import.cgi b/httemplate/misc/tax-import.cgi new file mode 100644 index 000000000..1f60dbe00 --- /dev/null +++ b/httemplate/misc/tax-import.cgi @@ -0,0 +1,91 @@ +<% include("/elements/header.html",'Batch Tax Rate Import') %> + +Import a CSV file set containing tax rate records. +

+ +
+ +<% &ntable("#cccccc", 2) %> + + + Format + + + + + + + code CSV filename + + + + + plus4 CSV filename + + + + + txmatrix CSV filename + + + + + detail CSV filename + + + + + + + + +
+ +
+ + + +<%$req%> Required fields +

+ +Field information: + +
    + +
  • refnum: Advertising source number - where a customer heard about your service. Configuration -> Miscellaneous -> View/Edit advertising sources. This field has special treatment upon import: If a string is passed instead +of an integer, the string is searched for and if necessary auto-created in the +advertising source table. + +
  • payinfo: Credit card number, or leave this, paycvv and paydate blank for email/paper invoicing. + +
  • paycvv: CVV2 number (three digits on the back of the credit card) + +
  • paydate: Credit card expiration date, MM/YYYY or MM/YY (M/YY and M/YYYY are also accepted). + +
  • invoicing_list: Email address for invoices, or POST for postal invoices. + +
  • pkgpart: Package definition. Configuration -> Provisioning, services and packages -> View/Edit package definitions + +
  • username and _password are required if pkgpart is specified. +
+ +
+ +<% include('/elements/footer.html') %> + +<%once> + +my $req = qq!*!; + + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Import'); + + -- 2.11.0