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;
122 $t = $self->existing_search
123 unless $self->disabled;
125 $t = $self->by_key( $self->taxratelocationnum )
126 if !$t && $self->taxratelocationnum;
128 return "geocode ". $self->geocode. " already in use for this vendor"
129 if ( $t && $t->taxratelocationnum != $self->taxratelocationnum );
131 return "may only be disabled"
132 if ( $t && scalar( grep { $t->$_ ne $self->$_ }
133 grep { $_ ne 'disabled' }
143 Finds an existing, non-disabled tax jurisdiction matching the data_vendor
144 and geocode fields. If there is one, updates its city, county, state, and
145 country to match this record. If there is no existing record, inserts this
152 my $existing = $self->existing_search;
155 foreach (qw(city county state country)) {
156 if ($self->get($_) ne $existing->get($_)) {
160 $self->set(taxratelocationnum => $existing->taxratelocationnum);
162 return $self->replace($existing);
167 return $self->insert;
171 sub existing_search {
174 my @unique = qw( data_vendor geocode );
175 push @unique, qw( state country )
176 if $self->data_vendor eq 'compliance_solutions';
178 qsearchs( 'tax_rate_location',
180 map { $_ => $self->$_ } @unique
189 =item location_sql KEY => VALUE, ...
191 Returns an SQL fragment identifying matching tax_rate_location /
192 cust_bill_pkg_tax_rate_location records.
194 Parameters are county, state, city and locationtaxid
199 my($class, %param) = @_;
202 'city' => 'tax_rate_location.city',
203 'county' => 'tax_rate_location.county',
204 'state' => 'tax_rate_location.state',
205 'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid',
208 my %ph = map { $pn{$_} => dbh->quote($param{$_}) } keys %pn;
211 map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } keys %ph
222 =item batch_import HASHREF, JOB
224 Starts importing tax_rate_location records from a file. HASHREF must contain
225 'filehandle' (an open handle to the input file) and 'format' (one of 'cch',
226 'cch-fixed', 'cch-update', 'cch-fixed-update', or 'billsoft'). JOB is an
227 L<FS::queue> object to receive progress messages.
231 # XXX move this into TaxEngine modules at some point
234 my ($param, $job) = @_;
236 my $fh = $param->{filehandle};
237 my $format = $param->{'format'};
245 my @column_lengths = ();
246 my @column_callbacks = ();
247 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
248 $format =~ s/-fixed//;
249 my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
250 push @column_lengths, qw( 28 25 2 10 );
251 push @column_lengths, 1 if $format eq 'cch-update';
252 push @column_callbacks, $trim foreach (@column_lengths);
256 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
257 if ( $job || scalar(@column_callbacks) ) { # this makes zero sense
259 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
260 return $error if $error;
263 if ( $format eq 'cch' || $format eq 'cch-update' ) {
264 @fields = qw( city county state geocode );
265 push @fields, 'actionflag' if $format eq 'cch-update';
270 $hash->{'data_vendor'} ='cch';
272 if (exists($hash->{'actionflag'}) && $hash->{'actionflag'} eq 'D') {
273 delete($hash->{actionflag});
275 $hash->{disabled} = '';
276 my $tax_rate_location = qsearchs('tax_rate_location', $hash);
277 return "Can't find tax_rate_location to delete: ".
278 join(" ", map { "$_ => ". $hash->{$_} } @fields)
279 unless $tax_rate_location;
281 $tax_rate_location->disabled('Y');
282 my $error = $tax_rate_location->replace;
283 return $error if $error;
285 delete($hash->{$_}) foreach (keys %$hash);
288 delete($hash->{'actionflag'});
294 } elsif ( $format eq 'billsoft' ) {
295 @fields = ( qw( geocode alt_location country state county city ), '', '' );
299 if ($hash->{alt_location}) {
300 # don't import these; the jurisdiction should be named using its
306 $hash->{data_vendor} = 'billsoft';
307 # unlike cust_tax_location, keep the whole-country and whole-state
308 # rows, but strip the whitespace
309 $hash->{county} =~ s/^ //g;
310 $hash->{state} =~ s/^ //g;
311 $hash->{country} =~ s/^ //g;
312 $hash->{city} =~ s/[^\w ]//g; # remove asterisks and other bad things
313 $hash->{country} = substr($hash->{country}, 0, 2);
317 } elsif ( $format eq 'extended' ) {
318 die "unimplemented\n";
322 die "unknown format $format";
325 eval "use Text::CSV_XS;";
328 my $csv = new Text::CSV_XS;
332 local $SIG{HUP} = 'IGNORE';
333 local $SIG{INT} = 'IGNORE';
334 local $SIG{QUIT} = 'IGNORE';
335 local $SIG{TERM} = 'IGNORE';
336 local $SIG{TSTP} = 'IGNORE';
337 local $SIG{PIPE} = 'IGNORE';
339 my $oldAutoCommit = $FS::UID::AutoCommit;
340 local $FS::UID::AutoCommit = 0;
343 while ( defined($line=<$fh>) ) {
344 $csv->parse($line) or do {
345 $dbh->rollback if $oldAutoCommit;
346 return "can't parse: ". $csv->error_input();
349 if ( $job ) { # progress bar
350 if ( time - $min_sec > $last ) {
351 my $error = $job->update_statustext(
352 int( 100 * $imported / $count ) .
353 ',Creating tax jurisdiction records'
355 die $error if $error;
360 my @columns = $csv->fields();
362 my %tax_rate_location = ();
363 foreach my $field ( @fields ) {
364 $tax_rate_location{$field} = shift @columns;
366 if ( scalar( @columns ) ) {
367 $dbh->rollback if $oldAutoCommit;
368 return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line";
371 my $error = &{$hook}(\%tax_rate_location);
373 $dbh->rollback if $oldAutoCommit;
377 if (scalar(keys %tax_rate_location)) { #inserts only
379 my $tax_rate_location = new FS::tax_rate_location( \%tax_rate_location );
380 $error = $tax_rate_location->insert;
383 $dbh->rollback if $oldAutoCommit;
384 return "can't insert tax_rate_location for $line: $error";
393 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
395 return "Empty file!" unless ($imported || $format eq 'cch-update');
404 my $sql = "UPDATE tax_rate_location SET data_vendor = 'compliance_solutions' WHERE data_vendor = 'compliance solutions'";
406 my $sth = dbh->prepare($sql) or die $DBI::errstr;
407 $sth->execute() or die $sth->errstr;
413 Currently somewhat specific to CCH supplied data.
417 L<FS::Record>, schema.html from the base documentation.