X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Ftax_rate.pm;h=0d9156b4316d62b7127c296c878872aec4e76afd;hb=399e73ed1e73443523d6889b703d04289ec0e117;hp=81b63abd71d90a384e822b8386376f508fcafc31;hpb=3b35ccbf226efe00c94f3a72dd1c7ed64d926a7c;p=freeside.git diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 81b63abd7..0d9156b43 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -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 ); @@ -199,7 +200,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') @@ -219,6 +220,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 ; @@ -292,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 { @@ -339,50 +342,80 @@ 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; + + 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' ); } - my $name = $self->taxname; - $name = 'Other surcharges' - if ($self->passtype == 2); - my $amount = 0; - unless ($self->setuptax =~ /^Y$/i) { $taxable_charged += $_->setup foreach @cust_bill_pkg; } @@ -393,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 ); } } @@ -417,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 @@ -494,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; @@ -547,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; + ''; }; @@ -618,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++; } @@ -638,6 +731,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"; } @@ -663,13 +758,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++; @@ -699,7 +796,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++; @@ -707,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 an batch import as a queued JSRPC job +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)); @@ -730,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; } - my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; + $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: $!"; } @@ -764,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; @@ -775,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 = $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}) { + 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; } @@ -798,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; @@ -820,14 +939,15 @@ 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: $!"; } - $error = "No DETAIL supplied" + $error ||= "No DETAIL supplied" unless ($files{detail}); open my $fh, "< $dir/". $files{detail} or $error ||= "Can't open DETAIL file: $!"; @@ -842,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: $!"; } @@ -862,6 +983,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 CVars)> +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