1 package FS::tax_rate_location;
4 use base qw( FS::Record );
5 use FS::Record qw( qsearch qsearchs dbh );
6 use FS::Misc qw( csv_from_fixed );
10 FS::tax_rate_location - Object methods for tax_rate_location records
14 use FS::tax_rate_location;
16 $record = new FS::tax_rate_location \%hash;
17 $record = new FS::tax_rate_location { 'column' => 'value' };
19 $error = $record->insert;
21 $error = $new_record->replace($old_record);
23 $error = $record->delete;
25 $error = $record->check;
29 An FS::tax_rate_location object represents a tax jurisdiction. The only
30 functional field is "geocode", a foreign key to tax rates (L<FS::tax_rate>)
31 that apply in the jurisdiction. The city, county, state, and country fields
32 are provided for description and reporting.
34 FS::tax_rate_location inherits from FS::Record. The following fields are
39 =item taxratelocationnum - Primary key (assigned automatically for new
42 =item data_vendor - The tax data vendor ('cch' or 'billsoft').
44 =item geocode - A unique geographic location code provided by the data vendor
50 =item state - State (2-letter code)
52 =item country - Country (2-letter code, optional)
54 =item disabled - If 'Y' this record is no longer active.
64 Creates a new tax rate location. To add the record to the database, see
67 Note that this stores the hash reference, not a distinct copy of the hash it
68 points to. You can ask the object for a copy with the I<hash> method.
72 sub table { 'tax_rate_location'; }
76 Adds this record to the database. If there is an error, returns the error,
77 otherwise returns false.
83 Delete this record from the database.
88 return "Can't delete tax rate locations. Set disable to 'Y' instead.";
89 # check that it is unused in any cust_bill_pkg_tax_location records instead?
92 =item replace OLD_RECORD
94 Replaces the OLD_RECORD with this one in the database. If there is an error,
95 returns the error, otherwise returns false.
101 Checks all fields to make sure this is a valid tax rate location. If there is
102 an error, returns the error, otherwise returns false. Called by the insert
111 $self->ut_numbern('taxratelocationnum')
112 || $self->ut_textn('data_vendor')
113 || $self->ut_alpha('geocode')
114 || $self->ut_textn('city')
115 || $self->ut_textn('county')
116 || $self->ut_textn('state')
117 || $self->ut_enum('disabled', [ '', 'Y' ])
119 return $error if $error;
121 my @unique = qw( data_vendor geocode );
122 push @unique, qw( state country )
123 if $self->data_vendor eq 'compliance_solutions';
126 $t = qsearchs( 'tax_rate_location',
128 ( map { $_ => $self->$_ } @unique ),
131 unless $self->disabled;
133 $t = $self->by_key( $self->taxratelocationnum )
134 if ( !$t && $self->taxratelocationnum );
136 return "geocode ". $self->geocode. " already in use for this vendor"
137 if ( $t && $t->taxratelocationnum != $self->taxratelocationnum );
139 return "may only be disabled"
140 if ( $t && scalar( grep { $t->$_ ne $self->$_ }
141 grep { $_ ne 'disabled' }
151 Finds an existing, non-disabled tax jurisdiction matching the data_vendor
152 and geocode fields. If there is one, updates its city, county, state, and
153 country to match this record. If there is no existing record, inserts this
160 my $existing = qsearchs('tax_rate_location', {
162 data_vendor => $self->data_vendor,
163 geocode => $self->geocode
167 foreach (qw(city county state country)) {
168 if ($self->get($_) ne $existing->get($_)) {
172 $self->set(taxratelocationnum => $existing->taxratelocationnum);
174 return $self->replace($existing);
179 return $self->insert;
187 =item location_sql KEY => VALUE, ...
189 Returns an SQL fragment identifying matching tax_rate_location /
190 cust_bill_pkg_tax_rate_location records.
192 Parameters are county, state, city and locationtaxid
197 my($class, %param) = @_;
200 'city' => 'tax_rate_location.city',
201 'county' => 'tax_rate_location.county',
202 'state' => 'tax_rate_location.state',
203 'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid',
206 my %ph = map { $pn{$_} => dbh->quote($param{$_}) } keys %pn;
209 map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } keys %ph
220 =item batch_import HASHREF, JOB
222 Starts importing tax_rate_location records from a file. HASHREF must contain
223 'filehandle' (an open handle to the input file) and 'format' (one of 'cch',
224 'cch-fixed', 'cch-update', 'cch-fixed-update', or 'billsoft'). JOB is an
225 L<FS::queue> object to receive progress messages.
229 # XXX move this into TaxEngine modules at some point
232 my ($param, $job) = @_;
234 my $fh = $param->{filehandle};
235 my $format = $param->{'format'};
243 my @column_lengths = ();
244 my @column_callbacks = ();
245 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
246 $format =~ s/-fixed//;
247 my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
248 push @column_lengths, qw( 28 25 2 10 );
249 push @column_lengths, 1 if $format eq 'cch-update';
250 push @column_callbacks, $trim foreach (@column_lengths);
254 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
255 if ( $job || scalar(@column_callbacks) ) { # this makes zero sense
257 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
258 return $error if $error;
261 if ( $format eq 'cch' || $format eq 'cch-update' ) {
262 @fields = qw( city county state geocode );
263 push @fields, 'actionflag' if $format eq 'cch-update';
268 $hash->{'data_vendor'} ='cch';
270 if (exists($hash->{'actionflag'}) && $hash->{'actionflag'} eq 'D') {
271 delete($hash->{actionflag});
273 $hash->{disabled} = '';
274 my $tax_rate_location = qsearchs('tax_rate_location', $hash);
275 return "Can't find tax_rate_location to delete: ".
276 join(" ", map { "$_ => ". $hash->{$_} } @fields)
277 unless $tax_rate_location;
279 $tax_rate_location->disabled('Y');
280 my $error = $tax_rate_location->replace;
281 return $error if $error;
283 delete($hash->{$_}) foreach (keys %$hash);
286 delete($hash->{'actionflag'});
292 } elsif ( $format eq 'billsoft' ) {
293 @fields = ( qw( geocode alt_location country state county city ), '', '' );
297 if ($hash->{alt_location}) {
298 # don't import these; the jurisdiction should be named using its
304 $hash->{data_vendor} = 'billsoft';
305 # unlike cust_tax_location, keep the whole-country and whole-state
306 # rows, but strip the whitespace
307 $hash->{county} =~ s/^ //g;
308 $hash->{state} =~ s/^ //g;
309 $hash->{country} =~ s/^ //g;
310 $hash->{city} =~ s/[^\w ]//g; # remove asterisks and other bad things
311 $hash->{country} = substr($hash->{country}, 0, 2);
315 } elsif ( $format eq 'extended' ) {
316 die "unimplemented\n";
320 die "unknown format $format";
323 eval "use Text::CSV_XS;";
326 my $csv = new Text::CSV_XS;
330 local $SIG{HUP} = 'IGNORE';
331 local $SIG{INT} = 'IGNORE';
332 local $SIG{QUIT} = 'IGNORE';
333 local $SIG{TERM} = 'IGNORE';
334 local $SIG{TSTP} = 'IGNORE';
335 local $SIG{PIPE} = 'IGNORE';
337 my $oldAutoCommit = $FS::UID::AutoCommit;
338 local $FS::UID::AutoCommit = 0;
341 while ( defined($line=<$fh>) ) {
342 $csv->parse($line) or do {
343 $dbh->rollback if $oldAutoCommit;
344 return "can't parse: ". $csv->error_input();
347 if ( $job ) { # progress bar
348 if ( time - $min_sec > $last ) {
349 my $error = $job->update_statustext(
350 int( 100 * $imported / $count ) .
351 ',Creating tax jurisdiction records'
353 die $error if $error;
358 my @columns = $csv->fields();
360 my %tax_rate_location = ();
361 foreach my $field ( @fields ) {
362 $tax_rate_location{$field} = shift @columns;
364 if ( scalar( @columns ) ) {
365 $dbh->rollback if $oldAutoCommit;
366 return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line";
369 my $error = &{$hook}(\%tax_rate_location);
371 $dbh->rollback if $oldAutoCommit;
375 if (scalar(keys %tax_rate_location)) { #inserts only
377 my $tax_rate_location = new FS::tax_rate_location( \%tax_rate_location );
378 $error = $tax_rate_location->insert;
381 $dbh->rollback if $oldAutoCommit;
382 return "can't insert tax_rate_location for $line: $error";
391 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
393 return "Empty file!" unless ($imported || $format eq 'cch-update');
402 my $sql = "UPDATE tax_rate_location SET data_vendor = 'compliance_solutions' WHERE data_vendor = 'compliance solutions'";
404 my $sth = dbh->prepare($sql) or die $DBI::errstr;
405 $sth->execute() or die $sth->errstr;
411 Currently somewhat specific to CCH supplied data.
415 L<FS::Record>, schema.html from the base documentation.