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('district')
115 || $self->ut_textn('city')
116 || $self->ut_textn('county')
117 || $self->ut_textn('state')
118 || $self->ut_enum('disabled', [ '', 'Y' ])
120 return $error if $error;
123 $t = $self->existing_search
124 unless $self->disabled;
126 $t = $self->by_key( $self->taxratelocationnum )
127 if !$t && $self->taxratelocationnum;
129 return "geocode ". $self->geocode. " already in use for this vendor"
130 if ( $t && $t->taxratelocationnum != $self->taxratelocationnum );
132 return "may only be disabled"
133 if ( $t && scalar( grep { $t->$_ ne $self->$_ }
134 grep { $_ ne 'disabled' }
144 Finds an existing, non-disabled tax jurisdiction matching the data_vendor
145 and geocode fields. If there is one, updates its city, county, state, and
146 country to match this record. If there is no existing record, inserts this
153 my $existing = $self->existing_search;
156 foreach (qw(city county state country)) {
157 if ($self->get($_) ne $existing->get($_)) {
161 $self->set(taxratelocationnum => $existing->taxratelocationnum);
163 return $self->replace($existing);
168 return $self->insert;
172 sub existing_search {
175 qsearchs( 'tax_rate_location',
177 map { $_ => $self->$_ } qw( data_vendor geocode )
186 =item location_sql KEY => VALUE, ...
188 Returns an SQL fragment identifying matching tax_rate_location /
189 cust_bill_pkg_tax_rate_location records.
191 Parameters are county, state, city and locationtaxid
196 my($class, %param) = @_;
199 'city' => 'tax_rate_location.city',
200 'county' => 'tax_rate_location.county',
201 'state' => 'tax_rate_location.state',
202 'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid',
205 my %ph = map { $pn{$_} => dbh->quote($param{$_}) } keys %pn;
208 map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } keys %ph
219 =item batch_import HASHREF, JOB
221 Starts importing tax_rate_location records from a file. HASHREF must contain
222 'filehandle' (an open handle to the input file) and 'format' (one of 'cch',
223 'cch-fixed', 'cch-update', 'cch-fixed-update', or 'billsoft'). JOB is an
224 L<FS::queue> object to receive progress messages.
228 # XXX move this into TaxEngine modules at some point
231 my ($param, $job) = @_;
233 my $fh = $param->{filehandle};
234 my $format = $param->{'format'};
242 my @column_lengths = ();
243 my @column_callbacks = ();
244 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
245 $format =~ s/-fixed//;
246 my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
247 push @column_lengths, qw( 28 25 2 10 );
248 push @column_lengths, 1 if $format eq 'cch-update';
249 push @column_callbacks, $trim foreach (@column_lengths);
253 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
254 if ( $job || scalar(@column_callbacks) ) { # this makes zero sense
256 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
257 return $error if $error;
260 if ( $format eq 'cch' || $format eq 'cch-update' ) {
261 @fields = qw( city county state geocode );
262 push @fields, 'actionflag' if $format eq 'cch-update';
267 $hash->{'data_vendor'} ='cch';
269 if (exists($hash->{'actionflag'}) && $hash->{'actionflag'} eq 'D') {
270 delete($hash->{actionflag});
272 $hash->{disabled} = '';
273 my $tax_rate_location = qsearchs('tax_rate_location', $hash);
274 return "Can't find tax_rate_location to delete: ".
275 join(" ", map { "$_ => ". $hash->{$_} } @fields)
276 unless $tax_rate_location;
278 $tax_rate_location->disabled('Y');
279 my $error = $tax_rate_location->replace;
280 return $error if $error;
282 delete($hash->{$_}) foreach (keys %$hash);
285 delete($hash->{'actionflag'});
291 } elsif ( $format eq 'billsoft' ) {
292 @fields = ( qw( geocode alt_location country state county city ), '', '' );
296 if ($hash->{alt_location}) {
297 # don't import these; the jurisdiction should be named using its
303 $hash->{data_vendor} = 'billsoft';
304 # unlike cust_tax_location, keep the whole-country and whole-state
305 # rows, but strip the whitespace
306 $hash->{county} =~ s/^ //g;
307 $hash->{state} =~ s/^ //g;
308 $hash->{country} =~ s/^ //g;
309 $hash->{city} =~ s/[^\w ]//g; # remove asterisks and other bad things
310 $hash->{country} = substr($hash->{country}, 0, 2);
314 } elsif ( $format eq 'extended' ) {
315 die "unimplemented\n";
319 die "unknown format $format";
322 eval "use Text::CSV_XS;";
325 my $csv = new Text::CSV_XS;
329 local $SIG{HUP} = 'IGNORE';
330 local $SIG{INT} = 'IGNORE';
331 local $SIG{QUIT} = 'IGNORE';
332 local $SIG{TERM} = 'IGNORE';
333 local $SIG{TSTP} = 'IGNORE';
334 local $SIG{PIPE} = 'IGNORE';
336 my $oldAutoCommit = $FS::UID::AutoCommit;
337 local $FS::UID::AutoCommit = 0;
340 while ( defined($line=<$fh>) ) {
341 $csv->parse($line) or do {
342 $dbh->rollback if $oldAutoCommit;
343 return "can't parse: ". $csv->error_input();
346 if ( $job ) { # progress bar
347 if ( time - $min_sec > $last ) {
348 my $error = $job->update_statustext(
349 int( 100 * $imported / $count ) .
350 ',Creating tax jurisdiction records'
352 die $error if $error;
357 my @columns = $csv->fields();
359 my %tax_rate_location = ();
360 foreach my $field ( @fields ) {
361 $tax_rate_location{$field} = shift @columns;
363 if ( scalar( @columns ) ) {
364 $dbh->rollback if $oldAutoCommit;
365 return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line";
368 my $error = &{$hook}(\%tax_rate_location);
370 $dbh->rollback if $oldAutoCommit;
374 if (scalar(keys %tax_rate_location)) { #inserts only
376 my $tax_rate_location = new FS::tax_rate_location( \%tax_rate_location );
377 $error = $tax_rate_location->insert;
380 $dbh->rollback if $oldAutoCommit;
381 return "can't insert tax_rate_location for $line: $error";
390 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
392 return "Empty file!" unless ($imported || $format eq 'cch-update');
399 #actually no, we want to leave those records behind now that they're giving us
403 # my $sql = "UPDATE tax_rate_location SET data_vendor = 'compliance_solutions' WHERE data_vendor = 'compliance solutions'";
405 # my $sth = dbh->prepare($sql) or die $DBI::errstr;
406 # $sth->execute() or die $sth->errstr;
411 Currently somewhat specific to CCH supplied data.
415 L<FS::Record>, schema.html from the base documentation.