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',
297 '15' => 'prior year gross receipts',
302 $tax_basetypes{$self->basetype};
307 Returns the human understandable value associated with the taxauth column
311 %tax_authorities = ( '0' => 'federal',
316 '5' => 'county administered by state',
317 '6' => 'city administered by state',
318 '7' => 'city administered by county',
319 '8' => 'local administered by state',
320 '9' => 'local administered by county',
325 $tax_authorities{$self->taxauth};
330 Returns the human understandable value associated with the passtype column
334 %tax_passtypes = ( '0' => 'separate tax line',
335 '1' => 'separate surcharge line',
336 '2' => 'surcharge not separated',
337 '3' => 'included in base rate',
342 $tax_passtypes{$self->passtype};
345 =item taxline CUST_BILL_PKG|AMOUNT, ...
347 Returns a listref of a name and an amount of tax calculated for the list
348 of packages/amounts. If an error occurs, a message is returned as a scalar.
355 my $name = $self->taxname;
356 $name = 'Other surcharges'
357 if ($self->passtype == 2);
360 return [$name, $amount] # we always know how to handle disabled taxes
363 my $taxable_charged = 0;
364 my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; } @_;
366 warn "calculating taxes for ". $self->taxnum. " on ".
367 join (",", map { $_->pkgnum } @cust_bill_pkg)
370 if ($self->passflag eq 'N') {
371 return "fatal: can't (yet) handle taxes not passed to the customer";
374 if ($self->maxtype != 0 && $self->maxtype != 9) {
375 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
379 if ($self->maxtype == 9) {
380 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
381 '" threshold'; # "texas" tax
384 # we treat gross revenue as gross receipts and expect the tax data
385 # to DTRT (i.e. tax on tax rules)
386 if ($self->basetype != 0 && $self->basetype != 1 &&
387 $self->basetype != 6 && $self->basetype != 7 &&
388 $self->basetype != 8 && $self->basetype != 14
390 return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name.
394 unless ($self->setuptax =~ /^Y$/i) {
395 $taxable_charged += $_->setup foreach @cust_bill_pkg;
397 unless ($self->recurtax =~ /^Y$/i) {
398 $taxable_charged += $_->recur foreach @cust_bill_pkg;
401 my $taxable_units = 0;
402 unless ($self->recurtax =~ /^Y$/i) {
403 if ($self->unittype == 0) {
405 foreach (@cust_bill_pkg) {
406 $taxable_units += $_->units
407 unless $seen{$_->pkgnum};
410 }elsif ($self->unittype == 1) {
411 return qq!fatal: can't (yet) handle fee with minute unit type!;
412 }elsif ($self->unittype == 2) {
415 return qq!fatal: can't (yet) handle unknown unit type in tax!.
421 # XXX insert exemption handling here
423 # the tax or fee is applied to taxbase or feebase and then
424 # the excessrate or excess fee is applied to taxmax or feemax
427 $amount += $taxable_charged * $self->tax;
428 $amount += $taxable_units * $self->fee;
430 warn "calculated taxes as [ $name, $amount ]\n"
433 return [$name, $amount];
437 =item tax_on_tax CUST_MAIN
439 Returns a list of taxes which are candidates for taxing taxes for the
440 given customer (see L<FS::cust_main>)
446 my $cust_main = shift;
448 warn "looking up taxes on tax ". $self->taxnum. " for customer ".
452 my $geocode = $cust_main->geocode($self->data_vendor);
456 my $extra_sql = ' AND ('.
457 join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
462 my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
463 my $select = 'DISTINCT ON(taxclassnum) *';
465 # should qsearch preface columns with the table to facilitate joins?
466 my @taxclassnums = map { $_->taxclassnum }
467 qsearch( { 'table' => 'part_pkg_taxrate',
469 'hashref' => { 'data_vendor' => $self->data_vendor,
470 'taxclassnumtaxed' => $self->taxclassnum,
472 'extra_sql' => $extra_sql,
473 'order_by' => $order_by,
476 return () unless @taxclassnums;
479 "AND (". join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
481 qsearch({ 'table' => 'tax_rate',
482 'hashref' => { 'geocode' => $geocode, },
483 'extra_sql' => $extra_sql,
499 my ($param, $job) = @_;
501 my $fh = $param->{filehandle};
502 my $format = $param->{'format'};
510 my @column_lengths = ();
511 my @column_callbacks = ();
512 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
513 $format =~ s/-fixed//;
514 my $date_format = sub { my $r='';
515 /^(\d{4})(\d{2})(\d{2})$/ && ($r="$1/$2/$3");
518 $column_callbacks[8] = $date_format;
519 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 );
520 push @column_lengths, 1 if $format eq 'cch-update';
524 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
525 if ( $job || scalar(@column_callbacks) ) {
527 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
528 return $error if $error;
532 if ( $format eq 'cch' || $format eq 'cch-update' ) {
533 @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
534 excessrate effective_date taxauth taxtype taxcat taxname
535 usetax useexcessrate fee unittype feemax maxtype passflag
537 push @fields, 'actionflag' if $format eq 'cch-update';
542 $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
543 $hash->{'data_vendor'} ='cch';
544 $hash->{'effective_date'} = str2time($hash->{'effective_date'});
547 join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
549 my %tax_class = ( 'data_vendor' => 'cch',
550 'taxclass' => $taxclassid,
553 my $tax_class = qsearchs( 'tax_class', \%tax_class );
554 return "Error updating tax rate: no tax class $taxclassid"
557 $hash->{'taxclassnum'} = $tax_class->taxclassnum;
559 foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
563 my %passflagmap = ( '0' => '',
567 $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
568 if exists $passflagmap{$hash->{'passflag'}};
570 foreach (keys %$hash) {
571 $hash->{$_} = substr($hash->{$_}, 0, 80)
572 if length($hash->{$_}) > 80;
575 my $actionflag = delete($hash->{'actionflag'});
577 $hash->{'taxname'} =~ s/`/'/g;
578 $hash->{'taxname'} =~ s|\\|/|g;
580 return '' if $format eq 'cch'; # but not cch-update
582 if ($actionflag eq 'I') {
583 $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
584 }elsif ($actionflag eq 'D') {
585 $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
587 return "Unexpected action flag: ". $hash->{'actionflag'};
590 delete($hash->{$_}) for keys %$hash;
596 } elsif ( $format eq 'extended' ) {
597 die "unimplemented\n";
601 die "unknown format $format";
604 eval "use Text::CSV_XS;";
607 my $csv = new Text::CSV_XS;
611 local $SIG{HUP} = 'IGNORE';
612 local $SIG{INT} = 'IGNORE';
613 local $SIG{QUIT} = 'IGNORE';
614 local $SIG{TERM} = 'IGNORE';
615 local $SIG{TSTP} = 'IGNORE';
616 local $SIG{PIPE} = 'IGNORE';
618 my $oldAutoCommit = $FS::UID::AutoCommit;
619 local $FS::UID::AutoCommit = 0;
622 while ( defined($line=<$fh>) ) {
623 $csv->parse($line) or do {
624 $dbh->rollback if $oldAutoCommit;
625 return "can't parse: ". $csv->error_input();
628 if ( $job ) { # progress bar
629 if ( time - $min_sec > $last ) {
630 my $error = $job->update_statustext(
631 int( 100 * $imported / $count )
633 die $error if $error;
638 my @columns = $csv->fields();
640 my %tax_rate = ( 'data_vendor' => $format );
641 foreach my $field ( @fields ) {
642 $tax_rate{$field} = shift @columns;
644 if ( scalar( @columns ) ) {
645 $dbh->rollback if $oldAutoCommit;
646 return "Unexpected trailing columns in line (wrong format?): $line";
649 my $error = &{$hook}(\%tax_rate);
651 $dbh->rollback if $oldAutoCommit;
655 if (scalar(keys %tax_rate)) { #inserts only, not updates for cch
657 my $tax_rate = new FS::tax_rate( \%tax_rate );
658 $error = $tax_rate->insert;
661 $dbh->rollback if $oldAutoCommit;
662 return "can't insert tax_rate for $line: $error";
671 for (grep { !exists($delete{$_}) } keys %insert) {
672 if ( $job ) { # progress bar
673 if ( time - $min_sec > $last ) {
674 my $error = $job->update_statustext(
675 int( 100 * $imported / $count )
677 die $error if $error;
682 my $tax_rate = new FS::tax_rate( $insert{$_} );
683 my $error = $tax_rate->insert;
686 $dbh->rollback if $oldAutoCommit;
687 my $hashref = $insert{$_};
688 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
689 return "can't insert tax_rate for $line: $error";
695 for (grep { exists($delete{$_}) } keys %insert) {
696 if ( $job ) { # progress bar
697 if ( time - $min_sec > $last ) {
698 my $error = $job->update_statustext(
699 int( 100 * $imported / $count )
701 die $error if $error;
706 my $old = qsearchs( 'tax_rate', $delete{$_} );
708 $dbh->rollback if $oldAutoCommit;
710 return "can't find tax_rate to replace for: ".
711 #join(" ", map { "$_ => ". $old->{$_} } @fields);
712 join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
714 my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' });
715 $new->taxnum($old->taxnum);
716 my $error = $new->replace($old);
719 $dbh->rollback if $oldAutoCommit;
720 my $hashref = $insert{$_};
721 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
722 return "can't replace tax_rate for $line: $error";
729 for (grep { !exists($insert{$_}) } keys %delete) {
730 if ( $job ) { # progress bar
731 if ( time - $min_sec > $last ) {
732 my $error = $job->update_statustext(
733 int( 100 * $imported / $count )
735 die $error if $error;
740 my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
742 $dbh->rollback if $oldAutoCommit;
743 $tax_rate = $delete{$_};
744 return "can't find tax_rate to delete for: ".
745 #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
746 join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
748 my $error = $tax_rate->delete;
751 $dbh->rollback if $oldAutoCommit;
752 my $hashref = $delete{$_};
753 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
754 return "can't delete tax_rate for $line: $error";
760 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
762 return "Empty file!" unless ($imported || $format eq 'cch-update');
770 Load a batch import as a queued JSRPC job
777 my $param = thaw(decode_base64(shift));
778 my $format = $param->{'format'}; #well... this is all cch specific
780 my $files = $param->{'uploaded_files'}
781 or die "No files provided.";
783 my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
785 if ($format eq 'cch' || $format eq 'cch-fixed') {
787 my $oldAutoCommit = $FS::UID::AutoCommit;
788 local $FS::UID::AutoCommit = 0;
791 my $have_location = 0;
793 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
794 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
795 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
796 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
797 'DETAIL', 'detail', \&FS::tax_rate::batch_import,
799 while( scalar(@list) ) {
800 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
801 unless ($files{$file}) {
802 next if $name eq 'PLUS4';
803 $error = "No $name supplied";
804 $error = "Neither PLUS4 nor ZIP supplied"
805 if ($name eq 'ZIP' && !$have_location);
808 $have_location = 1 if $name eq 'PLUS4';
809 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
810 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
811 my $filename = "$dir/". $files{$file};
812 open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
814 $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
816 unlink $filename or warn "Can't delete $filename: $!";
820 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
823 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
826 }elsif ($format eq 'cch-update' || $format eq 'cch-fixed-update') {
828 my $oldAutoCommit = $FS::UID::AutoCommit;
829 local $FS::UID::AutoCommit = 0;
832 my @insert_list = ();
833 my @delete_list = ();
835 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
836 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
837 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
838 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
840 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
841 while( scalar(@list) ) {
842 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
843 unless ($files{$file}) {
844 my $vendor = $name eq 'ZIP' ? 'cch' : 'cch-zip';
845 next # update expected only for previously installed location data
846 if ( ($name eq 'PLUS4' || $name eq 'ZIP')
847 && !scalar( qsearch( { table => 'cust_tax_location',
848 hashref => { data_vendor => $vendor },
849 select => 'DISTINCT data_vendor',
854 $error = "No $name supplied";
857 my $filename = "$dir/". $files{$file};
858 open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
859 unlink $filename or warn "Can't delete $filename: $!";
861 my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
864 ) or die "can't open temp file: $!\n";
866 my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
869 ) or die "can't open temp file: $!\n";
873 $handle = $ifh if $_ =~ /"I"\s*$/;
874 $handle = $dfh if $_ =~ /"D"\s*$/;
876 $error = "bad input line: $_" unless $handle;
885 push @insert_list, $name, $ifh->filename, $import_sub;
886 unshift @delete_list, $name, $dfh->filename, $import_sub;
889 while( scalar(@insert_list) ) {
890 my ($name, $file, $import_sub) =
891 (shift @insert_list, shift @insert_list, shift @insert_list);
893 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
894 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
896 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
898 unlink $file or warn "Can't delete $file: $!";
901 $error ||= "No DETAIL supplied"
902 unless ($files{detail});
903 open my $fh, "< $dir/". $files{detail}
904 or $error ||= "Can't open DETAIL file: $!";
906 &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
909 unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
912 while( scalar(@delete_list) ) {
913 my ($name, $file, $import_sub) =
914 (shift @delete_list, shift @delete_list, shift @delete_list);
916 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
917 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
919 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
921 unlink $file or warn "Can't delete $file: $!";
925 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
928 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
932 die "Unknown format: $format";
937 =item browse_queries PARAMS
939 Returns a list consisting of a hashref suited for use as the argument
940 to qsearch, and sql query string. Each is based on the PARAMS hashref
941 of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
942 from a form. This conveniently creates the query hashref and count_query
943 string required by the browse and search elements. As a side effect,
944 the PARAMS hashref is untainted and keys with unexpected values are removed.
952 'table' => 'tax_rate',
954 'order_by' => 'ORDER BY geocode, taxclassnum',
959 if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
960 $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
962 delete $params->{data_vendor};
965 if ( $params->{geocode} =~ /^(\w+)$/ ) {
966 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
967 'geocode LIKE '. dbh->quote($1.'%');
969 delete $params->{geocode};
972 if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
973 qsearchs( 'tax_class', {'taxclassnum' => $1} )
976 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
977 ' taxclassnum = '. dbh->quote($1)
979 delete $params->{taxclassnun};
983 if ( $params->{tax_type} =~ /^(\d+)$/ );
984 delete $params->{tax_type}
988 if ( $params->{tax_cat} =~ /^(\d+)$/ );
989 delete $params->{tax_cat}
992 my @taxclassnum = ();
993 if ($tax_type || $tax_cat ) {
994 my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
995 $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
996 @taxclassnum = map { $_->taxclassnum }
997 qsearch({ 'table' => 'tax_class',
999 'extra_sql' => "WHERE taxclass $compare",
1003 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
1004 join(' OR ', map { " taxclassnum = $_ " } @taxclassnum ). ' )'
1005 if ( @taxclassnum );
1007 unless ($params->{'showdisabled'}) {
1008 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
1009 "( disabled = '' OR disabled IS NULL )";
1012 $query->{extra_sql} = $extra_sql;
1014 return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
1021 Mixing automatic and manual editing works poorly at present.
1025 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base