diff options
30 files changed, 2640 insertions, 3 deletions
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 <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 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<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 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<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 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<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 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<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 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<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 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<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 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 = <<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; + 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!<FONT SIZE="-1"><A HREF="$url">!; +}; + +</%once> +<%init> + +die "access denied" +  unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my @menubar; + +my $html_init = +  "Click on <u>geocodes</u> to specify rates for a new area."; +$html_init .= "<BR>Click on <u>separate taxclasses</u> to specify taxes per taxclass."; +$html_init .= '<BR><BR>'; + +$html_init .= qq( +  <SCRIPT TYPE="text/javascript" SRC="${fsurl}elements/overlibmws.js"></SCRIPT> +  <SCRIPT TYPE="text/javascript" SRC="${fsurl}elements/overlibmws_iframe.js"></SCRIPT> +  <SCRIPT TYPE="text/javascript" SRC="${fsurl}elements/overlibmws_draggable.js"></SCRIPT> +  <SCRIPT TYPE="text/javascript" SRC="${fsurl}elements/iframecontentmws.js"></SCRIPT> +); + +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 (<A HREF="${p}edit/tax_class.html">add new</A>)!; +push @header2, '(per-tax classification)'; +push @fields, sub { $_[0]->taxclass_description || '(all) '. +                     &{$separate_taxclasses_link}($_[0], 'Separate Taxclasses'). +                     'separate taxclasses</A></FONT>' +                  }; +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. '% <FONT SIZE="-1">('. $select_word. ')</FONT>' }, +  $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, ''; + +</%init> 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()"');  +<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws.js"></SCRIPT> +<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_iframe.js"></SCRIPT> +<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/overlibmws_draggable.js"></SCRIPT> +<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/iframecontentmws.js"></SCRIPT> +  <% include('/elements/error.html') %>  <FORM NAME="dummy"> @@ -81,6 +86,34 @@ Tax information  % }  +% if ( $conf->exists('enable_taxproducts') ) {  + +  <TR><TD colspan="2"> +    <% ntable("#cccccc", 2) %> +      <TR> +        <TD align="right">Tax product</TD> +        <TD> +          <INPUT name="part_pkg_taxproduct_taxproductnum" id="taxproductnum" type="hidden" value="<% $hashref->{'taxproductnum'}%>"> +          <INPUT name="part_pkg_taxproduct_description" id="taxproduct_description" type="text" value="<% $taxproduct_description %>" size="12" onclick="overlib( OLiframeContent('part_pkg_taxproduct.html?'+document.getElementById('taxproductnum').value, 800, 400, 'tax_product_popup'), CAPTION, 'Select product', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK); return false;"> +        </TD> +      </TR> +      <TR> +        <TD colspan="2" align="right"> +          <INPUT name="tax_override" id="tax_override" type="hidden" value="<% $tax_override %>"> +          <A href="javascript:void(0)" onclick="overlib( OLiframeContent('part_pkg_taxoverride.html?'+document.getElementById('tax_override').value, 800, 400, 'tax_product_popup'), CAPTION, 'Edit product tax overrides', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK); return false;"> +            <% $tax_override ? 'Edit tax overrides' : 'Override taxes' %> +          </A> +        </TD> +      </TR> +    </TABLE> +  </TD></TR> + +% } else {  + +  <INPUT TYPE="hidden" NAME="taxproductnum" VALUE="<% $hashref->{taxproductnum} %>"> + +% }  +  </TABLE>  <BR> @@ -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;  </%init> 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 +   +</%doc> +<% include('/elements/header-popup.html', 'Select tax product') %> +<SCRIPT> +  function saveit2() { +    var num = parent.document.getElementById('tax_override'); +    var sel = document.getElementById('taxoverride_popup_select'); +    var value = ''; +    for (i=0; i< sel.length; i++) { +      if (sel.options[i].selected) { +        value = value + sel.options[i].value + ","; +      } +    } +    if (value.length > 0) { +      value = value.substr(0, value.length-1); +    } +       +    num.value = value; +    parent.cClick(); +  } +</SCRIPT> +<FORM="dummy" METHOD="POST" onsubmit="saveit2();return false;" > + +<% ntable("#cccccc", 2) %> +<TR> +  <TD align="left">Tax override</TD> +  <TD> +    <% include( '/elements/select-table.html', +                'table'       => 'tax_rate', +                'name_col'    => 'taxname', +                'curr_value'  => \@curr_value, +                'element_etc' => "id='taxoverride_popup_select'", +                'multiple'    => '1', +              ) +    %> +  </TD> +</TR> +</TABLE> +<BR><BR> +<CENTER><INPUT type="submit" value="Select"></CENTER> +</FORM> +<% 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; +} + +</%init> 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') %> +<SCRIPT> +  function saveit() { +    var num = parent.document.getElementById('taxproductnum'); +    var disp = parent.document.getElementById('taxproduct_description'); +    var sel = document.getElementById('taxproduct_popup_select'); +    num.value = sel.options[sel.selectedIndex].value; +    disp.value = sel.options[sel.selectedIndex].text; +    parent.cClick(); +  } +</SCRIPT> +<FORM="dummy" METHOD="POST" onsubmit="saveit();return false;" > + +<% ntable("#cccccc", 2) %> +<TR> +  <TD align="left">Tax product</TD> +  <TD> +    <% 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'", +              ) +    %> +  </TD> +</TR> +</TABLE> +<BR><BR> +<CENTER><INPUT type="submit" value="Select"></CENTER> +</FORM> +<% include('/elements/footer.html') %> + +<%init> + +my $conf = new FS::Conf; + +my ( $query ) = $cgi->keywords; +$query =~ /^(\d+)$/; +my $curr_value = $1; + +</%init> 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, +  ); +} +  </%init> 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; +#  } +# +#} + +</%init> 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'); + +</%init> 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') %> + +<FORM ACTION="<% $p1 %>process/tax_class.html" METHOD=POST> + +<INPUT TYPE="hidden" NAME="taxclassnum" VALUE=""> +<INPUT TYPE="hidden" NAME="data_vendor" VALUE=""> + +Tax class <INPUT TYPE="text" NAME="taxclass" VALUE="<% $taxclass |h %>"><BR> +Description <INPUT TYPE="text" NAME="description" VALUE="<% $description |h %>"> + +<BR><BR> +<INPUT TYPE="submit" VALUE="<% $action %> taxclass"> + +</FORM> + +<% 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); + +</%init> 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; + +</%once> + +<%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', }, +#; + +</%init> 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") %> +  <SCRIPT TYPE="text/javascript"> +    window.top.location.reload(); +  </SCRIPT> +  </BODY></HTML> +<%init> +my $conf = new FS::Conf; +</%init> 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; +} + +</%init> 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. +<BR><BR> + +<FORM ACTION="process/tax-import.cgi" METHOD="post" ENCTYPE="multipart/form-data"> + +<% &ntable("#cccccc", 2) %> + +<TR> +  <TH ALIGN="right">Format</TH> +  <TD> +    <SELECT NAME="format"> +      <OPTION VALUE="cch">CCH +<!--      <OPTION VALUE="extended" SELECTED>Extended +      <OPTION VALUE="extended-plus_company">Extended plus company --> +    </SELECT> +  </TD> +</TR> + +<TR> +  <TH ALIGN="right">code CSV filename</TH> +  <TD><INPUT TYPE="file" NAME="codefile"></TD> +</TR> + +<TR> +  <TH ALIGN="right">plus4 CSV filename</TH> +  <TD><INPUT TYPE="file" NAME="plus4file"></TD> +</TR> + +<TR> +  <TH ALIGN="right">txmatrix CSV filename</TH> +  <TD><INPUT TYPE="file" NAME="txmatrix"></TD> +</TR> + +<TR> +  <TH ALIGN="right">detail CSV filename</TH> +  <TD><INPUT TYPE="file" NAME="detail"></TD> +</TR> + + +<TR><TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px"><INPUT TYPE="submit" VALUE="Import CSV files"></TD></TR> + +</TABLE> + +</FORM> + +<BR> + +<!-- Simple file format is CSV, with the following field order: <i>cust_pkg.setup, dayphone, first, last, address1, address2, city, state, zip, comments</i> +<BR><BR> --> + +<%$req%> Required fields +<BR><BR> + +Field information: + +<ul> + +  <li><i>refnum</i>: 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. + +  <li><i>payinfo</i>: Credit card number, or leave this, <i>paycvv</i> and <i>paydate</i> blank for email/paper invoicing. + +  <li><i>paycvv</i>: CVV2 number (three digits on the back of the credit card) + +  <li><i>paydate</i>: Credit card expiration date, MM/YYYY or MM/YY (M/YY and M/YYYY are also accepted). + +  <li><i>invoicing_list</i>: Email address for invoices, or POST for postal invoices. + +  <li><i>pkgpart</i>: Package definition.  Configuration -> Provisioning, services and packages -> View/Edit package definitions + +  <li><i>username</i> and <i>_password</i> are required if <i>pkgpart</i> is specified. +</ul> + +<BR> + +<% include('/elements/footer.html') %> + +<%once> + +my $req = qq!<font color="#ff0000">*</font>!; + +</%once> +<%init> + +die "access denied" +  unless $FS::CurrentUser::CurrentUser->access_right('Import'); + +</%init>  | 
