4 use vars qw( @ISA $DEBUG $me
5 %tax_unittypes %tax_maxtypes %tax_basetypes %tax_authorities
8 use Storable qw( thaw );
10 use FS::Record qw( qsearch qsearchs dbh );
12 use FS::cust_bill_pkg;
13 use FS::cust_tax_location;
14 use FS::part_pkg_taxrate;
16 use FS::Misc qw( csv_from_fixed );
18 @ISA = qw( FS::Record );
21 $me = '[FS::tax_rate]';
25 FS::tax_rate - Object methods for tax_rate objects
31 $record = new FS::tax_rate \%hash;
32 $record = new FS::tax_rate { 'column' => 'value' };
34 $error = $record->insert;
36 $error = $new_record->replace($old_record);
38 $error = $record->delete;
40 $error = $record->check;
44 An FS::tax_rate object represents a tax rate, defined by locale.
45 FS::tax_rate inherits from FS::Record. The following fields are
52 primary key (assigned automatically for new tax rates)
56 a geographic location code provided by a tax data vendor
64 a location code provided by a tax authority
68 a foreign key into FS::tax_class - the type of tax
69 referenced but FS::part_pkg_taxrate
72 the time after which the tax applies
80 second bracket percentage
84 the amount to which the tax applies (first bracket)
88 a cap on the amount of tax if a cap exists
92 percentage on out of jurisdiction purchases
96 second bracket percentage on out of jurisdiction purchases
100 one of the values in %tax_unittypes
104 amount of tax per unit
108 second bracket amount of tax per unit
112 the number of units to which the fee applies (first bracket)
116 the most units to which fees apply (first and second brackets)
120 a value from %tax_maxtypes indicating how brackets accumulate (i.e. monthly, per invoice, etc)
124 if defined, printed on invoices instead of "Tax"
128 a value from %tax_authorities
132 a value from %tax_basetypes indicating the tax basis
136 a value from %tax_passtypes indicating how the tax should displayed to the customer
140 'Y', 'N', or blank indicating the tax can be passed to the customer
144 if 'Y', this tax does not apply to setup fees
148 if 'Y', this tax does not apply to recurring fees
152 if 'Y', has been manually edited
162 Creates a new tax rate. To add the tax rate to the database, see L<"insert">.
166 sub table { 'tax_rate'; }
170 Adds this tax rate to the database. If there is an error, returns the error,
171 otherwise returns false.
175 Deletes this tax rate from the database. If there is an error, returns the
176 error, otherwise returns false.
178 =item replace OLD_RECORD
180 Replaces the OLD_RECORD with this one in the database. If there is an error,
181 returns the error, otherwise returns false.
185 Checks all fields to make sure this is a valid tax rate. If there is an error,
186 returns the error, otherwise returns false. Called by the insert and replace
194 foreach (qw( taxbase taxmax )) {
195 $self->$_(0) unless $self->$_;
198 $self->ut_numbern('taxnum')
199 || $self->ut_text('geocode')
200 || $self->ut_textn('data_vendor')
201 || $self->ut_textn('location')
202 || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
203 || $self->ut_snumbern('effective_date')
204 || $self->ut_float('tax')
205 || $self->ut_floatn('excessrate')
206 || $self->ut_money('taxbase')
207 || $self->ut_money('taxmax')
208 || $self->ut_floatn('usetax')
209 || $self->ut_floatn('useexcessrate')
210 || $self->ut_numbern('unittype')
211 || $self->ut_floatn('fee')
212 || $self->ut_floatn('excessfee')
213 || $self->ut_floatn('feemax')
214 || $self->ut_numbern('maxtype')
215 || $self->ut_textn('taxname')
216 || $self->ut_numbern('taxauth')
217 || $self->ut_numbern('basetype')
218 || $self->ut_numbern('passtype')
219 || $self->ut_enum('passflag', [ '', 'Y', 'N' ])
220 || $self->ut_enum('setuptax', [ '', 'Y' ] )
221 || $self->ut_enum('recurtax', [ '', 'Y' ] )
222 || $self->ut_enum('manual', [ '', 'Y' ] )
223 || $self->ut_enum('disabled', [ '', 'Y' ] )
224 || $self->SUPER::check
229 =item taxclass_description
231 Returns the human understandable value associated with the related
236 sub taxclass_description {
238 my $tax_class = qsearchs('tax_class', {'taxclassnum' => $self->taxclassnum });
239 $tax_class ? $tax_class->description : '';
244 Returns the human understandable value associated with the unittype column
248 %tax_unittypes = ( '0' => 'access line',
255 $tax_unittypes{$self->unittype};
260 Returns the human understandable value associated with the maxtype column
264 %tax_maxtypes = ( '0' => 'receipts per invoice',
265 '1' => 'receipts per item',
266 '2' => 'total utility charges per utility tax year',
267 '3' => 'total charges per utility tax year',
268 '4' => 'receipts per access line',
269 '9' => 'monthly receipts per location',
274 $tax_maxtypes{$self->maxtype};
279 Returns the human understandable value associated with the basetype column
283 %tax_basetypes = ( '0' => 'sale price',
284 '1' => 'gross receipts',
285 '2' => 'sales taxable telecom revenue',
286 '3' => 'minutes carried',
287 '4' => 'minutes billed',
288 '5' => 'gross operating revenue',
289 '6' => 'access line',
291 '8' => 'gross revenue',
292 '9' => 'portion gross receipts attributable to interstate service',
293 '10' => 'access line',
294 '11' => 'gross profits',
295 '12' => 'tariff rate',
301 $tax_basetypes{$self->basetype};
306 Returns the human understandable value associated with the taxauth column
310 %tax_authorities = ( '0' => 'federal',
315 '5' => 'county administered by state',
316 '6' => 'city administered by state',
317 '7' => 'city administered by county',
318 '8' => 'local administered by state',
319 '9' => 'local administered by county',
324 $tax_authorities{$self->taxauth};
329 Returns the human understandable value associated with the passtype column
333 %tax_passtypes = ( '0' => 'separate tax line',
334 '1' => 'separate surcharge line',
335 '2' => 'surcharge not separated',
336 '3' => 'included in base rate',
341 $tax_passtypes{$self->passtype};
344 =item taxline CUST_BILL_PKG|AMOUNT, ...
346 Returns a listref of a name and an amount of tax calculated for the list
347 of packages/amounts. If an error occurs, a message is returned as a scalar.
354 my $name = $self->taxname;
355 $name = 'Other surcharges'
356 if ($self->passtype == 2);
359 return [$name, $amount] # we always know how to handle disabled taxes
362 my $taxable_charged = 0;
363 my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; } @_;
365 warn "calculating taxes for ". $self->taxnum. " on ".
366 join (",", map { $_->pkgnum } @cust_bill_pkg)
369 if ($self->passflag eq 'N') {
370 return "fatal: can't (yet) handle taxes not passed to the customer";
373 if ($self->maxtype != 0 && $self->maxtype != 9) {
374 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
378 if ($self->maxtype == 9) {
379 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
380 '" threshold'; # "texas" tax
383 if ($self->basetype != 0 && $self->basetype != 1 &&
384 $self->basetype != 6 && $self->basetype != 7 &&
385 $self->basetype != 14
387 return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name.
391 unless ($self->setuptax =~ /^Y$/i) {
392 $taxable_charged += $_->setup foreach @cust_bill_pkg;
394 unless ($self->recurtax =~ /^Y$/i) {
395 $taxable_charged += $_->recur foreach @cust_bill_pkg;
398 my $taxable_units = 0;
399 unless ($self->recurtax =~ /^Y$/i) {
400 if ($self->unittype == 0) {
402 foreach (@cust_bill_pkg) {
403 $taxable_units += $_->units
404 unless $seen{$_->pkgnum};
407 }elsif ($self->unittype == 1) {
408 return qq!fatal: can't (yet) handle fee with minute unit type!;
409 }elsif ($self->unittype == 2) {
412 return qq!fatal: can't (yet) handle unknown unit type in tax!.
418 # XXX insert exemption handling here
420 # the tax or fee is applied to taxbase or feebase and then
421 # the excessrate or excess fee is applied to taxmax or feemax
424 $amount += $taxable_charged * $self->tax;
425 $amount += $taxable_units * $self->fee;
427 warn "calculated taxes as [ $name, $amount ]\n"
430 return [$name, $amount];
434 =item tax_on_tax CUST_MAIN
436 Returns a list of taxes which are candidates for taxing taxes for the
437 given customer (see L<FS::cust_main>)
443 my $cust_main = shift;
445 warn "looking up taxes on tax ". $self->taxnum. " for customer ".
449 my $geocode = $cust_main->geocode($self->data_vendor);
453 my $extra_sql = ' AND ('.
454 join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
459 my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
460 my $select = 'DISTINCT ON(taxclassnum) *';
462 # should qsearch preface columns with the table to facilitate joins?
463 my @taxclassnums = map { $_->taxclassnum }
464 qsearch( { 'table' => 'part_pkg_taxrate',
466 'hashref' => { 'data_vendor' => $self->data_vendor,
467 'taxclassnumtaxed' => $self->taxclassnum,
469 'extra_sql' => $extra_sql,
470 'order_by' => $order_by,
473 return () unless @taxclassnums;
476 "AND (". join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
478 qsearch({ 'table' => 'tax_rate',
479 'hashref' => { 'geocode' => $geocode, },
480 'extra_sql' => $extra_sql,
496 my ($param, $job) = @_;
498 my $fh = $param->{filehandle};
499 my $format = $param->{'format'};
507 my @column_lengths = ();
508 my @column_callbacks = ();
509 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
510 $format =~ s/-fixed//;
511 my $date_format = sub { my $r='';
512 /^(\d{4})(\d{2})(\d{2})$/ && ($r="$1/$2/$3");
515 $column_callbacks[8] = $date_format;
516 push @column_lengths, qw( 10 1 1 8 8 5 8 8 8 1 2 2 30 8 8 10 2 8 2 1 2 2 );
517 push @column_lengths, 1 if $format eq 'cch-update';
521 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
522 if ( $job || scalar(@column_callbacks) ) {
524 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
525 return $error if $error;
529 if ( $format eq 'cch' || $format eq 'cch-update' ) {
530 @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
531 excessrate effective_date taxauth taxtype taxcat taxname
532 usetax useexcessrate fee unittype feemax maxtype passflag
534 push @fields, 'actionflag' if $format eq 'cch-update';
539 $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
540 $hash->{'data_vendor'} ='cch';
541 $hash->{'effective_date'} = str2time($hash->{'effective_date'});
544 join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
546 my %tax_class = ( 'data_vendor' => 'cch',
547 'taxclass' => $taxclassid,
550 my $tax_class = qsearchs( 'tax_class', \%tax_class );
551 return "Error updating tax rate: no tax class $taxclassid"
554 $hash->{'taxclassnum'} = $tax_class->taxclassnum;
556 foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
560 my %passflagmap = ( '0' => '',
564 $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
565 if exists $passflagmap{$hash->{'passflag'}};
567 foreach (keys %$hash) {
568 $hash->{$_} = substr($hash->{$_}, 0, 80)
569 if length($hash->{$_}) > 80;
572 my $actionflag = delete($hash->{'actionflag'});
574 $hash->{'taxname'} =~ s/`/'/g;
575 $hash->{'taxname'} =~ s|\\|/|g;
577 return '' if $format eq 'cch'; # but not cch-update
579 if ($actionflag eq 'I') {
580 $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
581 }elsif ($actionflag eq 'D') {
582 $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
584 return "Unexpected action flag: ". $hash->{'actionflag'};
587 delete($hash->{$_}) for keys %$hash;
593 } elsif ( $format eq 'extended' ) {
594 die "unimplemented\n";
598 die "unknown format $format";
601 eval "use Text::CSV_XS;";
604 my $csv = new Text::CSV_XS;
608 local $SIG{HUP} = 'IGNORE';
609 local $SIG{INT} = 'IGNORE';
610 local $SIG{QUIT} = 'IGNORE';
611 local $SIG{TERM} = 'IGNORE';
612 local $SIG{TSTP} = 'IGNORE';
613 local $SIG{PIPE} = 'IGNORE';
615 my $oldAutoCommit = $FS::UID::AutoCommit;
616 local $FS::UID::AutoCommit = 0;
619 while ( defined($line=<$fh>) ) {
620 $csv->parse($line) or do {
621 $dbh->rollback if $oldAutoCommit;
622 return "can't parse: ". $csv->error_input();
625 if ( $job ) { # progress bar
626 if ( time - $min_sec > $last ) {
627 my $error = $job->update_statustext(
628 int( 100 * $imported / $count )
630 die $error if $error;
635 my @columns = $csv->fields();
637 my %tax_rate = ( 'data_vendor' => $format );
638 foreach my $field ( @fields ) {
639 $tax_rate{$field} = shift @columns;
641 if ( scalar( @columns ) ) {
642 $dbh->rollback if $oldAutoCommit;
643 return "Unexpected trailing columns in line (wrong format?): $line";
646 my $error = &{$hook}(\%tax_rate);
648 $dbh->rollback if $oldAutoCommit;
652 if (scalar(keys %tax_rate)) { #inserts only, not updates for cch
654 my $tax_rate = new FS::tax_rate( \%tax_rate );
655 $error = $tax_rate->insert;
658 $dbh->rollback if $oldAutoCommit;
659 return "can't insert tax_rate for $line: $error";
668 for (grep { !exists($delete{$_}) } keys %insert) {
669 if ( $job ) { # progress bar
670 if ( time - $min_sec > $last ) {
671 my $error = $job->update_statustext(
672 int( 100 * $imported / $count )
674 die $error if $error;
679 my $tax_rate = new FS::tax_rate( $insert{$_} );
680 my $error = $tax_rate->insert;
683 $dbh->rollback if $oldAutoCommit;
684 my $hashref = $insert{$_};
685 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
686 return "can't insert tax_rate for $line: $error";
692 for (grep { exists($delete{$_}) } keys %insert) {
693 if ( $job ) { # progress bar
694 if ( time - $min_sec > $last ) {
695 my $error = $job->update_statustext(
696 int( 100 * $imported / $count )
698 die $error if $error;
703 my $old = qsearchs( 'tax_rate', $delete{$_} );
705 $dbh->rollback if $oldAutoCommit;
707 return "can't find tax_rate to replace for: ".
708 #join(" ", map { "$_ => ". $old->{$_} } @fields);
709 join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
711 my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' });
712 $new->taxnum($old->taxnum);
713 my $error = $new->replace($old);
716 $dbh->rollback if $oldAutoCommit;
717 my $hashref = $insert{$_};
718 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
719 return "can't replace tax_rate for $line: $error";
726 for (grep { !exists($insert{$_}) } keys %delete) {
727 if ( $job ) { # progress bar
728 if ( time - $min_sec > $last ) {
729 my $error = $job->update_statustext(
730 int( 100 * $imported / $count )
732 die $error if $error;
737 my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
739 $dbh->rollback if $oldAutoCommit;
740 $tax_rate = $delete{$_};
741 return "can't find tax_rate to delete for: ".
742 #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
743 join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
745 my $error = $tax_rate->delete;
748 $dbh->rollback if $oldAutoCommit;
749 my $hashref = $delete{$_};
750 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
751 return "can't delete tax_rate for $line: $error";
757 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
759 return "Empty file!" unless ($imported || $format eq 'cch-update');
767 Load a batch import as a queued JSRPC job
774 my $param = thaw(decode_base64(shift));
775 my $format = $param->{'format'}; #well... this is all cch specific
777 my $files = $param->{'uploaded_files'}
778 or die "No files provided.";
780 my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
782 if ($format eq 'cch' || $format eq 'cch-fixed') {
784 my $oldAutoCommit = $FS::UID::AutoCommit;
785 local $FS::UID::AutoCommit = 0;
788 my $have_location = 0;
790 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
791 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
792 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
793 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
794 'DETAIL', 'detail', \&FS::tax_rate::batch_import,
796 while( scalar(@list) ) {
797 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
798 unless ($files{$file}) {
799 next if $name eq 'PLUS4';
800 $error = "No $name supplied";
801 $error = "Neither PLUS4 nor ZIP supplied"
802 if ($name eq 'ZIP' && !$have_location);
805 $have_location = 1 if $name eq 'PLUS4';
806 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
807 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
808 my $filename = "$dir/". $files{$file};
809 open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
811 $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
813 unlink $filename or warn "Can't delete $filename: $!";
817 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
820 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
823 }elsif ($format eq 'cch-update' || $format eq 'cch-fixed-update') {
825 my $oldAutoCommit = $FS::UID::AutoCommit;
826 local $FS::UID::AutoCommit = 0;
829 my @insert_list = ();
830 my @delete_list = ();
832 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
833 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
834 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
835 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
837 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
838 while( scalar(@list) ) {
839 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
840 unless ($files{$file}) {
841 my $vendor = $name eq 'ZIP' ? 'cch' : 'cch-zip';
842 next # update expected only for previously installed location data
843 if ( ($name eq 'PLUS4' || $name eq 'ZIP')
844 && !scalar( qsearch( { table => 'cust_tax_location',
845 hashref => { data_vendor => $vendor },
846 select => 'DISTINCT data_vendor',
851 $error = "No $name supplied";
854 my $filename = "$dir/". $files{$file};
855 open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
856 unlink $filename or warn "Can't delete $filename: $!";
858 my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
861 ) or die "can't open temp file: $!\n";
863 my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
866 ) or die "can't open temp file: $!\n";
870 $handle = $ifh if $_ =~ /"I"\s*$/;
871 $handle = $dfh if $_ =~ /"D"\s*$/;
873 $error = "bad input line: $_" unless $handle;
882 push @insert_list, $name, $ifh->filename, $import_sub;
883 unshift @delete_list, $name, $dfh->filename, $import_sub;
886 while( scalar(@insert_list) ) {
887 my ($name, $file, $import_sub) =
888 (shift @insert_list, shift @insert_list, shift @insert_list);
890 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
891 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
893 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
895 unlink $file or warn "Can't delete $file: $!";
898 $error ||= "No DETAIL supplied"
899 unless ($files{detail});
900 open my $fh, "< $dir/". $files{detail}
901 or $error ||= "Can't open DETAIL file: $!";
903 &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
906 unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
909 while( scalar(@delete_list) ) {
910 my ($name, $file, $import_sub) =
911 (shift @delete_list, shift @delete_list, shift @delete_list);
913 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
914 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
916 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
918 unlink $file or warn "Can't delete $file: $!";
922 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
925 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
929 die "Unknown format: $format";
934 =item browse_queries PARAMS
936 Returns a list consisting of a hashref suited for use as the argument
937 to qsearch, and sql query string. Each is based on the PARAMS hashref
938 of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
939 from a form. This conveniently creates the query hashref and count_query
940 string required by the browse and search elements. As a side effect,
941 the PARAMS hashref is untainted and keys with unexpected values are removed.
949 'table' => 'tax_rate',
951 'order_by' => 'ORDER BY geocode, taxclassnum',
956 if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
957 $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
959 delete $params->{data_vendor};
962 if ( $params->{geocode} =~ /^(\w+)$/ ) {
963 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
964 'geocode LIKE '. dbh->quote($1.'%');
966 delete $params->{geocode};
969 if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
970 qsearchs( 'tax_class', {'taxclassnum' => $1} )
973 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
974 ' taxclassnum = '. dbh->quote($1)
976 delete $params->{taxclassnun};
980 if ( $params->{tax_type} =~ /^(\d+)$/ );
981 delete $params->{tax_type}
985 if ( $params->{tax_cat} =~ /^(\d+)$/ );
986 delete $params->{tax_cat}
989 my @taxclassnum = ();
990 if ($tax_type || $tax_cat ) {
991 my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
992 $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
993 @taxclassnum = map { $_->taxclassnum }
994 qsearch({ 'table' => 'tax_class',
996 'extra_sql' => "WHERE taxclass $compare",
1000 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
1001 join(' OR ', map { " taxclassnum = $_ " } @taxclassnum ). ' )'
1002 if ( @taxclassnum );
1004 unless ($params->{'showdisabled'}) {
1005 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
1006 "( disabled = '' OR disabled IS NULL )";
1009 $query->{extra_sql} = $extra_sql;
1011 return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
1018 Mixing automatic and manual editing works poorly at present.
1022 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base