6 use FS::Record qw( qsearch qsearchs );
7 use FS::Misc qw( csv_from_fixed );
8 use FS::part_pkg_taxrate;
9 use FS::part_pkg_taxoverride;
11 @ISA = qw(FS::Record);
15 FS::tax_class - Object methods for tax_class records
21 $record = new FS::tax_class \%hash;
22 $record = new FS::tax_class { 'column' => 'value' };
24 $error = $record->insert;
26 $error = $new_record->replace($old_record);
28 $error = $record->delete;
30 $error = $record->check;
34 An FS::tax_class object represents a class of tax definitions. FS::tax_class
35 inherits from FS::Record.
37 This should not be confused with L<FS::part_pkg_taxclass>, which defines tax
38 classes for I<package> definitions. The two kinds of tax classes are
41 The following fields are currently supported:
45 =item taxclassnum - Primary key
47 =item data_vendor - Vendor of the tax data ('cch' or 'billsoft')
49 =item taxclass - The identifier used in the tax tables for this class.
51 =item description - Human readable description of the tax class.
61 Creates a new tax class. To add the tax class to the database, see L<"insert">.
63 Note that this stores the hash reference, not a distinct copy of the hash it
64 points to. You can ask the object for a copy with the I<hash> method.
68 sub table { 'tax_class'; }
72 Adds this record to the database. If there is an error, returns the error,
73 otherwise returns false.
79 Delete this record from the database.
86 #return "Can't delete a tax class which has package tax rates!"
87 #if qsearch( 'part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum
88 # If this tax class is manually assigned to a package,
89 # then return a useful error message instead of just having a conniption.
90 my @overrides = qsearch( 'part_pkg_taxoverride', {
91 'taxclassnum' => $self->taxclassnum
94 return "Tried to delete tax class " . $self->taxclass .
95 ", which is assigned to package definition " .
96 join(', ', map { '#'.$_->pkgpart} @overrides) .
100 # part_pkg_taxrate.taxclass identifies taxes belonging to this taxclass.
101 # part_pkg_taxrate.taxclassnumtaxed identifies taxes applying to this
103 # If this taxclass goes away, remove all of them. (CCH upgrade CAN'T
104 # remove them, because it removes the tax_class first and then doesn't
105 # know what the taxclassnum was. Yeah, I know. So it will just skip
106 # over them at the TXMATRIX stage.)
107 my @part_pkg_taxrate = (
108 qsearch('part_pkg_taxrate', { 'taxclassnum' => $self->taxclassnum }),
109 qsearch('part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum })
111 foreach (@part_pkg_taxrate) {
112 my $error = $_->delete;
113 return "when deleting taxclass ".$self->taxclass.": $error"
117 $self->SUPER::delete(@_);
121 =item replace OLD_RECORD
123 Replaces the OLD_RECORD with this one in the database. If there is an error,
124 returns the error, otherwise returns false.
130 Checks all fields to make sure this is a valid tax class. If there is
131 an error, returns the error, otherwise returns false. Called by the insert
140 $self->ut_numbern('taxclassnum')
141 || $self->ut_text('taxclass')
142 || $self->ut_textn('data_vendor')
143 || $self->ut_textn('description')
145 return $error if $error;
152 Loads part_pkg_taxrate records from an external CSV file. If there is
153 an error, returns the error, otherwise returns false.
158 my ($param, $job) = @_;
160 my $fh = $param->{filehandle};
161 my $format = $param->{'format'};
170 my @column_lengths = ();
171 my @column_callbacks = ();
172 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
173 $format =~ s/-fixed//;
174 push @column_lengths, qw( 8 10 3 2 2 10 100 );
175 push @column_lengths, 1 if $format eq 'cch-update';
179 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
180 if ( $job || scalar(@column_lengths) ) {
181 my $error = csv_from_fixed(\$fh, \$count, \@column_lengths);
182 return $error if $error;
185 if ( $format eq 'cch' || $format eq 'cch-update' ) {
186 @fields = qw( table name pos length number value description );
187 push @fields, 'actionflag' if $format eq 'cch-update';
192 if ($hash->{'table'} eq 'DETAIL') {
193 push @{$data->{'taxcat'}}, [ $hash->{'value'}, $hash->{'description'} ]
194 if ($hash->{'name'} eq 'TAXCAT' &&
195 (!exists($hash->{actionflag}) || $hash->{actionflag} eq 'I') );
197 push @{$data->{'taxtype'}}, [ $hash->{'value'}, $hash->{'description'} ]
198 if ($hash->{'name'} eq 'TAXTYPE' &&
199 (!exists($hash->{actionflag}) || $hash->{actionflag} eq 'I') );
201 if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
202 my $name = $hash->{'name'};
203 my $value = $hash->{'value'};
204 return "Bad value for $name: $value"
205 unless $value =~ /^\d+$/;
207 if ($name eq 'TAXCAT' || $name eq 'TAXTYPE') {
208 my @tax_class = qsearch( 'tax_class',
209 { 'data_vendor' => 'cch' },
211 "AND taxclass LIKE '".
212 ($name eq 'TAXTYPE' ? $value : '%').":".
213 ($name eq 'TAXCAT' ? $value : '%')."'",
215 foreach (@tax_class) {
216 my $error = $_->delete;
217 return $error if $error;
225 for qw( data_vendor table name pos length number value description );
226 delete($hash->{actionflag}) if exists($hash->{actionflag});
234 my $sql = "SELECT DISTINCT ".
235 "substring(taxclass from 1 for position(':' in taxclass)-1),".
236 "substring(description from 1 for position(':' in description)-1) ".
237 "FROM tax_class WHERE data_vendor='cch'";
239 my $sth = $dbh->prepare($sql) or die $dbh->errstr;
240 $sth->execute or die $sth->errstr;
241 my @old_types = @{$sth->fetchall_arrayref};
243 $sql = "SELECT DISTINCT ".
244 "substring(taxclass from position(':' in taxclass)+1),".
245 "substring(description from position(':' in description)+1) ".
246 "FROM tax_class WHERE data_vendor='cch'";
248 $sth = $dbh->prepare($sql) or die $dbh->errstr;
249 $sth->execute or die $sth->errstr;
250 my @old_cats = @{$sth->fetchall_arrayref};
252 my $catcount = exists($data->{'taxcat'}) ? scalar(@{$data->{'taxcat'}})
254 my $typecount = exists($data->{'taxtype'}) ? scalar(@{$data->{'taxtype'}})
257 my $count = scalar(@old_types) * $catcount
258 + $typecount * (scalar(@old_cats) + $catcount);
260 $imported = 1 if $format eq 'cch-update'; #empty file ok
262 foreach my $type (@old_types) {
263 foreach my $cat (@{$data->{'taxcat'}}) {
265 if ( $job ) { # progress bar
266 if ( time - $min_sec > $last ) {
267 my $error = $job->update_statustext(
268 int( 100 * $imported / $count ). ",Importing tax classes"
270 die $error if $error;
275 my %hash = ( 'data_vendor' => 'cch',
276 'taxclass' => $type->[0].':'.$cat->[0],
277 'description' => $type->[1].':'.$cat->[1],
279 unless ( qsearchs('tax_class', \%hash) ) {
280 my $tax_class = new FS::tax_class \%hash;
281 my $error = $tax_class->insert;
283 return "can't insert tax_class for ".
284 " old TAXTYPE ". $type->[0].':'.$type->[1].
285 " and new TAXCAT ". $cat->[0].':'. $cat->[1].
295 foreach my $type (@{$data->{'taxtype'}}) {
296 foreach my $cat (@old_cats, @{$data->{'taxcat'}}) {
298 if ( $job ) { # progress bar
299 if ( time - $min_sec > $last ) {
300 my $error = $job->update_statustext(
301 int( 100 * $imported / $count ). ",Importing tax classes"
303 die $error if $error;
309 new FS::tax_class( { 'data_vendor' => 'cch',
310 'taxclass' => $type->[0].':'.$cat->[0],
311 'description' => $type->[1].':'.$cat->[1],
313 my $error = $tax_class->insert;
314 return "can't insert tax_class for new TAXTYPE $type and TAXCAT $cat: $error" if $error;
322 } elsif ( $format eq 'billsoft' ) {
323 # Billsoft doesn't actually have a format for this; it's just my own
324 # invention to have a way to load the list of tax classes from the
326 @fields = qw( taxclass description );
327 $endhook = $hook = sub {};
329 } elsif ( $format eq 'extended' ) {
330 die "unimplemented\n";
334 die "unknown format $format";
337 eval "use Text::CSV_XS;";
340 my $csv = new Text::CSV_XS;
342 local $SIG{HUP} = 'IGNORE';
343 local $SIG{INT} = 'IGNORE';
344 local $SIG{QUIT} = 'IGNORE';
345 local $SIG{TERM} = 'IGNORE';
346 local $SIG{TSTP} = 'IGNORE';
347 local $SIG{PIPE} = 'IGNORE';
349 my $oldAutoCommit = $FS::UID::AutoCommit;
350 local $FS::UID::AutoCommit = 0;
352 while ( defined($line=<$fh>) ) {
354 if ( $job ) { # progress bar
355 if ( time - $min_sec > $last ) {
356 my $error = $job->update_statustext(
357 int( 100 * $imported / $count ). ",Importing tax classes"
359 die $error if $error;
364 $csv->parse($line) or do {
365 $dbh->rollback if $oldAutoCommit;
366 return "can't parse: ". $csv->error_input();
369 my @columns = $csv->fields();
371 my %tax_class = ( 'data_vendor' => $format );
372 foreach my $field ( @fields ) {
373 $tax_class{$field} = shift @columns;
375 if ( scalar( @columns ) ) {
376 $dbh->rollback if $oldAutoCommit;
377 return "Unexpected trailing columns in line (wrong format?) importing tax_class: $line";
380 my $error = &{$hook}(\%tax_class);
382 $dbh->rollback if $oldAutoCommit;
386 next unless scalar(keys %tax_class);
388 my $tax_class = new FS::tax_class( \%tax_class );
389 $error = $tax_class->insert;
391 $dbh->rollback if $oldAutoCommit;
392 return "can't insert tax_class for $line: $error";
398 my $error = &{$endhook}();
400 $dbh->rollback if $oldAutoCommit;
401 return "can't run end hook: $error";
404 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
406 return "Empty File!" unless ($imported || $format eq 'cch-update');
418 L<FS::Record>, schema.html from the base documentation.