From 6a24254d490f3d023728044daba0765f20f6971e Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 15 Apr 2008 20:47:59 +0000 Subject: [PATCH] (finally) wrap up new tax rate engine (for now) --- FS/FS/cust_main.pm | 10 +- FS/FS/cust_tax_location.pm | 71 +++++++- FS/FS/part_pkg.pm | 4 +- FS/FS/part_pkg_taxproduct.pm | 14 +- FS/FS/part_pkg_taxrate.pm | 66 +++++++- FS/FS/tax_class.pm | 155 +++++++++++++++-- FS/FS/tax_rate.pm | 285 ++++++++++++++++++++++++++++++-- httemplate/elements/file-upload.html | 80 +++++++++ httemplate/elements/header-minimal.html | 19 +++ httemplate/misc/file-upload.html | 47 ++++++ httemplate/misc/process/tax-import.cgi | 55 +----- httemplate/misc/process/tax-upgrade.cgi | 147 ++++++++++++++++ httemplate/misc/tax-import.cgi | 70 +++++--- 13 files changed, 907 insertions(+), 116 deletions(-) create mode 100644 httemplate/elements/file-upload.html create mode 100644 httemplate/elements/header-minimal.html create mode 100644 httemplate/misc/file-upload.html create mode 100644 httemplate/misc/process/tax-upgrade.cgi diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 3490e46d2..168c43dc8 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -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, } ); diff --git a/FS/FS/cust_tax_location.pm b/FS/FS/cust_tax_location.pm index 11faa3f1f..66d32a5a7 100644 --- a/FS/FS/cust_tax_location.pm +++ b/FS/FS/cust_tax_location.pm @@ -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; diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index cffdc8857..d4570f7fc 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -749,7 +749,7 @@ specified by GEOCODE (see L 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, diff --git a/FS/FS/part_pkg_taxproduct.pm b/FS/FS/part_pkg_taxproduct.pm index 000d0d46b..c66fb8c90 100644 --- a/FS/FS/part_pkg_taxproduct.pm +++ b/FS/FS/part_pkg_taxproduct.pm @@ -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, diff --git a/FS/FS/part_pkg_taxrate.pm b/FS/FS/part_pkg_taxrate.pm index 3e7e7bd5b..5ef887da4 100644 --- a/FS/FS/part_pkg_taxrate.pm +++ b/FS/FS/part_pkg_taxrate.pm @@ -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; diff --git a/FS/FS/tax_class.pm b/FS/FS/tax_class.pm index 0a939adf6..ed63939f6 100644 --- a/FS/FS/tax_class.pm +++ b/FS/FS/tax_class.pm @@ -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, schema.html from the base documentation. diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 3d56a0de1..268edcae0 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -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 index 000000000..2859a676f --- /dev/null +++ b/httemplate/elements/file-upload.html @@ -0,0 +1,80 @@ + + + + + +% foreach (@field) { + + <% shift @label %> + + +% } +
Debugging:
+ +<%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'}; +} + + diff --git a/httemplate/elements/header-minimal.html b/httemplate/elements/header-minimal.html new file mode 100644 index 000000000..f74a9cc62 --- /dev/null +++ b/httemplate/elements/header-minimal.html @@ -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 section +% my $conf = new FS::Conf; +% + + + + + + <% $title %> + + + + + <% $head %> + + > diff --git a/httemplate/misc/file-upload.html b/httemplate/misc/file-upload.html new file mode 100644 index 000000000..9649d3663 --- /dev/null +++ b/httemplate/misc/file-upload.html @@ -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); + + diff --git a/httemplate/misc/process/tax-import.cgi b/httemplate/misc/process/tax-import.cgi index 77fba61f5..f66d6db29 100644 --- a/httemplate/misc/process/tax-import.cgi +++ b/httemplate/misc/process/tax-import.cgi @@ -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; diff --git a/httemplate/misc/process/tax-upgrade.cgi b/httemplate/misc/process/tax-upgrade.cgi new file mode 100644 index 000000000..8782282bd --- /dev/null +++ b/httemplate/misc/process/tax-upgrade.cgi @@ -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; +} + + diff --git a/httemplate/misc/tax-import.cgi b/httemplate/misc/tax-import.cgi index 6bdea6a56..9044ac9eb 100644 --- a/httemplate/misc/tax-import.cgi +++ b/httemplate/misc/tax-import.cgi @@ -3,41 +3,63 @@ Import a CSV file set containing tax rate records.

-
+<% include( '/elements/progress-init.html', + 'TaxRateUpload', + [ 'format', 'uploaded_files' ], + 'process/tax-import.cgi', + { 'message' => 'Tax rates imported' }, + ) +%> -<% &ntable("#cccccc", 2) %> + + +
+ +<% &ntable("#cccccc", 2) %> Format - - code CSV filename - - - - - plus4 CSV filename - - - - - txmatrix CSV filename - - - - - detail CSV filename - - - +<% 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, + ) +%> - + -- 2.11.0