remove useless line
[freeside.git] / FS / FS / tax_rate.pm
index 4769b32..0d9156b 100644 (file)
@@ -13,6 +13,7 @@ use FS::cust_bill_pkg;
 use FS::cust_tax_location;
 use FS::part_pkg_taxrate;
 use FS::cust_main;
+use FS::Misc qw( csv_from_fixed );
 
 @ISA = qw( FS::Record );
 
@@ -293,6 +294,7 @@ Returns the human understandable value associated with the basetype column
                    '11' => 'gross profits',
                    '12' => 'tariff rate',
                    '14' => 'account',
+                   '15' => 'prior year gross receipts',
 );
 
 sub basetype_name {
@@ -340,51 +342,78 @@ sub passtype_name {
   $tax_passtypes{$self->passtype};
 }
 
-=item taxline CUST_BILL_PKG|AMOUNT, ...
+=item taxline TAXABLES, [ OPTIONSHASH ]
 
 Returns a listref of a name and an amount of tax calculated for the list
-of packages/amounts.  If an error occurs, a message is returned as a scalar.
+of packages/amounts referenced by TAXABLES.  If an error occurs, a message
+is returned as a scalar.
 
 =cut
 
 sub taxline {
   my $self = shift;
 
+  my $taxables;
+  my %opt = ();
+
+  if (ref($_[0]) eq 'ARRAY') {
+    $taxables = shift;
+    %opt = @_;
+  }else{
+    $taxables = [ @_ ];
+    #exemptions would be broken in this case
+  }
+
   my $name = $self->taxname;
   $name = 'Other surcharges'
     if ($self->passtype == 2);
   my $amount = 0;
   
-  return [$name, $amount]  # we always know how to handle disabled taxes
-    if $self->disabled;
+  if ( $self->disabled ) { # we always know how to handle disabled taxes
+    return {
+      'name'   => $name,
+      'amount' => $amount,
+    };
+  }
 
   my $taxable_charged = 0;
-  my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; } @_;
+  my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; }
+                      @$taxables;
 
   warn "calculating taxes for ". $self->taxnum. " on ".
     join (",", map { $_->pkgnum } @cust_bill_pkg)
     if $DEBUG;
 
   if ($self->passflag eq 'N') {
-    return "fatal: can't (yet) handle taxes not passed to the customer";
+    # return "fatal: can't (yet) handle taxes not passed to the customer";
+    # until someone needs to track these in freeside
+    return {
+      'name'   => $name,
+      'amount' => 0,
+    };
   }
 
   if ($self->maxtype != 0 && $self->maxtype != 9) {
-    return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name. 
-      '" threshold';
+    return $self->_fatal_or_null( 'tax with "'.
+                                    $self->maxtype_name. '" threshold'
+                                );
   }
 
   if ($self->maxtype == 9) {
-    return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name. 
-      '" threshold';  # "texas" tax
+    return
+      $self->_fatal_or_null( 'tax with "'. $self->maxtype_name. '" threshold' );
+                                                                # "texas" tax
   }
 
+  # we treat gross revenue as gross receipts and expect the tax data
+  # to DTRT (i.e. tax on tax rules)
   if ($self->basetype != 0 && $self->basetype != 1 &&
-      $self->basetype != 6 && $self->basetype != 7 &&
+      $self->basetype != 5 && $self->basetype != 6 &&
+      $self->basetype != 7 && $self->basetype != 8 &&
       $self->basetype != 14
   ) {
-    return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name. 
-      '" basis';
+    return
+      $self->_fatal_or_null( 'tax with "'. $self->basetype_name. '" basis' );
   }
 
   unless ($self->setuptax =~ /^Y$/i) {
@@ -397,14 +426,18 @@ sub taxline {
   my $taxable_units = 0;
   unless ($self->recurtax =~ /^Y$/i) {
     if ($self->unittype == 0) {
-      $taxable_units += $_->units foreach @cust_bill_pkg;
+      my %seen = ();
+      foreach (@cust_bill_pkg) {
+        $taxable_units += $_->units
+          unless $seen{$_->pkgnum};
+        $seen{$_->pkgnum}++;
+      }
     }elsif ($self->unittype == 1) {
-      return qq!fatal: can't (yet) handle fee with minute unit type!;
+      return $self->_fatal_or_null( 'fee with minute unit type' );
     }elsif ($self->unittype == 2) {
       $taxable_units = 1;
     }else {
-      return qq!fatal: can't (yet) handle unknown unit type in tax!.
-        $self->taxnum;
+      return $self->_fatal_or_null( 'unknown unit type in tax'. $self->taxnum );
     }
   }
 
@@ -421,10 +454,31 @@ sub taxline {
   warn "calculated taxes as [ $name, $amount ]\n"
     if $DEBUG;
 
-  return [$name, $amount];
+  return {
+    'name'   => $name,
+    'amount' => $amount,
+  };
 
 }
 
+sub _fatal_or_null {
+  my ($self, $error) = @_;
+
+  my $conf = new FS::Conf;
+
+  $error = "fatal: can't yet handle ". $error;
+  my $name = $self->taxname;
+  $name = 'Other surcharges'
+    if ($self->passtype == 2);
+
+  if ($conf->exists('ignore_incalculable_taxes')) {
+    warn $error;
+    return { name => $name, amount => 0 };
+  } else {
+    return $error;
+  }
+}
+
 =item tax_on_tax CUST_MAIN
 
 Returns a list of taxes which are candidates for taxing taxes for the
@@ -498,12 +552,27 @@ sub batch_import {
   my @fields;
   my $hook;
 
+  my @column_lengths = ();
+  my @column_callbacks = ();
+  if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
+    $format =~ s/-fixed//;
+    my $date_format = sub { my $r='';
+                            /^(\d{4})(\d{2})(\d{2})$/ && ($r="$1/$2/$3");
+                            $r;
+                          };
+    my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
+    push @column_lengths, qw( 10 1 1 8 8 5 8 8 8 1 2 2 30 8 8 10 2 8 2 1 2 2 );
+    push @column_lengths, 1 if $format eq 'cch-update';
+    push @column_callbacks, $trim foreach (@column_lengths); # 5, 6, 15, 17 esp
+    $column_callbacks[8] = $date_format;
+  }
+  
   my $line;
   my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
-  if ( $job ) {
-    $count++
-      while ( defined($line=<$fh>) );
-    seek $fh, 0, 0;
+  if ( $job || scalar(@column_callbacks) ) {
+    my $error =
+      csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
+    return $error if $error;
   }
   $count *=2;
 
@@ -551,14 +620,22 @@ sub batch_import {
       }
 
       my $actionflag = delete($hash->{'actionflag'});
+
+      $hash->{'taxname'} =~ s/`/'/g; 
+      $hash->{'taxname'} =~ s|\\|/|g;
+
+      return '' if $format eq 'cch';  # but not cch-update
+
       if ($actionflag eq 'I') {
-        $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
+        $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
       }elsif ($actionflag eq 'D') {
-        $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
+        $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
       }else{
         return "Unexpected action flag: ". $hash->{'actionflag'};
       }
 
+      delete($hash->{$_}) for keys %$hash;
+
       '';
 
     };
@@ -622,6 +699,18 @@ sub batch_import {
       return $error;
     }
 
+    if (scalar(keys %tax_rate)) { #inserts only, not updates for cch
+
+      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++;
 
   }
@@ -717,19 +806,19 @@ sub batch_import {
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
-  return "Empty file!" unless $imported;
+  return "Empty file!" unless ($imported || $format eq 'cch-update');
 
   ''; #no error
 
 }
 
-=item process_batch
+=item process_batch_import
 
 Load a batch import as a queued JSRPC job
 
 =cut
 
-sub process_batch {
+sub process_batch_import {
   my $job = shift;
 
   my $param = thaw(decode_base64(shift));
@@ -740,29 +829,36 @@ sub process_batch {
 
   my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
 
-  if ($format eq 'cch') {
+  if ($format eq 'cch' || $format eq 'cch-fixed') {
 
     my $oldAutoCommit = $FS::UID::AutoCommit;
     local $FS::UID::AutoCommit = 0;
     my $dbh = dbh;
     my $error = '';
+    my $have_location = 0;
 
     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
+                 'ZIP',      'zipfile',   \&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}) {
+        next if $name eq 'PLUS4';
         $error = "No $name supplied";
+        $error = "Neither PLUS4 nor ZIP supplied"
+          if ($name eq 'ZIP' && !$have_location);
         next;
       }
+      $have_location = 1 if $name eq 'PLUS4';
+      my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
       my $dir = '%%%FREESIDE_CACHE%%%/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);
+      $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
       close $fh;
       unlink $filename or warn "Can't delete $filename: $!";
     }
@@ -774,7 +870,7 @@ sub process_batch {
       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
     }
 
-  }elsif ($format eq 'cch-update') {
+  }elsif ($format eq 'cch-update' || $format eq 'cch-fixed-update') {
 
     my $oldAutoCommit = $FS::UID::AutoCommit;
     local $FS::UID::AutoCommit = 0;
@@ -785,12 +881,23 @@ sub process_batch {
 
     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
+                 'ZIP',      'zipfile',   \&FS::cust_tax_location::batch_import,
                  'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
                );
     my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
     while( scalar(@list) ) {
       my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
       unless ($files{$file}) {
+        my $vendor = $name eq 'ZIP' ? 'cch' : 'cch-zip';
+        next     # update expected only for previously installed location data
+          if (   ($name eq 'PLUS4' || $name eq 'ZIP')
+               && !scalar( qsearch( { table => 'cust_tax_location',
+                                      hashref => { data_vendor => $vendor },
+                                      select => 'DISTINCT data_vendor',
+                                  } )
+                         )
+             );
+
         $error = "No $name supplied";
         next;
       }
@@ -808,10 +915,12 @@ sub process_batch {
                                 UNLINK   => 0,     #meh
                               ) or die "can't open temp file: $!\n";
 
+      my $insert_pattern = ($format eq 'cch-update') ? qr/"I"\s*$/ : qr/I\s*$/;
+      my $delete_pattern = ($format eq 'cch-update') ? qr/"D"\s*$/ : qr/D\s*$/;
       while(<$fh>) {
         my $handle = '';
-        $handle = $ifh if $_ =~ /"I"\s*$/;
-        $handle = $dfh if $_ =~ /"D"\s*$/;
+        $handle = $ifh if $_ =~ /$insert_pattern/;
+        $handle = $dfh if $_ =~ /$delete_pattern/;
         unless ($handle) {
           $error = "bad input line: $_" unless $handle;
           last;
@@ -830,9 +939,10 @@ sub process_batch {
       my ($name, $file, $import_sub) =
         (shift @insert_list, shift @insert_list, shift @insert_list);
 
+      my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
       $error ||=
-        &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+        &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
       close $fh;
       unlink $file or warn "Can't delete $file: $!";
     }
@@ -852,9 +962,10 @@ sub process_batch {
       my ($name, $file, $import_sub) =
         (shift @delete_list, shift @delete_list, shift @delete_list);
 
+      my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
       $error ||=
-        &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+        &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
       close $fh;
       unlink $file or warn "Can't delete $file: $!";
     }