bug squashing for multiple usage classes
[freeside.git] / FS / FS / tax_rate.pm
index 268edca..bfb9c8c 100644 (file)
@@ -7,11 +7,12 @@ use vars qw( @ISA $DEBUG $me
 use Date::Parse;
 use Storable qw( thaw );
 use MIME::Base64;
-use FS::Record qw( qsearchs dbh );
+use FS::Record qw( qsearch qsearchs dbh );
 use FS::tax_class;
 use FS::cust_bill_pkg;
 use FS::cust_tax_location;
 use FS::part_pkg_taxrate;
+use FS::cust_main;
 
 @ISA = qw( FS::Record );
 
@@ -198,7 +199,7 @@ sub check {
     || $self->ut_textn('data_vendor')
     || $self->ut_textn('location')
     || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
-    || $self->ut_numbern('effective_date')
+    || $self->ut_snumbern('effective_date')
     || $self->ut_float('tax')
     || $self->ut_floatn('excessrate')
     || $self->ut_money('taxbase')
@@ -218,6 +219,7 @@ sub check {
     || $self->ut_enum('setuptax', [ '', 'Y' ] )
     || $self->ut_enum('recurtax', [ '', 'Y' ] )
     || $self->ut_enum('manual', [ '', 'Y' ] )
+    || $self->ut_enum('disabled', [ '', 'Y' ] )
     || $self->SUPER::check
     ;
 
@@ -338,16 +340,30 @@ sub passtype_name {
   $tax_passtypes{$self->passtype};
 }
 
-=item taxline CUST_BILL_PKG, ...
+=item taxline CUST_BILL_PKG|AMOUNT, ...
 
 Returns a listref of a name and an amount of tax calculated for the list
-of packages.  If an error occurs, a message is returned as a scalar.
+of packages/amounts.  If an error occurs, a message is returned as a scalar.
 
 =cut
 
 sub taxline {
   my $self = shift;
-  my @cust_bill_pkg = @_;
+
+  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;
+
+  my $taxable_charged = 0;
+  my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; } @_;
+
+  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";
@@ -371,12 +387,6 @@ sub taxline {
       '" basis';
   }
 
-  my $name = $self->taxname;
-  $name = 'Other surcharges'
-    if ($self->passtype == 2);
-  my $amount = 0;
-  
-  my $taxable_charged = 0;
   unless ($self->setuptax =~ /^Y$/i) {
     $taxable_charged += $_->setup foreach @cust_bill_pkg;
   }
@@ -386,7 +396,21 @@ sub taxline {
 
   my $taxable_units = 0;
   unless ($self->recurtax =~ /^Y$/i) {
-    $taxable_units += $_->units foreach @cust_bill_pkg;
+    if ($self->unittype == 0) {
+      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!;
+    }elsif ($self->unittype == 2) {
+      $taxable_units = 1;
+    }else {
+      return qq!fatal: can't (yet) handle unknown unit type in tax!.
+        $self->taxnum;
+    }
   }
 
   #
@@ -399,10 +423,64 @@ sub taxline {
   $amount += $taxable_charged * $self->tax;
   $amount += $taxable_units * $self->fee;
   
+  warn "calculated taxes as [ $name, $amount ]\n"
+    if $DEBUG;
+
   return [$name, $amount];
 
 }
 
+=item tax_on_tax CUST_MAIN
+
+Returns a list of taxes which are candidates for taxing taxes for the
+given customer (see L<FS::cust_main>)
+
+=cut
+
+sub tax_on_tax {
+  my $self = shift;
+  my $cust_main = shift;
+
+  warn "looking up taxes on tax ". $self->taxnum. " for customer ".
+    $cust_main->custnum
+    if $DEBUG;
+
+  my $geocode = $cust_main->geocode($self->data_vendor);
+
+  # CCH oddness in m2m
+  my $dbh = dbh;
+  my $extra_sql = ' AND ('.
+    join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
+                 qw(10 5 2)
+        ).
+    ')';
+
+  my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
+  my $select   = 'DISTINCT ON(taxclassnum) *';
+
+  # should qsearch preface columns with the table to facilitate joins?
+  my @taxclassnums = map { $_->taxclassnum }
+    qsearch( { 'table'     => 'part_pkg_taxrate',
+               'select'    => $select,
+               'hashref'   => { 'data_vendor'      => $self->data_vendor,
+                                'taxclassnumtaxed' => $self->taxclassnum,
+                              },
+               'extra_sql' => $extra_sql,
+               'order_by'  => $order_by,
+           } );
+
+  return () unless @taxclassnums;
+
+  $extra_sql =
+    "AND (".  join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
+
+  qsearch({ 'table'     => 'tax_rate',
+            'hashref'   => { 'geocode' => $geocode, },
+            'extra_sql' => $extra_sql,
+         })
+
+}
+
 =back
 
 =head1 SUBROUTINES
@@ -569,6 +647,8 @@ sub batch_import {
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
+      my $hashref = $insert{$_};
+      $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
       return "can't insert tax_rate for $line: $error";
     }
 
@@ -594,13 +674,15 @@ sub batch_import {
         #join(" ", map { "$_ => ". $old->{$_} } @fields);
         join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
     }
-    my $new = new FS::tax_rate( $insert{$_} );
+    my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => ''  });
     $new->taxnum($old->taxnum);
     my $error = $new->replace($old);
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "can't insert tax_rate for $line: $error";
+      my $hashref = $insert{$_};
+      $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
+      return "can't replace tax_rate for $line: $error";
     }
 
     $imported++;
@@ -630,7 +712,9 @@ sub batch_import {
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "can't insert tax_rate for $line: $error";
+      my $hashref = $delete{$_};
+      $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
+      return "can't delete tax_rate for $line: $error";
     }
 
     $imported++;
@@ -638,7 +722,7 @@ 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
 
@@ -646,7 +730,7 @@ sub batch_import {
 
 =item process_batch
 
-Load an batch import as a queued JSRPC job
+Load a batch import as a queued JSRPC job
 
 =cut
 
@@ -679,7 +763,7 @@ sub process_batch {
         $error = "No $name supplied";
         next;
       }
-      my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+      my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
       my $filename = "$dir/".  $files{$file};
       open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
 
@@ -708,7 +792,7 @@ sub process_batch {
                  '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;
+    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}) {
@@ -758,7 +842,7 @@ sub process_batch {
       unlink $file or warn "Can't delete $file: $!";
     }
     
-    $error = "No DETAIL supplied"
+    $error ||= "No DETAIL supplied"
       unless ($files{detail});
     open my $fh, "< $dir/". $files{detail}
       or $error ||= "Can't open DETAIL file: $!";
@@ -793,6 +877,86 @@ sub process_batch {
 
 }
 
+=item browse_queries PARAMS
+
+Returns a list consisting of a hashref suited for use as the argument
+to qsearch, and sql query string.  Each is based on the PARAMS hashref
+of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
+from a form.  This conveniently creates the query hashref and count_query
+string required by the browse and search elements.  As a side effect, 
+the PARAMS hashref is untainted and keys with unexpected values are removed.
+
+=cut
+
+sub browse_queries {
+  my $params = shift;
+
+  my $query = {
+                'table'     => 'tax_rate',
+                'hashref'   => {},
+                'order_by'  => 'ORDER BY geocode, taxclassnum',
+              },
+
+  my $extra_sql = '';
+
+  if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
+    $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
+  } else {
+    delete $params->{data_vendor};
+  }
+   
+  if ( $params->{geocode} =~ /^(\w+)$/ ) {
+    $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
+                    'geocode LIKE '. dbh->quote($1.'%');
+  } else {
+    delete $params->{geocode};
+  }
+
+  if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
+       qsearchs( 'tax_class', {'taxclassnum' => $1} )
+     )
+  {
+    $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
+                  ' taxclassnum  = '. dbh->quote($1)
+  } else {
+    delete $params->{taxclassnun};
+  }
+
+  my $tax_type = $1
+    if ( $params->{tax_type} =~ /^(\d+)$/ );
+  delete $params->{tax_type}
+    unless $tax_type;
+
+  my $tax_cat = $1
+    if ( $params->{tax_cat} =~ /^(\d+)$/ );
+  delete $params->{tax_cat}
+    unless $tax_cat;
+
+  my @taxclassnum = ();
+  if ($tax_type || $tax_cat ) {
+    my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
+    $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
+    @taxclassnum = map { $_->taxclassnum } 
+                   qsearch({ 'table'     => 'tax_class',
+                             'hashref'   => {},
+                             'extra_sql' => "WHERE taxclass $compare",
+                          });
+  }
+
+  $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
+                join(' OR ', map { " taxclassnum  = $_ " } @taxclassnum ). ' )'
+    if ( @taxclassnum );
+
+  unless ($params->{'showdisabled'}) {
+    $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
+                  "( disabled = '' OR disabled IS NULL )";
+  }
+
+  $query->{extra_sql} = $extra_sql;
+
+  return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
+}
+
 =back
 
 =head1 BUGS