compliance solutions, RT#77676
[freeside.git] / FS / FS / tax_rate_location.pm
index 65bef7b..ad3618a 100644 (file)
@@ -3,6 +3,7 @@ package FS::tax_rate_location;
 use strict;
 use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs dbh );
+use FS::Misc qw( csv_from_fixed );
 
 =head1 NAME
 
@@ -25,39 +26,32 @@ FS::tax_rate_location - Object methods for tax_rate_location records
 
 =head1 DESCRIPTION
 
-An FS::tax_rate_location object represents an example.  FS::tax_rate_location inherits from
-FS::Record.  The following fields are currently supported:
+An FS::tax_rate_location object represents a tax jurisdiction.  The only
+functional field is "geocode", a foreign key to tax rates (L<FS::tax_rate>) 
+that apply in the jurisdiction.  The city, county, state, and country fields 
+are provided for description and reporting.
 
-=over 4
-
-=item taxratelocationnum
-
-Primary key (assigned automatically for new tax_rate_locations)
-
-=item data_vendor
-
-The tax data vendor
-
-=item geocode
-
-A unique geographic location code provided by the data vendor
+FS::tax_rate_location inherits from FS::Record.  The following fields are 
+currently supported:
 
-=item city
+=over 4
 
-City
+=item taxratelocationnum - Primary key (assigned automatically for new 
+tax_rate_locations)
 
-=item county
+=item data_vendor - The tax data vendor ('cch' or 'billsoft').
 
-County
+=item geocode - A unique geographic location code provided by the data vendor
 
-=item state
+=item city - City
 
-State
+=item county -  County
 
-=item disabled
+=item state - State (2-letter code)
 
-If 'Y' this record is no longer active.
+=item country - Country (2-letter code, optional)
 
+=item disabled - If 'Y' this record is no longer active.
 
 =back
 
@@ -124,11 +118,14 @@ sub check {
   ;
   return $error if $error;
 
-  my $t = qsearchs( 'tax_rate_location',
-                    { map { $_ => $self->$_ } qw( data_vendor geocode ) },
-                  );
+  my $t = '';
+  $t = $self->existing_search
+    unless $self->disabled;
+
+  $t = $self->by_key( $self->taxratelocationnum )
+    if !$t && $self->taxratelocationnum;
 
-  return "geocode already in use for this vendor"
+  return "geocode ". $self->geocode. " already in use for this vendor"
     if ( $t && $t->taxratelocationnum != $self->taxratelocationnum );
 
   return "may only be disabled"
@@ -141,16 +138,98 @@ sub check {
   $self->SUPER::check;
 }
 
+=item find_or_insert
+
+Finds an existing, non-disabled tax jurisdiction matching the data_vendor 
+and geocode fields. If there is one, updates its city, county, state, and
+country to match this record.  If there is no existing record, inserts this 
+record.
+
+=cut
+
+sub find_or_insert {
+  my $self = shift;
+  my $existing = $self->existing_search;
+  if ($existing) {
+    my $update = 0;
+    foreach (qw(city county state country)) {
+      if ($self->get($_) ne $existing->get($_)) {
+        $update++;
+      }
+    }
+    $self->set(taxratelocationnum => $existing->taxratelocationnum);
+    if ($update) {
+      return $self->replace($existing);
+    } else {
+      return;
+    }
+  } else {
+    return $self->insert;
+  }
+}
+
+sub existing_search {
+  my $self = shift;
+
+  my @unique = qw( data_vendor geocode );
+  push @unique, qw( state country )
+    if $self->data_vendor eq 'compliance_solutions';
+
+  qsearchs( 'tax_rate_location',
+            { disabled => '',
+              map { $_ => $self->$_ } @unique
+            }
+          );
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=item location_sql KEY => VALUE, ...
+
+Returns an SQL fragment identifying matching tax_rate_location /
+cust_bill_pkg_tax_rate_location records.
+
+Parameters are county, state, city and locationtaxid
+
+=cut
+
+sub location_sql {
+  my($class, %param) = @_;
+
+  my %pn = (
+   'city'          => 'tax_rate_location.city',
+   'county'        => 'tax_rate_location.county',
+   'state'         => 'tax_rate_location.state',
+   'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid',
+  );
+
+  my %ph = map { $pn{$_} => dbh->quote($param{$_}) } keys %pn;
+
+  join( ' AND ',
+    map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } keys %ph
+  );
+
+}
+
 =back
 
 =head1 SUBROUTINES
 
 =over 4
 
-=item batch_import
+=item batch_import HASHREF, JOB
+
+Starts importing tax_rate_location records from a file.  HASHREF must contain
+'filehandle' (an open handle to the input file) and 'format' (one of 'cch',
+'cch-fixed', 'cch-update', 'cch-fixed-update', or 'billsoft').  JOB is an
+L<FS::queue> object to receive progress messages.
 
 =cut
 
+# XXX move this into TaxEngine modules at some point
+
 sub batch_import {
   my ($param, $job) = @_;
 
@@ -175,7 +254,7 @@ sub batch_import {
 
   my $line;
   my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
-  if ( $job || scalar(@column_callbacks) ) {
+  if ( $job || scalar(@column_callbacks) ) { # this makes zero sense
     my $error =
       csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
     return $error if $error;
@@ -193,7 +272,7 @@ sub batch_import {
       if (exists($hash->{'actionflag'}) && $hash->{'actionflag'} eq 'D') {
         delete($hash->{actionflag});
 
-        $hash->{deleted} = '';
+        $hash->{disabled} = '';
         my $tax_rate_location = qsearchs('tax_rate_location', $hash);
         return "Can't find tax_rate_location to delete: ".
                join(" ", map { "$_ => ". $hash->{$_} } @fields)
@@ -212,6 +291,29 @@ sub batch_import {
 
     };
 
+  } elsif ( $format eq 'billsoft' ) {
+    @fields = ( qw( geocode alt_location country state county city ), '', '' );
+
+    $hook = sub {
+      my $hash = shift;
+      if ($hash->{alt_location}) {
+        # don't import these; the jurisdiction should be named using its 
+        # primary city
+        %$hash = ();
+        return;
+      }
+
+      $hash->{data_vendor} = 'billsoft';
+      # unlike cust_tax_location, keep the whole-country and whole-state 
+      # rows, but strip the whitespace
+      $hash->{county} =~ s/^ //g;
+      $hash->{state} =~ s/^ //g;
+      $hash->{country} =~ s/^ //g;
+      $hash->{city} =~ s/[^\w ]//g; # remove asterisks and other bad things
+      $hash->{country} = substr($hash->{country}, 0, 2);
+      '';
+    }
+
   } elsif ( $format eq 'extended' ) {
     die "unimplemented\n";
     @fields = qw( );
@@ -247,7 +349,8 @@ sub batch_import {
     if ( $job ) {  # progress bar
       if ( time - $min_sec > $last ) {
         my $error = $job->update_statustext(
-          int( 100 * $imported / $count )
+          int( 100 * $imported / $count ) .
+          ',Creating tax jurisdiction records'
         );
         die $error if $error;
         $last = time;
@@ -262,7 +365,7 @@ sub batch_import {
     }
     if ( scalar( @columns ) ) {
       $dbh->rollback if $oldAutoCommit;
-      return "Unexpected trailing columns in line (wrong format?): $line";
+      return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line";
     }
 
     my $error = &{$hook}(\%tax_rate_location);
@@ -278,7 +381,7 @@ sub batch_import {
 
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
-        return "can't insert tax_rate for $line: $error";
+        return "can't insert tax_rate_location for $line: $error";
       }
 
     }
@@ -295,6 +398,16 @@ sub batch_import {
 
 }
 
+sub _upgrade_data {
+  my $class = shift;
+
+  my $sql = "UPDATE tax_rate_location SET data_vendor = 'compliance_solutions' WHERE data_vendor = 'compliance solutions'";
+
+  my $sth = dbh->prepare($sql) or die $DBI::errstr;
+  $sth->execute() or die $sth->errstr;
+  
+}
+
 =head1 BUGS
 
 Currently somewhat specific to CCH supplied data.