use vars qw( @ISA );
use FS::UID qw(dbh);
use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw( csv_from_fixed );
+use FS::part_pkg_taxrate;
+use FS::part_pkg_taxoverride;
@ISA = qw(FS::Record);
=head1 DESCRIPTION
-An FS::tax_class object represents a tax class. FS::tax_class
-inherits from FS::Record. The following fields are currently supported:
+An FS::tax_class object represents a class of tax definitions. FS::tax_class
+inherits from FS::Record.
-=over 4
-
-=item taxclassnum
+This should not be confused with L<FS::part_pkg_taxclass>, which defines tax
+classes for I<package> definitions. The two kinds of tax classes are
+completely unrelated.
-Primary key
+The following fields are currently supported:
-=item data_vendor
-
-Vendor of the tax data
+=over 4
-=item taxclass
+=item taxclassnum - Primary key
-Tax class
+=item data_vendor - Vendor of the tax data ('cch' or 'billsoft')
-=item description
+=item taxclass - The identifier used in the tax tables for this class.
-Human readable description of the tax class
+=item description - Human readable description of the tax class.
=back
=cut
+sub delete {
+ my $self = shift;
+
+ #return "Can't delete a tax class which has package tax rates!"
+ #if qsearch( 'part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum
+ # If this tax class is manually assigned to a package,
+ # then return a useful error message instead of just having a conniption.
+ my @overrides = qsearch( 'part_pkg_taxoverride', {
+ 'taxclassnum' => $self->taxclassnum
+ } );
+ if (@overrides) {
+ return "Tried to delete tax class " . $self->taxclass .
+ ", which is assigned to package definition " .
+ join(', ', map { '#'.$_->pkgpart} @overrides) .
+ ".";
+ }
+
+ # part_pkg_taxrate.taxclass identifies taxes belonging to this taxclass.
+ # part_pkg_taxrate.taxclassnumtaxed identifies taxes applying to this
+ # taxclass.
+ # If this taxclass goes away, remove all of them. (CCH upgrade CAN'T
+ # remove them, because it removes the tax_class first and then doesn't
+ # know what the taxclassnum was. Yeah, I know. So it will just skip
+ # over them at the TXMATRIX stage.)
+ my @part_pkg_taxrate = (
+ qsearch('part_pkg_taxrate', { 'taxclassnum' => $self->taxclassnum }),
+ qsearch('part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum })
+ );
+ foreach (@part_pkg_taxrate) {
+ my $error = $_->delete;
+ return "when deleting taxclass ".$self->taxclass.": $error"
+ if $error;
+ }
+
+ $self->SUPER::delete(@_);
+
+}
+
=item replace OLD_RECORD
Replaces the OLD_RECORD with this one in the database. If there is an error,
=cut
sub batch_import {
- my $param = shift;
+ my ($param, $job) = @_;
my $fh = $param->{filehandle};
my $format = $param->{'format'};
my $endhook;
my $data = {};
my $imported = 0;
+ my $dbh = dbh;
- if ( $format eq 'cch' ) {
- @fields = qw( table name pos number length value description );
+ my @column_lengths = ();
+ my @column_callbacks = ();
+ if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
+ $format =~ s/-fixed//;
+ push @column_lengths, qw( 8 10 3 2 2 10 100 );
+ push @column_lengths, 1 if $format eq 'cch-update';
+ }
+
+ my $line;
+ my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
+ if ( $job || scalar(@column_lengths) ) {
+ my $error = csv_from_fixed(\$fh, \$count, \@column_lengths);
+ return $error if $error;
+ }
+
+ if ( $format eq 'cch' || $format eq 'cch-update' ) {
+ @fields = qw( table name pos length number 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 );
+ for qw( data_vendor table name pos length number 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 ). ",Importing tax classes"
+ );
+ die $error if $error;
+ $last = time;
+ }
+ }
+
+ my %hash = ( 'data_vendor' => 'cch',
+ 'taxclass' => $type->[0].':'.$cat->[0],
+ 'description' => $type->[1].':'.$cat->[1],
+ );
+ unless ( qsearchs('tax_class', \%hash) ) {
+ my $tax_class = new FS::tax_class \%hash;
+ my $error = $tax_class->insert;
+
+ return "can't insert tax_class for ".
+ " old TAXTYPE ". $type->[0].':'.$type->[1].
+ " and new TAXCAT ". $cat->[0].':'. $cat->[1].
+ " : $error"
+ if $error;
+ }
+
+ $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 ). ",Importing tax classes"
+ );
+ 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;
+ return "can't insert tax_class for new TAXTYPE $type and TAXCAT $cat: $error" if $error;
$imported++;
}
}
+
'';
};
+ } elsif ( $format eq 'billsoft' ) {
+ # Billsoft doesn't actually have a format for this; it's just my own
+ # invention to have a way to load the list of tax classes from the
+ # documentation.
+ @fields = qw( taxclass description );
+ $endhook = $hook = sub {};
+
} elsif ( $format eq 'extended' ) {
die "unimplemented\n";
@fields = qw( );
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 ). ",Importing tax classes"
+ );
+ die $error if $error;
+ $last = time;
+ }
+ }
+
$csv->parse($line) or do {
$dbh->rollback if $oldAutoCommit;
return "can't parse: ". $csv->error_input();
foreach my $field ( @fields ) {
$tax_class{$field} = shift @columns;
}
+ if ( scalar( @columns ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Unexpected trailing columns in line (wrong format?) importing tax_class: $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";
my $error = &{$endhook}();
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "can't insert tax_class for $line: $error";
+ return "can't run end hook: $error";
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return "Empty file!" unless $imported;
+ return "Empty File!" unless ($imported || $format eq 'cch-update');
''; #no error
}
-
=back
=head1 BUGS