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 = qsearchs( 'tax_rate_location',
124 ( map { $_ => $self->$_ } qw( data_vendor geocode ) ),
127 unless $self->disabled;
129 $t = $self->by_key( $self->taxratelocationnum )
130 if ( !$t && $self->taxratelocationnum );
132 return "geocode ". $self->geocode. " already in use for this vendor"
133 if ( $t && $t->taxratelocationnum != $self->taxratelocationnum );
135 return "may only be disabled"
136 if ( $t && scalar( grep { $t->$_ ne $self->$_ }
137 grep { $_ ne 'disabled' }
147 Finds an existing, non-disabled tax jurisdiction matching the data_vendor
148 and geocode fields. If there is one, updates its city, county, state, and
149 country to match this record. If there is no existing record, inserts this
156 my $existing = qsearchs('tax_rate_location', {
158 data_vendor => $self->data_vendor,
159 geocode => $self->geocode
163 foreach (qw(city county state country)) {
164 if ($self->get($_) ne $existing->get($_)) {
168 $self->set(taxratelocationnum => $existing->taxratelocationnum);
170 return $self->replace($existing);
175 return $self->insert;
183 =item location_sql KEY => VALUE, ...
185 Returns an SQL fragment identifying matching tax_rate_location /
186 cust_bill_pkg_tax_rate_location records.
188 Parameters are county, state, city and locationtaxid
193 my($class, %param) = @_;
196 'city' => 'tax_rate_location.city',
197 'county' => 'tax_rate_location.county',
198 'state' => 'tax_rate_location.state',
199 'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid',
202 my %ph = map { $pn{$_} => dbh->quote($param{$_}) } keys %pn;
205 map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } keys %ph
216 =item batch_import HASHREF, JOB
218 Starts importing tax_rate_location records from a file. HASHREF must contain
219 'filehandle' (an open handle to the input file) and 'format' (one of 'cch',
220 'cch-fixed', 'cch-update', 'cch-fixed-update', or 'billsoft'). JOB is an
221 L<FS::queue> object to receive progress messages.
225 # XXX move this into TaxEngine modules at some point
228 my ($param, $job) = @_;
230 my $fh = $param->{filehandle};
231 my $format = $param->{'format'};
239 my @column_lengths = ();
240 my @column_callbacks = ();
241 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
242 $format =~ s/-fixed//;
243 my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
244 push @column_lengths, qw( 28 25 2 10 );
245 push @column_lengths, 1 if $format eq 'cch-update';
246 push @column_callbacks, $trim foreach (@column_lengths);
250 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
251 if ( $job || scalar(@column_callbacks) ) { # this makes zero sense
253 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
254 return $error if $error;
257 if ( $format eq 'cch' || $format eq 'cch-update' ) {
258 @fields = qw( city county state geocode );
259 push @fields, 'actionflag' if $format eq 'cch-update';
264 $hash->{'data_vendor'} ='cch';
266 if (exists($hash->{'actionflag'}) && $hash->{'actionflag'} eq 'D') {
267 delete($hash->{actionflag});
269 $hash->{disabled} = '';
270 my $tax_rate_location = qsearchs('tax_rate_location', $hash);
271 return "Can't find tax_rate_location to delete: ".
272 join(" ", map { "$_ => ". $hash->{$_} } @fields)
273 unless $tax_rate_location;
275 $tax_rate_location->disabled('Y');
276 my $error = $tax_rate_location->replace;
277 return $error if $error;
279 delete($hash->{$_}) foreach (keys %$hash);
282 delete($hash->{'actionflag'});
288 } elsif ( $format eq 'billsoft' ) {
289 @fields = ( qw( geocode alt_location country state county city ), '', '' );
293 if ($hash->{alt_location}) {
294 # don't import these; the jurisdiction should be named using its
300 $hash->{data_vendor} = 'billsoft';
301 # unlike cust_tax_location, keep the whole-country and whole-state
302 # rows, but strip the whitespace
303 $hash->{county} =~ s/^ //g;
304 $hash->{state} =~ s/^ //g;
305 $hash->{country} =~ s/^ //g;
306 $hash->{city} =~ s/[^\w ]//g; # remove asterisks and other bad things
307 $hash->{country} = substr($hash->{country}, 0, 2);
311 } elsif ( $format eq 'extended' ) {
312 die "unimplemented\n";
316 die "unknown format $format";
319 eval "use Text::CSV_XS;";
322 my $csv = new Text::CSV_XS;
326 local $SIG{HUP} = 'IGNORE';
327 local $SIG{INT} = 'IGNORE';
328 local $SIG{QUIT} = 'IGNORE';
329 local $SIG{TERM} = 'IGNORE';
330 local $SIG{TSTP} = 'IGNORE';
331 local $SIG{PIPE} = 'IGNORE';
333 my $oldAutoCommit = $FS::UID::AutoCommit;
334 local $FS::UID::AutoCommit = 0;
337 while ( defined($line=<$fh>) ) {
338 $csv->parse($line) or do {
339 $dbh->rollback if $oldAutoCommit;
340 return "can't parse: ". $csv->error_input();
343 if ( $job ) { # progress bar
344 if ( time - $min_sec > $last ) {
345 my $error = $job->update_statustext(
346 int( 100 * $imported / $count ) .
347 ',Creating tax jurisdiction records'
349 die $error if $error;
354 my @columns = $csv->fields();
356 my %tax_rate_location = ();
357 foreach my $field ( @fields ) {
358 $tax_rate_location{$field} = shift @columns;
360 if ( scalar( @columns ) ) {
361 $dbh->rollback if $oldAutoCommit;
362 return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line";
365 my $error = &{$hook}(\%tax_rate_location);
367 $dbh->rollback if $oldAutoCommit;
371 if (scalar(keys %tax_rate_location)) { #inserts only
373 my $tax_rate_location = new FS::tax_rate_location( \%tax_rate_location );
374 $error = $tax_rate_location->insert;
377 $dbh->rollback if $oldAutoCommit;
378 return "can't insert tax_rate_location for $line: $error";
387 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
389 return "Empty file!" unless ($imported || $format eq 'cch-update');
397 Currently somewhat specific to CCH supplied data.
401 L<FS::Record>, schema.html from the base documentation.