(finally) wrap up new tax rate engine (for now)
authorjeff <jeff>
Tue, 15 Apr 2008 20:47:59 +0000 (20:47 +0000)
committerjeff <jeff>
Tue, 15 Apr 2008 20:47:59 +0000 (20:47 +0000)
13 files changed:
FS/FS/cust_main.pm
FS/FS/cust_tax_location.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg_taxproduct.pm
FS/FS/part_pkg_taxrate.pm
FS/FS/tax_class.pm
FS/FS/tax_rate.pm
httemplate/elements/file-upload.html [new file with mode: 0644]
httemplate/elements/header-minimal.html [new file with mode: 0644]
httemplate/misc/file-upload.html [new file with mode: 0644]
httemplate/misc/process/tax-import.cgi
httemplate/misc/process/tax-upgrade.cgi [new file with mode: 0644]
httemplate/misc/tax-import.cgi

index 3490e46..168c43d 100644 (file)
@@ -4828,15 +4828,15 @@ sub country_full {
   code2country($self->country);
 }
 
-=item geocode DATA_PROVIDER
+=item geocode DATA_VENDOR
 
-Returns a value for the customer location as encoded by DATA_PROVIDER.
-Currently this only makes sense for "CCH" as DATA_PROVIDER.
+Returns a value for the customer location as encoded by DATA_VENDOR.
+Currently this only makes sense for "CCH" as DATA_VENDOR.
 
 =cut
 
 sub geocode {
-  my ($self, $data_provider) = (shift, shift);  #always cch for now
+  my ($self, $data_vendor) = (shift, shift);  #always cch for now
 
   my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) )
                ? 'ship_'
@@ -4852,7 +4852,7 @@ sub geocode {
   my $cust_tax_location =
     qsearchs( {
                 'table'     => 'cust_tax_location', 
-                'hashref'   => { 'zip' => $zip, 'data_provider' => $data_provider },
+                'hashref'   => { 'zip' => $zip, 'data_vendor' => $data_vendor },
                 'extra_sql' => $extra_sql,
               }
             );
index 11faa3f..66d32a5 100644 (file)
@@ -126,14 +126,54 @@ sub check {
 
 
 sub batch_import {
-  my $param = shift;
+  my ($param, $job) = @_;
 
   my $fh = $param->{filehandle};
   my $format = $param->{'format'};
 
+  my $imported = 0;
   my @fields;
-  if ( $format eq 'cch' ) {
+  my $hook;
+
+  my $line;
+  my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
+  if ( $job ) {
+    $count++
+      while ( defined($line=<$fh>) );
+    seek $fh, 0, 0;
+  }
+
+  if ( $format eq 'cch' || $format eq 'cch-update' ) {
     @fields = qw( zip state plus4lo plus4hi geocode default );
+    push @fields, 'actionflag' if $format eq 'cch-update';
+
+    $imported++ if $format eq 'cch-update'; #empty file ok
+    
+    $hook = sub {
+      my $hash = shift;
+
+      $hash->{'data_vendor'} = 'cch';
+
+      if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
+        delete($hash->{actionflag});
+
+        my $cust_tax_location = qsearchs('cust_tax_location', $hash);
+        return "Can't find cust_tax_location to delete: ".
+               join(" ", map { "$_ => ". $hash->{$_} } @fields)
+          unless $cust_tax_location;
+
+        my $error = $cust_tax_location->delete;
+        return $error if $error;
+
+        delete($hash->{$_}) foreach (keys %$hash);
+      }
+
+      delete($hash->{'actionflag'});
+
+      '';
+      
+    };
+
   } elsif ( $format eq 'extended' ) {
     die "unimplemented\n";
     @fields = qw( );
@@ -146,8 +186,6 @@ sub batch_import {
 
   my $csv = new Text::CSV_XS;
 
-  my $imported = 0;
-
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -159,22 +197,43 @@ sub batch_import {
   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();
     };
 
+    if ( $job ) {  # progress bar
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $imported / $count )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
     my @columns = $csv->fields();
 
     my %cust_tax_location = ( 'data_vendor' => $format );;
     foreach my $field ( @fields ) {
       $cust_tax_location{$field} = shift @columns; 
     }
+    if ( scalar( @columns ) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Unexpected trailing columns in line (wrong format?): $line";
+    }
+
+    my $error = &{$hook}(\%cust_tax_location);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
+    next unless scalar(keys %cust_tax_location);
 
     my $cust_tax_location = new FS::cust_tax_location( \%cust_tax_location );
-    my $error = $cust_tax_location->insert;
+    $error = $cust_tax_location->insert;
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
index cffdc88..d4570f7 100644 (file)
@@ -749,7 +749,7 @@ specified by GEOCODE (see L<FS::part_pkg_taxrate> and ).
 
 sub part_pkg_taxrate {
   my $self = shift;
-  my ($data_provider, $geocode) = @_;
+  my ($data_vendor, $geocode) = @_;
 
   my $dbh = dbh;
   # CCH oddness in m2m
@@ -763,7 +763,7 @@ sub part_pkg_taxrate {
 
   qsearch( { 'table'     => 'part_pkg_taxrate',
              'select'    => 'distinct on(taxclassnum) *',
-             'hashref'   => { 'data_provider' => $data_provider,
+             'hashref'   => { 'data_vendor'   => $data_vendor,
                               'taxproductnum' => $self->taxproductnum,
                             },
              'extra_sql' => $extra_sql,
index 000d0d4..c66fb8c 100644 (file)
@@ -2,7 +2,7 @@ package FS::part_pkg_taxproduct;
 
 use strict;
 use vars qw( @ISA );
-use FS::Record;
+use FS::Record qw( qsearch );
 
 @ISA = qw(FS::Record);
 
@@ -79,6 +79,18 @@ Delete this record from the database.
 
 =cut
 
+sub delete {
+  my $self = shift;
+
+  return "Can't delete a tax product which has attached package tax rates!"
+    if qsearch( 'part_pkg_taxrate', { 'taxproductnum' => $self->taxproductnum } );
+
+  return "Can't delete a tax product which has attached packages!"
+    if qsearch( 'part_pkg', { 'taxproductnum' => $self->taxproductnum } );
+
+  $self->SUPER::delete(@_);
+}
+
 =item replace OLD_RECORD
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
index 3e7e7bd..5ef887d 100644 (file)
@@ -166,17 +166,30 @@ an error, returns the error, otherwise returns false.
 =cut 
 
 sub batch_import {
-  my $param = shift;
+  my ($param, $job) = @_;
 
   my $fh = $param->{filehandle};
   my $format = $param->{'format'};
 
+  my $imported = 0;
   my @fields;
   my $hook;
-  if ( $format eq 'cch' ) {
+
+  my $line;
+  my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
+  if ( $job ) {
+    $count++
+      while ( defined($line=<$fh>) );
+    seek $fh, 0, 0;
+  }
+
+  if ( $format eq 'cch' ||  $format eq 'cch-update' ) {
     @fields = qw( city county state local geocode group groupdesc item
                   itemdesc provider customer taxtypetaxed taxcattaxed
                   taxable taxtype taxcat effdate rectype );
+    push @fields, 'actionflag' if $format eq 'cch-update';
+
+    $imported++ if $format eq 'cch-update';  #empty file ok
 
     $hook = sub { 
       my $hash = shift;
@@ -186,6 +199,8 @@ sub batch_import {
         return;
       }
 
+      $hash->{'data_vendor'} = 'cch';
+
       my %providers = ( '00' => 'Regulated LEC',
                         '01' => 'Regulated IXC',
                         '02' => 'Unregulated LEC',
@@ -213,6 +228,10 @@ sub batch_import {
                                         );
 
       unless ($part_pkg_taxproduct) {
+        return "Can't find part_pkg_taxproduct for txmatrix deletion: ".
+               join(" ", map { "$_ => ". $hash->{$_} } @fields)
+          if $hash->{'actionflag'} eq 'D';
+
         $part_pkg_taxproduct{'description'} = 
           join(' : ', (map{ $hash->{$_} } qw(groupdesc itemdesc)),
                       $providers{$hash->{'provider'}},
@@ -234,15 +253,20 @@ sub batch_import {
                 );
 
       for my $item (keys %map) {
+        my $class = join(':', map($hash->{$_}, @{$map{$item}}));
         my $tax_class =
           qsearchs( 'tax_class',
                     { data_vendor => 'cch',
-                      'taxclass' => join(':', map($hash->{$_}, @{$map{$item}})),
+                      'taxclass' => $class,
                     }
                   );
         $hash->{$item} = $tax_class->taxclassnum
           if $tax_class;
 
+        return "Can't find tax class for txmatrix deletion: ".
+               join(" ", map { "$_ => ". $hash->{$_} } @fields)
+          if ($hash->{'actionflag'} eq 'D' && !$tax_class && $class ne ':');
+
         delete($hash->{$_}) foreach @{$map{$item}};
       }
 
@@ -253,6 +277,23 @@ sub batch_import {
 
       delete($hash->{'taxable'}) if ($hash->{'taxable'} eq 'N');
 
+      if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
+        delete($hash->{actionflag});
+
+        my $part_pkg_taxrate = qsearchs('part_pkg_taxrate', $hash);
+        return "Can't find part_pkg_taxrate to delete: ".
+               #join(" ", map { "$_ => ". $hash->{$_} } @fields)
+               join(" ", map { "$_ => *". $hash->{$_}. '*' } keys(%$hash) )
+          unless $part_pkg_taxrate;
+
+        my $error = $part_pkg_taxrate->delete;
+        return $error if $error;
+
+        delete($hash->{$_}) foreach (keys %$hash);
+      }
+
+      delete($hash->{actionflag});
+
       '';
     };
 
@@ -269,8 +310,6 @@ sub batch_import {
 
   my $csv = new Text::CSV_XS;
 
-  my $imported = 0;
-
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -282,19 +321,34 @@ sub batch_import {
   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();
     };
 
+
+    if ( $job ) {  # progress bar
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $imported / $count )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
     my @columns = $csv->fields();
 
     my %part_pkg_taxrate = ( 'data_vendor' => $format );
     foreach my $field ( @fields ) {
       $part_pkg_taxrate{$field} = shift @columns; 
     }
+    if ( scalar( @columns ) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Unexpected trailing columns in line (wrong format?): $line";
+    }
+
     my $error = &{$hook}(\%part_pkg_taxrate);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
index 0a939ad..ed63939 100644 (file)
@@ -79,6 +79,25 @@ Delete this record from the database.
 
 =cut
 
+sub delete {
+  my $self = shift;
+
+  return "Can't delete a tax class which has tax rates!"
+    if qsearch( 'tax_rate', { 'taxclassnum' => $self->taxclassnum } );
+
+  return "Can't delete a tax class which has package tax rates!"
+    if qsearch( 'part_pkg_taxrate', { 'taxclassnum' => $self->taxclassnum } );
+
+  return "Can't delete a tax class which has package tax rates!"
+    if qsearch( 'part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum } );
+
+  return "Can't delete a tax class which has package tax overrides!"
+    if qsearch( 'part_pkg_taxoverride', { 'taxclassnum' => $self->taxclassnum } );
+
+  $self->SUPER::delete(@_);
+  
+}
+
 =item replace OLD_RECORD
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
@@ -116,7 +135,7 @@ an error, returns the error, otherwise returns false.
 =cut 
 
 sub batch_import {
-  my $param = shift;
+  my ($param, $job) = @_;
 
   my $fh = $param->{filehandle};
   my $format = $param->{'format'};
@@ -126,31 +145,106 @@ sub batch_import {
   my $endhook;
   my $data = {};
   my $imported = 0;
+  my $dbh = dbh;
 
-  if ( $format eq 'cch' ) {
+  my $line;
+  my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
+  if ( $job ) {
+    $count++
+      while ( defined($line=<$fh>) );
+    seek $fh, 0, 0;
+  }
+
+  if ( $format eq 'cch' || $format eq 'cch-update' ) {
     @fields = qw( table name pos number length value description );
+    push @fields, 'actionflag' if $format eq 'cch-update';
 
     $hook = sub { 
       my $hash = shift;
 
       if ($hash->{'table'} eq 'DETAIL') {
         push @{$data->{'taxcat'}}, [ $hash->{'value'}, $hash->{'description'} ]
-          if $hash->{'name'} eq 'TAXCAT';
+          if ($hash->{'name'} eq 'TAXCAT' &&
+             (!exists($hash->{actionflag}) || $hash->{actionflag} eq 'I') );
 
         push @{$data->{'taxtype'}}, [ $hash->{'value'}, $hash->{'description'} ]
-          if $hash->{'name'} eq 'TAXTYPE';
+          if ($hash->{'name'} eq 'TAXTYPE' &&
+             (!exists($hash->{actionflag}) || $hash->{actionflag} eq 'I') );
+
+        if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
+          my $name = $hash->{'name'};
+          my $value = $hash->{'value'};
+          return "Bad value for $name: $value"
+            unless $value =~ /^\d+$/;
+
+          if ($name eq 'TAXCAT' || $name eq 'TAXTYPE') {
+            my @tax_class = qsearch( 'tax_class',
+                                     { 'data_vendor' => 'cch' },
+                                     '',
+                                     "AND taxclass LIKE '".
+                                       ($name eq 'TAXTYPE' ? $value : '%').":".
+                                       ($name eq 'TAXCAT' ? $value : '%')."'",
+                                   );
+            foreach (@tax_class) {
+              my $error = $_->delete;
+              return $error if $error;
+            }
+          }
+        }
+
       }
 
       delete($hash->{$_})
         for qw( data_vendor table name pos number length value description );
+      delete($hash->{actionflag}) if exists($hash->{actionflag});
 
       '';
 
     };
 
     $endhook = sub { 
-      foreach my $type (@{$data->{'taxtype'}}) {
+
+      my $sql = "SELECT DISTINCT ".
+         "substring(taxclass from 1 for position(':' in taxclass)-1),".
+         "substring(description from 1 for position(':' in description)-1) ".
+         "FROM tax_class WHERE data_vendor='cch'";
+
+      my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+      $sth->execute or die $sth->errstr;
+      my @old_types = @{$sth->fetchall_arrayref};
+
+      $sql = "SELECT DISTINCT ".
+         "substring(taxclass from position(':' in taxclass)+1),".
+         "substring(description from position(':' in description)+1) ".
+         "FROM tax_class WHERE data_vendor='cch'";
+
+      $sth = $dbh->prepare($sql) or die $dbh->errstr;
+      $sth->execute or die $sth->errstr;
+      my @old_cats = @{$sth->fetchall_arrayref};
+
+      my $catcount  = exists($data->{'taxcat'})  ? scalar(@{$data->{'taxcat'}})
+                                                 : 0;
+      my $typecount = exists($data->{'taxtype'}) ? scalar(@{$data->{'taxtype'}})
+                                                 : 0;
+
+      my $count = scalar(@old_types) * $catcount
+                + $typecount * (scalar(@old_cats) + $catcount);
+
+      $imported = 1 if $format eq 'cch-update';  #empty file ok
+
+      foreach my $type (@old_types) {
         foreach my $cat (@{$data->{'taxcat'}}) {
+
+          if ( $job ) {  # progress bar
+            if ( time - $min_sec > $last ) {
+              my $error = $job->update_statustext(
+                int( 100 * $imported / $count )
+              );
+              die $error if $error;
+              $last = time;
+            }
+          }
+
           my $tax_class =
             new FS::tax_class( { 'data_vendor' => 'cch',
                                  'taxclass'    => $type->[0].':'.$cat->[0],
@@ -161,6 +255,31 @@ sub batch_import {
           $imported++;
         }
       }
+
+      foreach my $type (@{$data->{'taxtype'}}) {
+        foreach my $cat (@old_cats, @{$data->{'taxcat'}}) {
+
+          if ( $job ) {  # progress bar
+            if ( time - $min_sec > $last ) {
+              my $error = $job->update_statustext(
+                int( 100 * $imported / $count )
+              );
+              die $error if $error;
+              $last = time;
+            }
+          }
+
+          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++;
+        }
+      }
+
       '';
     };
 
@@ -186,10 +305,19 @@ sub batch_import {
 
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
-  my $dbh = dbh;
   
-  my $line;
   while ( defined($line=<$fh>) ) {
+
+    if ( $job ) {  # progress bar
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $imported / $count )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
     $csv->parse($line) or do {
       $dbh->rollback if $oldAutoCommit;
       return "can't parse: ". $csv->error_input();
@@ -201,16 +329,21 @@ sub batch_import {
     foreach my $field ( @fields ) {
       $tax_class{$field} = shift @columns; 
     }
+    if ( scalar( @columns ) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Unexpected trailing columns in line (wrong format?): $line";
+    }
+
     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";
@@ -227,17 +360,19 @@ sub batch_import {
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
-  return "Empty file!" unless $imported;
+  return "Empty File!" unless $imported;
 
   ''; #no error
 
 }
 
-
 =back
 
 =head1 BUGS
 
+  batch_import does not handle mixed I and D records in the same file for
+  format cch-update
+
 =head1 SEE ALSO
 
 L<FS::Record>, schema.html from the base documentation.
index 3d56a0d..268edca 100644 (file)
@@ -5,9 +5,13 @@ use vars qw( @ISA $DEBUG $me
              %tax_unittypes %tax_maxtypes %tax_basetypes %tax_authorities
              %tax_passtypes );
 use Date::Parse;
+use Storable qw( thaw );
+use MIME::Base64;
 use FS::Record qw( qsearchs dbh );
 use FS::tax_class;
 use FS::cust_bill_pkg;
+use FS::cust_tax_location;
+use FS::part_pkg_taxrate;
 
 @ISA = qw( FS::Record );
 
@@ -410,21 +414,38 @@ sub taxline {
 =cut
 
 sub batch_import {
-  my $param = shift;
+  my ($param, $job) = @_;
 
   my $fh = $param->{filehandle};
   my $format = $param->{'format'};
 
+  my %insert = ();
+  my %delete = ();
+
   my @fields;
   my $hook;
-  if ( $format eq 'cch' ) {
+
+  my $line;
+  my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
+  if ( $job ) {
+    $count++
+      while ( defined($line=<$fh>) );
+    seek $fh, 0, 0;
+  }
+  $count *=2;
+
+  if ( $format eq 'cch' || $format eq 'cch-update' ) {
     @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 );
+    push @fields, 'actionflag' if $format eq 'cch-update';
+
     $hook = sub {
       my $hash = shift;
 
+      $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
+      $hash->{'data_vendor'} ='cch';
       $hash->{'effective_date'} = str2time($hash->{'effective_date'});
 
       my $taxclassid =
@@ -435,7 +456,7 @@ sub batch_import {
                       );
 
       my $tax_class = qsearchs( 'tax_class', \%tax_class );
-      return "Error inserting tax rate: no tax class $taxclassid"
+      return "Error updating tax rate: no tax class $taxclassid"
         unless $tax_class;
 
       $hash->{'taxclassnum'} = $tax_class->taxclassnum;
@@ -456,6 +477,15 @@ sub batch_import {
           if length($hash->{$_}) > 80;
       }
 
+      my $actionflag = delete($hash->{'actionflag'});
+      if ($actionflag eq 'I') {
+        $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
+      }elsif ($actionflag eq 'D') {
+        $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
+      }else{
+        return "Unexpected action flag: ". $hash->{'actionflag'};
+      }
+
       '';
 
     };
@@ -486,15 +516,21 @@ sub batch_import {
   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);
+    if ( $job ) {  # progress bar
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $imported / $count )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
 
     my @columns = $csv->fields();
 
@@ -502,14 +538,95 @@ sub batch_import {
     foreach my $field ( @fields ) {
       $tax_rate{$field} = shift @columns; 
     }
+    if ( scalar( @columns ) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Unexpected trailing columns in line (wrong format?): $line";
+    }
+
     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;
+    $imported++;
+
+  }
+
+  for (grep { !exists($delete{$_}) } keys %insert) {
+    if ( $job ) {  # progress bar
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $imported / $count )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
+    my $tax_rate = new FS::tax_rate( $insert{$_} );
+    my $error = $tax_rate->insert;
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't insert tax_rate for $line: $error";
+    }
+
+    $imported++;
+  }
+
+  for (grep { exists($delete{$_}) } keys %insert) {
+    if ( $job ) {  # progress bar
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $imported / $count )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
+    my $old = qsearchs( 'tax_rate', $delete{$_} );
+    unless ($old) {
+      $dbh->rollback if $oldAutoCommit;
+      $old = $delete{$_};
+      return "can't find tax_rate to replace for: ".
+        #join(" ", map { "$_ => ". $old->{$_} } @fields);
+        join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
+    }
+    my $new = new FS::tax_rate( $insert{$_} );
+    $new->taxnum($old->taxnum);
+    my $error = $new->replace($old);
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't insert tax_rate for $line: $error";
+    }
+
+    $imported++;
+    $imported++;
+  }
+
+  for (grep { !exists($insert{$_}) } keys %delete) {
+    if ( $job ) {  # progress bar
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $imported / $count )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
+    my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
+    unless ($tax_rate) {
+      $dbh->rollback if $oldAutoCommit;
+      $tax_rate = $delete{$_};
+      return "can't find tax_rate to delete for: ".
+        #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
+        join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
+    }
+    my $error = $tax_rate->delete;
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -527,12 +644,160 @@ sub batch_import {
 
 }
 
+=item process_batch
+
+Load an batch import as a queued JSRPC job
+
+=cut
+
+sub process_batch {
+  my $job = shift;
+
+  my $param = thaw(decode_base64(shift));
+  my $format = $param->{'format'};        #well... this is all cch specific
+
+  my $files = $param->{'uploaded_files'}
+    or die "No files provided.";
+
+  my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
+
+  if ($format eq 'cch') {
+
+    my $oldAutoCommit = $FS::UID::AutoCommit;
+    local $FS::UID::AutoCommit = 0;
+    my $dbh = dbh;
+    my $error = '';
+
+    my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
+                 'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
+                 'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
+                 'DETAIL',   'detail',    \&FS::tax_rate::batch_import,
+               );
+    while( scalar(@list) ) {
+      my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
+      unless ($files{$file}) {
+        $error = "No $name supplied";
+        next;
+      }
+      my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+      my $filename = "$dir/".  $files{$file};
+      open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
+
+      $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+      close $fh;
+      unlink $filename or warn "Can't delete $filename: $!";
+    }
+    
+    if ($error) {
+      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      die $error;
+    }else{
+      $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    }
+
+  }elsif ($format eq 'cch-update') {
+
+    my $oldAutoCommit = $FS::UID::AutoCommit;
+    local $FS::UID::AutoCommit = 0;
+    my $dbh = dbh;
+    my $error = '';
+    my @insert_list = ();
+    my @delete_list = ();
+
+    my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
+                 'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
+                 'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
+               );
+    my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+    while( scalar(@list) ) {
+      my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
+      unless ($files{$file}) {
+        $error = "No $name supplied";
+        next;
+      }
+      my $filename = "$dir/".  $files{$file};
+      open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
+      unlink $filename or warn "Can't delete $filename: $!";
+
+      my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
+                                DIR      => $dir,
+                                UNLINK   => 0,     #meh
+                              ) or die "can't open temp file: $!\n";
+
+      my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
+                                DIR      => $dir,
+                                UNLINK   => 0,     #meh
+                              ) or die "can't open temp file: $!\n";
+
+      while(<$fh>) {
+        my $handle = '';
+        $handle = $ifh if $_ =~ /"I"\s*$/;
+        $handle = $dfh if $_ =~ /"D"\s*$/;
+        unless ($handle) {
+          $error = "bad input line: $_" unless $handle;
+          last;
+        }
+        print $handle $_;
+      }
+      close $fh;
+      close $ifh;
+      close $dfh;
+
+      push @insert_list, $name, $ifh->filename, $import_sub;
+      unshift @delete_list, $name, $dfh->filename, $import_sub;
+
+    }
+    while( scalar(@insert_list) ) {
+      my ($name, $file, $import_sub) =
+        (shift @insert_list, shift @insert_list, shift @insert_list);
+
+      open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
+      $error ||=
+        &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+      close $fh;
+      unlink $file or warn "Can't delete $file: $!";
+    }
+    
+    $error = "No DETAIL supplied"
+      unless ($files{detail});
+    open my $fh, "< $dir/". $files{detail}
+      or $error ||= "Can't open DETAIL file: $!";
+    $error ||=
+      &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
+                                  $job);
+    close $fh;
+    unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
+      if $files{detail};
+
+    while( scalar(@delete_list) ) {
+      my ($name, $file, $import_sub) =
+        (shift @delete_list, shift @delete_list, shift @delete_list);
+
+      open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
+      $error ||=
+        &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+      close $fh;
+      unlink $file or warn "Can't delete $file: $!";
+    }
+    
+    if ($error) {
+      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      die $error;
+    }else{
+      $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    }
+
+  }else{
+    die "Unknown format: $format";
+  }
+
+}
+
 =back
 
 =head1 BUGS
 
-regionselector?  putting web ui components in here?  they should probably live
-somewhere else...
+  Mixing automatic and manual editing works poorly at present.
 
 =head1 SEE ALSO
 
diff --git a/httemplate/elements/file-upload.html b/httemplate/elements/file-upload.html
new file mode 100644 (file)
index 0000000..2859a67
--- /dev/null
@@ -0,0 +1,80 @@
+
+<script type="text/javascript">
+
+  function doUpload(form, callback) {
+    var name = 'form' + Math.floor(Math.random() * 99999); // perlize?
+    var d = document.createElement('DIV');
+    d.innerHTML = '<iframe style="display:none" src="about:blank" id="'+name+'" name="'+name+'" onload="uploadComplete(\''+name+'\')"></iframe>';
+    document.body.appendChild(d);
+
+    var i = document.getElementById(name);
+    if (callback && typeof(callback) == 'function') {
+      i.onComplete = callback;
+    }
+
+    form.setAttribute('target', name);
+    return true;
+  }
+
+  function uploadComplete(id) {
+    var i = document.getElementById(id);
+    if (i.contentDocument) {
+      var d = i.contentDocument;
+    } else if (i.contentWindow) {
+      var d = i.contentWindow.document;
+    } else {
+      var d = window.frames[id].document;
+    }
+    if (d.location.href == "about:blank") {
+      return;
+    }
+
+    document.getElementById('r').innerHTML = d.body.innerHTML;
+    if (typeof(i.onComplete) == 'function') {
+      var p;
+      if (p = d.body.innerHTML.indexOf("Freeside File Upload Successful ") >= 0) {
+        var v = d.body.innerHTML.substr(p+33)
+        var u = document.getElementById('uploaded_files');
+        v = v.substr(0, v.indexOf(';'));
+        u.value = v;
+        i.onComplete(true, '');
+      }else{
+        i.onComplete(false, d.body.innerHTML);
+      }
+    }
+  }
+
+</script>
+
+    <input type="hidden" name="uploaded_files" id="uploaded_files" value="" />
+    <input type="hidden" name="upload_fields" value="<% join(',', @field) %>" />
+% foreach (@field) {
+    <tr>
+      <th><% shift @label %></th>
+      <td><input type="file" name="<% $_ %>" /></td>
+    </tr>
+% }
+  <div style="display:<% $debug ? 'visible' : 'none' %>">Debugging: <pre id="r"></pre></div>
+
+<%init>
+my %param = @_;
+
+my $debug = $param{'debug'};
+
+my $callback = $param{'callback'} || "''";
+
+my @label = ();
+if ( ref($param{'label'}) ) {
+  push @label, @{$param{'label'}};
+}else{
+  push @label, $param{'label'};
+}
+
+my @field = ();
+if ( ref($param{'field'}) ) {
+  push @field, @{$param{'field'}};
+}else{
+  push @field, $param{'field'};
+}
+
+</%init>
diff --git a/httemplate/elements/header-minimal.html b/httemplate/elements/header-minimal.html
new file mode 100644 (file)
index 0000000..f74a9cc
--- /dev/null
@@ -0,0 +1,19 @@
+%
+%  my($title, $menubar) = ( shift, shift ); #$menubar is unused here though
+%  my $etc = @_ ? shift : ''; #$etc is for things like onLoad= etc.
+%  my $head = @_ ? shift : ''; #$head is for things that go in the <HEAD> section
+%  my $conf = new FS::Conf;
+%
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<HTML>
+  <HEAD>
+    <TITLE>
+      <% $title %>
+    </TITLE>
+    <META HTTP-Equiv="Cache-Control" Content="no-cache">
+    <META HTTP-Equiv="Pragma" Content="no-cache">
+    <META HTTP-Equiv="Expires" Content="0"> 
+    <% $head %>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8" <% $etc %>>
diff --git a/httemplate/misc/file-upload.html b/httemplate/misc/file-upload.html
new file mode 100644 (file)
index 0000000..9649d36
--- /dev/null
@@ -0,0 +1,47 @@
+<% include('/elements/header-minimal.html', 'File Upload') %>
+% if ($error) {
+Error: <% $error %>
+% }else{
+Freeside File Upload Successful <% join(',', @filenames) %>;
+% }
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Import'); #?
+
+my @filenames = ();
+my $error = '';     # could be extended to the access control
+
+$cgi->param('upload_fields') =~ /^([,\w]+)$/
+  or $error = "invalid upload_fields";
+my $fields = $1;
+
+my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+
+foreach my $field (split /,/, $fields) {
+  next if $error;
+
+  my $fh = $cgi->upload($field)
+    or $error = "No valid file was provided.";
+
+  my $sh = new File::Temp( TEMPLATE => 'upload.XXXXXXXX',
+                           DIR      => $dir,
+                           UNLINK   => 0,
+                         )
+    or $error ||= "can't open temporary file to store upload: $!\n";
+
+  unless ($error) {
+    while(<$fh>) {
+      print $sh $_;
+    }
+    $sh->filename =~ m!.*/([.\w]+)$!;
+    push @filenames,  "$field:$1";
+    close $sh
+  }
+
+}
+
+$error = "No files" unless scalar(@filenames);
+
+</%init>
index 77fba61..f66d6db 100644 (file)
@@ -1,58 +1,9 @@
-% if ( $error ) {
-%   warn $error;
-%   errorpage($error);
-%  } else {
-    <% include('/elements/header.html','Import successful') %> 
-    <% include('/elements/footer.html') %> 
-%  }
+<% $server->process %>
 <%init>
 
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Import');
+  unless $FS::CurrentUser::CurrentUser->access_right('Resend invoices');
 
-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;
-}
+my $server = new FS::UI::Web::JSRPC 'FS::tax_rate::process_batch', $cgi; 
 
 </%init>
diff --git a/httemplate/misc/process/tax-upgrade.cgi b/httemplate/misc/process/tax-upgrade.cgi
new file mode 100644 (file)
index 0000000..8782282
--- /dev/null
@@ -0,0 +1,147 @@
+% 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 = '';
+
+my ($cifh, $cdfh, $zifh, $zdfh, $tifh, $tdfh);
+
+if (defined($cfh)) {
+  $cifh = new File::Temp( TEMPLATE => 'code.insert.XXXXXXXX',
+                          DIR      => $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc,
+                        ) or die "can't open temp file: $!\n";
+
+  $cdfh = new File::Temp( TEMPLATE => 'code.insert.XXXXXXXX',
+                          DIR      => $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc,
+                        ) or die "can't open temp file: $!\n";
+
+  while(<$cfh>) {
+    my $fh = '';
+    $fh = $cifh if $_ =~ /"I"\s*$/;
+    $fh = $cdfh if $_ =~ /"D"\s*$/;
+    die "bad input line: $_" unless $fh;
+    print $fh $_;
+  }
+  seek $cifh, 0, 0;
+  seek $cdfh, 0, 0;
+
+}else{
+  $error = 'No code file';
+}
+
+$error ||= FS::tax_class::batch_import( {
+             filehandle => $cifh,
+             'format'   => scalar($cgi->param('format')),
+           } );
+
+close $cifh if $cifh;
+
+if (defined($zfh)) {
+  $zifh = new File::Temp( TEMPLATE => 'plus4.insert.XXXXXXXX',
+                          DIR      => $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc,
+                        ) or die "can't open temp file: $!\n";
+
+  $zdfh = new File::Temp( TEMPLATE => 'plus4.insert.XXXXXXXX',
+                          DIR      => $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc,
+                        ) or die "can't open temp file: $!\n";
+
+  while(<$zfh>) {
+    my $fh = '';
+    $fh = $zifh if $_ =~ /"I"\s*$/;
+    $fh = $zdfh if $_ =~ /"D"\s*$/;
+    die "bad input line: $_" unless $fh;
+    print $fh $_;
+  }
+  seek $zifh, 0, 0;
+  seek $zdfh, 0, 0;
+
+}else{
+  $error = 'No plus4 file';
+}
+
+$error ||= FS::cust_tax_location::batch_import( {
+             filehandle => $zifh,
+             'format'   => scalar($cgi->param('format')),
+           } );
+close $zifh if $zifh;
+
+if (defined($tfh)) {
+  $tifh = new File::Temp( TEMPLATE => 'txmatrix.insert.XXXXXXXX',
+                          DIR      => $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc,
+                        ) or die "can't open temp file: $!\n";
+
+  $tdfh = new File::Temp( TEMPLATE => 'txmatrix.insert.XXXXXXXX',
+                          DIR      => $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc,
+                        ) or die "can't open temp file: $!\n";
+
+  while(<$tfh>) {
+    my $fh = '';
+    $fh = $tifh if $_ =~ /"I"\s*$/;
+    $fh = $tdfh if $_ =~ /"D"\s*$/;
+    die "bad input line: $_" unless $fh;
+    print $fh $_;
+  }
+  seek $tifh, 0, 0;
+  seek $tdfh, 0, 0;
+
+}else{
+  $error = 'No tax matrix file';
+}
+
+$error ||= FS::part_pkg_taxrate::batch_import( {
+             filehandle => $tifh,
+             'format'   => scalar($cgi->param('format')),
+           } );
+close $tifh if $tifh;
+
+$error ||= defined($dfh)
+  ? FS::tax_rate::batch_update( {
+      filehandle => $dfh,
+      'format'   => scalar($cgi->param('format')),
+    } )
+  : 'No tax detail file';
+
+$error ||= FS::part_pkg_taxrate::batch_import( {
+             filehandle => $tdfh,
+             'format'   => scalar($cgi->param('format')),
+           } );
+close $tdfh if $tdfh;
+
+$error ||= FS::cust_tax_location::batch_import( {
+             filehandle => $zdfh,
+             'format'   => scalar($cgi->param('format')),
+           } );
+close $zdfh if $zdfh;
+
+$error ||= FS::tax_class::batch_import( {
+             filehandle => $cdfh,
+             'format'   => scalar($cgi->param('format')),
+           } );
+close $cdfh if $cdfh;
+
+if ($error) {
+  $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+}else{
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+}
+
+</%init>
index 6bdea6a..9044ac9 100644 (file)
@@ -3,41 +3,63 @@
 Import a CSV file set containing tax rate records.
 <BR><BR>
 
-<FORM ACTION="process/tax-import.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+<% include( '/elements/progress-init.html',
+            'TaxRateUpload',
+            [ 'format', 'uploaded_files' ],
+            'process/tax-import.cgi', 
+            { 'message' => 'Tax rates imported' },
+          )
+%>
 
-<% &ntable("#cccccc", 2) %>
+<SCRIPT>
+
+  function gotLoaded(success, message) {
+
+    var uploaded = document.getElementById('uploaded_files');
+    var a = uploaded.value.split(',');
+    if (uploaded.value.split(',').length == 4){
+      process(); 
+    }else{
+      var p = document.getElementById('uploadError');
+      p.innerHTML='<FONT SIZE="+1" COLOR="#ff0000">Error: '+message+'</FONT><BR><BR>';
+      p.style='display:visible';
+      return false;
+    }
+    
+  }
+
+</SCRIPT>
+
+<div style="display:none:" id="uploadError"></div>
+<FORM NAME="TaxRateUpload" ACTION="<% $fsurl %>misc/file-upload.html" METHOD="post" ENCTYPE="multipart/form-data" onsubmit="return doUpload(this, gotLoaded )">
 
+<% &ntable("#cccccc", 2) %>
 <TR>
   <TH ALIGN="right">Format</TH>
   <TD>
     <SELECT NAME="format">
-      <OPTION VALUE="cch" SELECTED>CCH
+      <OPTION VALUE="cch-update" SELECTED>CCH update
+      <OPTION VALUE="cch">CCH initial import
     </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>
-
+<% include('/elements/file-upload.html', 'field'    => [ 'codefile',
+                                                         'plus4file',
+                                                         'txmatrix',
+                                                         'detail',
+                                                       ],
+                                         'label'    => [ 'code CSV filename',
+                                                         'plus4 CSV filename',
+                                                         'txmatrix CSV filename',
+                                                         'detail CSV filename',
+                                                       ],
+                                         'callback' => 'gotLoaded',
+                                         'debug'    => 0,
+   )
+%>
 
-<TR><TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px"><INPUT TYPE="submit" VALUE="Import CSV files"></TD></TR>
+<TR><TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px"><INPUT TYPE="submit" VALUE="Import CSV files" onClick="document.TaxRateUpload.submit.disabled=true;"></TD></TR>
 
 </TABLE>