4 use vars qw( @ISA @EXPORT_OK $conf $DEBUG $me
5 %tax_unittypes %tax_maxtypes %tax_basetypes %tax_authorities
7 @tax_rate %tax_rate $countyflag );
11 use FS::Record qw( qsearchs qsearch dbh );
14 @ISA = qw( FS::Record );
15 @EXPORT_OK = qw( regionselector );
18 $me = '[FS::tax_rate]';
23 #ask FS::UID to run this stuff for us later
24 $FS::UID::callback{'FS::tax_rate'} = sub {
30 FS::tax_rate - Object methods for tax_rate objects
36 $record = new FS::tax_rate \%hash;
37 $record = new FS::tax_rate { 'column' => 'value' };
39 $error = $record->insert;
41 $error = $new_record->replace($old_record);
43 $error = $record->delete;
45 $error = $record->check;
47 ($county_html, $state_html, $country_html) =
48 FS::tax_rate::regionselector( $county, $state, $country );
52 An FS::tax_rate object represents a tax rate, defined by locale.
53 FS::tax_rate inherits from FS::Record. The following fields are
60 primary key (assigned automatically for new tax rates)
64 a geographic location code provided by a tax data vendor
72 a location code provided by a tax authority
76 a foreign key into FS::tax_class - the type of tax
77 referenced but FS::part_pkg_taxrate
81 the time after which the tax applies
89 second bracket percentage
93 the amount to which the tax applies (first bracket)
97 a cap on the amount of tax if a cap exists
101 percentage on out of jurisdiction purchases
105 second bracket percentage on out of jurisdiction purchases
109 one of the values in %tax_unittypes
113 amount of tax per unit
117 second bracket amount of tax per unit
121 the number of units to which the fee applies (first bracket)
125 the most units to which fees apply (first and second brackets)
129 a value from %tax_maxtypes indicating how brackets accumulate (i.e. monthly, per invoice, etc)
133 if defined, printed on invoices instead of "Tax"
137 a value from %tax_authorities
141 a value from %tax_basetypes indicating the tax basis
145 a value from %tax_passtypes indicating how the tax should displayed to the customer
149 'Y', 'N', or blank indicating the tax can be passed to the customer
153 if 'Y', this tax does not apply to setup fees
157 if 'Y', this tax does not apply to recurring fees
161 if 'Y', has been manually edited
171 Creates a new tax rate. To add the tax rate to the database, see L<"insert">.
175 sub table { 'tax_rate'; }
179 Adds this tax rate to the database. If there is an error, returns the error,
180 otherwise returns false.
184 Deletes this tax rate from the database. If there is an error, returns the
185 error, otherwise returns false.
187 =item replace OLD_RECORD
189 Replaces the OLD_RECORD with this one in the database. If there is an error,
190 returns the error, otherwise returns false.
194 Checks all fields to make sure this is a valid tax rate. If there is an error,
195 returns the error, otherwise returns false. Called by the insert and replace
203 foreach (qw( taxbase taxmax )) {
204 $self->$_(0) unless $self->$_;
207 $self->ut_numbern('taxnum')
208 || $self->ut_text('geocode')
209 || $self->ut_textn('data_vendor')
210 || $self->ut_textn('location')
211 || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
212 || $self->ut_numbern('effective_date')
213 || $self->ut_float('tax')
214 || $self->ut_floatn('excessrate')
215 || $self->ut_money('taxbase')
216 || $self->ut_money('taxmax')
217 || $self->ut_floatn('usetax')
218 || $self->ut_floatn('useexcessrate')
219 || $self->ut_numbern('unittype')
220 || $self->ut_floatn('fee')
221 || $self->ut_floatn('excessfee')
222 || $self->ut_floatn('feemax')
223 || $self->ut_numbern('maxtype')
224 || $self->ut_textn('taxname')
225 || $self->ut_numbern('taxauth')
226 || $self->ut_numbern('basetype')
227 || $self->ut_numbern('passtype')
228 || $self->ut_enum('passflag', [ '', 'Y', 'N' ])
229 || $self->ut_enum('setuptax', [ '', 'Y' ] )
230 || $self->ut_enum('recurtax', [ '', 'Y' ] )
231 || $self->ut_enum('manual', [ '', 'Y' ] )
232 || $self->SUPER::check
237 =item taxclass_description
239 Returns the human understandable value associated with the related
244 sub taxclass_description {
246 my $tax_class = qsearchs('tax_class', {'taxclassnum' => $self->taxclassnum });
247 $tax_class ? $tax_class->description : '';
252 Returns the human understandable value associated with the unittype column
256 %tax_unittypes = ( '0' => 'access line',
263 $tax_unittypes{$self->unittype};
268 Returns the human understandable value associated with the maxtype column
272 %tax_maxtypes = ( '0' => 'receipts per invoice',
273 '1' => 'receipts per item',
274 '2' => 'total utility charges per utility tax year',
275 '3' => 'total charges per utility tax year',
276 '4' => 'receipts per access line',
277 '9' => 'monthly receipts per location',
282 $tax_maxtypes{$self->maxtype};
287 Returns the human understandable value associated with the basetype column
291 %tax_basetypes = ( '0' => 'sale price',
292 '1' => 'gross receipts',
293 '2' => 'sales taxable telecom revenue',
294 '3' => 'minutes carried',
295 '4' => 'minutes billed',
296 '5' => 'gross operating revenue',
297 '6' => 'access line',
299 '8' => 'gross revenue',
300 '9' => 'portion gross receipts attributable to interstate service',
301 '10' => 'access line',
302 '11' => 'gross profits',
303 '12' => 'tariff rate',
309 $tax_basetypes{$self->basetype};
314 Returns the human understandable value associated with the taxauth column
318 %tax_authorities = ( '0' => 'federal',
323 '5' => 'county administered by state',
324 '6' => 'city administered by state',
325 '7' => 'city administered by county',
326 '8' => 'local administered by state',
327 '9' => 'local administered by county',
332 $tax_authorities{$self->taxauth};
337 Returns the human understandable value associated with the passtype column
341 %tax_passtypes = ( '0' => 'separate tax line',
342 '1' => 'separate surcharge line',
343 '2' => 'surcharge not separated',
344 '3' => 'included in base rate',
349 $tax_passtypes{$self->passtype};
358 =item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
363 my ( $selected_county, $selected_state, $selected_country,
364 $prefix, $onchange, $disabled ) = @_;
366 $prefix = '' unless defined $prefix;
370 # unless ( @tax_rate ) { #cache
371 @tax_rate = qsearch('tax_rate', {} );
372 foreach my $c ( @tax_rate ) {
373 $countyflag=1 if $c->county;
374 #push @{$tax_rate{$c->country}{$c->state}}, $c->county;
375 $tax_rate{$c->country}{$c->state}{$c->county} = 1;
378 $countyflag=1 if $selected_county;
380 my $script_html = <<END;
382 function opt(what,value,text) {
383 var optionName = new Option(text, value, false, false);
384 var length = what.length;
385 what.options[length] = optionName;
387 function ${prefix}country_changed(what) {
388 country = what.options[what.selectedIndex].text;
389 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
390 what.form.${prefix}state.options[i] = null;
392 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
394 foreach my $country ( sort keys %tax_rate ) {
395 $script_html .= "\nif ( country == \"$country\" ) {\n";
396 foreach my $state ( sort keys %{$tax_rate{$country}} ) {
397 ( my $dstate = $state ) =~ s/[\n\r]//g;
398 my $text = $dstate || '(n/a)';
399 $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
401 $script_html .= "}\n";
404 $script_html .= <<END;
406 function ${prefix}state_changed(what) {
410 $script_html .= <<END;
411 state = what.options[what.selectedIndex].text;
412 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
413 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
414 what.form.${prefix}county.options[i] = null;
417 foreach my $country ( sort keys %tax_rate ) {
418 $script_html .= "\nif ( country == \"$country\" ) {\n";
419 foreach my $state ( sort keys %{$tax_rate{$country}} ) {
420 $script_html .= "\nif ( state == \"$state\" ) {\n";
421 #foreach my $county ( sort @{$tax_rate{$country}{$state}} ) {
422 foreach my $county ( sort keys %{$tax_rate{$country}{$state}} ) {
423 my $text = $county || '(n/a)';
425 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
427 $script_html .= "}\n";
429 $script_html .= "}\n";
433 $script_html .= <<END;
438 my $county_html = $script_html;
440 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
441 $county_html .= '</SELECT>';
444 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
447 my $state_html = qq!<SELECT NAME="${prefix}state" !.
448 qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
449 foreach my $state ( sort keys %{ $tax_rate{$selected_country} } ) {
450 my $text = $state || '(n/a)';
451 my $selected = $state eq $selected_state ? 'SELECTED' : '';
452 $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
454 $state_html .= '</SELECT>';
456 $state_html .= '</SELECT>';
458 my $country_html = qq!<SELECT NAME="${prefix}country" !.
459 qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
460 my $countrydefault = $conf->config('countrydefault') || 'US';
461 foreach my $country (
462 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
465 my $selected = $country eq $selected_country ? ' SELECTED' : '';
466 $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
468 $country_html .= '</SELECT>';
470 ($county_html, $state_html, $country_html);
477 my $fh = $param->{filehandle};
478 my $format = $param->{'format'};
482 if ( $format eq 'cch' ) {
483 @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
484 excessrate effective_date taxauth taxtype taxcat taxname
485 usetax useexcessrate fee unittype feemax maxtype passflag
490 $hash->{'effective_date'} = str2time($hash->{'effective_date'});
493 join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
495 my %tax_class = ( 'data_vendor' => 'cch',
496 'taxclass' => $taxclassid,
499 my $tax_class = qsearchs( 'tax_class', \%tax_class );
500 return "Error inserting tax rate: no tax class $taxclassid"
503 $hash->{'taxclassnum'} = $tax_class->taxclassnum;
505 foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
509 my %passflagmap = ( '0' => '',
513 $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
514 if exists $passflagmap{$hash->{'passflag'}};
516 foreach (keys %$hash) {
517 $hash->{$_} = substr($hash->{$_}, 0, 80)
518 if length($hash->{$_}) > 80;
523 } elsif ( $format eq 'extended' ) {
524 die "unimplemented\n";
528 die "unknown format $format";
531 eval "use Text::CSV_XS;";
534 my $csv = new Text::CSV_XS;
538 local $SIG{HUP} = 'IGNORE';
539 local $SIG{INT} = 'IGNORE';
540 local $SIG{QUIT} = 'IGNORE';
541 local $SIG{TERM} = 'IGNORE';
542 local $SIG{TSTP} = 'IGNORE';
543 local $SIG{PIPE} = 'IGNORE';
545 my $oldAutoCommit = $FS::UID::AutoCommit;
546 local $FS::UID::AutoCommit = 0;
550 while ( defined($line=<$fh>) ) {
551 $csv->parse($line) or do {
552 $dbh->rollback if $oldAutoCommit;
553 return "can't parse: ". $csv->error_input();
556 warn "$me batch_import: $imported\n"
557 if (!($imported % 100) && $DEBUG);
559 my @columns = $csv->fields();
561 my %tax_rate = ( 'data_vendor' => $format );
562 foreach my $field ( @fields ) {
563 $tax_rate{$field} = shift @columns;
565 my $error = &{$hook}(\%tax_rate);
567 $dbh->rollback if $oldAutoCommit;
571 my $tax_rate = new FS::tax_rate( \%tax_rate );
572 $error = $tax_rate->insert;
575 $dbh->rollback if $oldAutoCommit;
576 return "can't insert tax_rate for $line: $error";
582 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
584 return "Empty file!" unless $imported;
594 regionselector? putting web ui components in here? they should probably live
599 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base