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 TAXABLES, [ OPTIONSHASH ]
347 Returns a listref of a name and an amount of tax calculated for the list
348 of packages/amounts referenced by TAXABLES. If an error occurs, a message
349 is returned as a scalar.
359 if (ref($_[0]) eq 'ARRAY') {
364 #exemptions would be broken in this case
367 my $name = $self->taxname;
368 $name = 'Other surcharges'
369 if ($self->passtype == 2);
372 if ( $self->disabled ) { # we always know how to handle disabled taxes
379 my $taxable_charged = 0;
380 my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; }
383 warn "calculating taxes for ". $self->taxnum. " on ".
384 join (",", map { $_->pkgnum } @cust_bill_pkg)
387 if ($self->passflag eq 'N') {
388 return "fatal: can't (yet) handle taxes not passed to the customer";
391 if ($self->maxtype != 0 && $self->maxtype != 9) {
392 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
396 if ($self->maxtype == 9) {
397 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
398 '" threshold'; # "texas" tax
401 # we treat gross revenue as gross receipts and expect the tax data
402 # to DTRT (i.e. tax on tax rules)
403 if ($self->basetype != 0 && $self->basetype != 1 &&
404 $self->basetype != 6 && $self->basetype != 7 &&
405 $self->basetype != 8 && $self->basetype != 14
407 return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name.
411 unless ($self->setuptax =~ /^Y$/i) {
412 $taxable_charged += $_->setup foreach @cust_bill_pkg;
414 unless ($self->recurtax =~ /^Y$/i) {
415 $taxable_charged += $_->recur foreach @cust_bill_pkg;
418 my $taxable_units = 0;
419 unless ($self->recurtax =~ /^Y$/i) {
420 if ($self->unittype == 0) {
422 foreach (@cust_bill_pkg) {
423 $taxable_units += $_->units
424 unless $seen{$_->pkgnum};
427 }elsif ($self->unittype == 1) {
428 return qq!fatal: can't (yet) handle fee with minute unit type!;
429 }elsif ($self->unittype == 2) {
432 return qq!fatal: can't (yet) handle unknown unit type in tax!.
438 # XXX insert exemption handling here
440 # the tax or fee is applied to taxbase or feebase and then
441 # the excessrate or excess fee is applied to taxmax or feemax
444 $amount += $taxable_charged * $self->tax;
445 $amount += $taxable_units * $self->fee;
447 warn "calculated taxes as [ $name, $amount ]\n"
457 =item tax_on_tax CUST_MAIN
459 Returns a list of taxes which are candidates for taxing taxes for the
460 given customer (see L<FS::cust_main>)
466 my $cust_main = shift;
468 warn "looking up taxes on tax ". $self->taxnum. " for customer ".
472 my $geocode = $cust_main->geocode($self->data_vendor);
476 my $extra_sql = ' AND ('.
477 join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
482 my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
483 my $select = 'DISTINCT ON(taxclassnum) *';
485 # should qsearch preface columns with the table to facilitate joins?
486 my @taxclassnums = map { $_->taxclassnum }
487 qsearch( { 'table' => 'part_pkg_taxrate',
489 'hashref' => { 'data_vendor' => $self->data_vendor,
490 'taxclassnumtaxed' => $self->taxclassnum,
492 'extra_sql' => $extra_sql,
493 'order_by' => $order_by,
496 return () unless @taxclassnums;
499 "AND (". join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
501 qsearch({ 'table' => 'tax_rate',
502 'hashref' => { 'geocode' => $geocode, },
503 'extra_sql' => $extra_sql,
519 my ($param, $job) = @_;
521 my $fh = $param->{filehandle};
522 my $format = $param->{'format'};
530 my @column_lengths = ();
531 my @column_callbacks = ();
532 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
533 $format =~ s/-fixed//;
534 my $date_format = sub { my $r='';
535 /^(\d{4})(\d{2})(\d{2})$/ && ($r="$1/$2/$3");
538 $column_callbacks[8] = $date_format;
539 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 );
540 push @column_lengths, 1 if $format eq 'cch-update';
544 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
545 if ( $job || scalar(@column_callbacks) ) {
547 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
548 return $error if $error;
552 if ( $format eq 'cch' || $format eq 'cch-update' ) {
553 @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
554 excessrate effective_date taxauth taxtype taxcat taxname
555 usetax useexcessrate fee unittype feemax maxtype passflag
557 push @fields, 'actionflag' if $format eq 'cch-update';
562 $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
563 $hash->{'data_vendor'} ='cch';
564 $hash->{'effective_date'} = str2time($hash->{'effective_date'});
567 join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
569 my %tax_class = ( 'data_vendor' => 'cch',
570 'taxclass' => $taxclassid,
573 my $tax_class = qsearchs( 'tax_class', \%tax_class );
574 return "Error updating tax rate: no tax class $taxclassid"
577 $hash->{'taxclassnum'} = $tax_class->taxclassnum;
579 foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
583 my %passflagmap = ( '0' => '',
587 $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
588 if exists $passflagmap{$hash->{'passflag'}};
590 foreach (keys %$hash) {
591 $hash->{$_} = substr($hash->{$_}, 0, 80)
592 if length($hash->{$_}) > 80;
595 my $actionflag = delete($hash->{'actionflag'});
597 $hash->{'taxname'} =~ s/`/'/g;
598 $hash->{'taxname'} =~ s|\\|/|g;
600 return '' if $format eq 'cch'; # but not cch-update
602 if ($actionflag eq 'I') {
603 $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
604 }elsif ($actionflag eq 'D') {
605 $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
607 return "Unexpected action flag: ". $hash->{'actionflag'};
610 delete($hash->{$_}) for keys %$hash;
616 } elsif ( $format eq 'extended' ) {
617 die "unimplemented\n";
621 die "unknown format $format";
624 eval "use Text::CSV_XS;";
627 my $csv = new Text::CSV_XS;
631 local $SIG{HUP} = 'IGNORE';
632 local $SIG{INT} = 'IGNORE';
633 local $SIG{QUIT} = 'IGNORE';
634 local $SIG{TERM} = 'IGNORE';
635 local $SIG{TSTP} = 'IGNORE';
636 local $SIG{PIPE} = 'IGNORE';
638 my $oldAutoCommit = $FS::UID::AutoCommit;
639 local $FS::UID::AutoCommit = 0;
642 while ( defined($line=<$fh>) ) {
643 $csv->parse($line) or do {
644 $dbh->rollback if $oldAutoCommit;
645 return "can't parse: ". $csv->error_input();
648 if ( $job ) { # progress bar
649 if ( time - $min_sec > $last ) {
650 my $error = $job->update_statustext(
651 int( 100 * $imported / $count )
653 die $error if $error;
658 my @columns = $csv->fields();
660 my %tax_rate = ( 'data_vendor' => $format );
661 foreach my $field ( @fields ) {
662 $tax_rate{$field} = shift @columns;
664 if ( scalar( @columns ) ) {
665 $dbh->rollback if $oldAutoCommit;
666 return "Unexpected trailing columns in line (wrong format?): $line";
669 my $error = &{$hook}(\%tax_rate);
671 $dbh->rollback if $oldAutoCommit;
675 if (scalar(keys %tax_rate)) { #inserts only, not updates for cch
677 my $tax_rate = new FS::tax_rate( \%tax_rate );
678 $error = $tax_rate->insert;
681 $dbh->rollback if $oldAutoCommit;
682 return "can't insert tax_rate for $line: $error";
691 for (grep { !exists($delete{$_}) } keys %insert) {
692 if ( $job ) { # progress bar
693 if ( time - $min_sec > $last ) {
694 my $error = $job->update_statustext(
695 int( 100 * $imported / $count )
697 die $error if $error;
702 my $tax_rate = new FS::tax_rate( $insert{$_} );
703 my $error = $tax_rate->insert;
706 $dbh->rollback if $oldAutoCommit;
707 my $hashref = $insert{$_};
708 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
709 return "can't insert tax_rate for $line: $error";
715 for (grep { exists($delete{$_}) } keys %insert) {
716 if ( $job ) { # progress bar
717 if ( time - $min_sec > $last ) {
718 my $error = $job->update_statustext(
719 int( 100 * $imported / $count )
721 die $error if $error;
726 my $old = qsearchs( 'tax_rate', $delete{$_} );
728 $dbh->rollback if $oldAutoCommit;
730 return "can't find tax_rate to replace for: ".
731 #join(" ", map { "$_ => ". $old->{$_} } @fields);
732 join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
734 my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' });
735 $new->taxnum($old->taxnum);
736 my $error = $new->replace($old);
739 $dbh->rollback if $oldAutoCommit;
740 my $hashref = $insert{$_};
741 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
742 return "can't replace tax_rate for $line: $error";
749 for (grep { !exists($insert{$_}) } keys %delete) {
750 if ( $job ) { # progress bar
751 if ( time - $min_sec > $last ) {
752 my $error = $job->update_statustext(
753 int( 100 * $imported / $count )
755 die $error if $error;
760 my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
762 $dbh->rollback if $oldAutoCommit;
763 $tax_rate = $delete{$_};
764 return "can't find tax_rate to delete for: ".
765 #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
766 join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
768 my $error = $tax_rate->delete;
771 $dbh->rollback if $oldAutoCommit;
772 my $hashref = $delete{$_};
773 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
774 return "can't delete tax_rate for $line: $error";
780 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
782 return "Empty file!" unless ($imported || $format eq 'cch-update');
788 =item process_batch_import
790 Load a batch import as a queued JSRPC job
794 sub process_batch_import {
797 my $param = thaw(decode_base64(shift));
798 my $format = $param->{'format'}; #well... this is all cch specific
800 my $files = $param->{'uploaded_files'}
801 or die "No files provided.";
803 my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
805 if ($format eq 'cch' || $format eq 'cch-fixed') {
807 my $oldAutoCommit = $FS::UID::AutoCommit;
808 local $FS::UID::AutoCommit = 0;
811 my $have_location = 0;
813 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
814 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
815 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
816 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
817 'DETAIL', 'detail', \&FS::tax_rate::batch_import,
819 while( scalar(@list) ) {
820 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
821 unless ($files{$file}) {
822 next if $name eq 'PLUS4';
823 $error = "No $name supplied";
824 $error = "Neither PLUS4 nor ZIP supplied"
825 if ($name eq 'ZIP' && !$have_location);
828 $have_location = 1 if $name eq 'PLUS4';
829 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
830 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
831 my $filename = "$dir/". $files{$file};
832 open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
834 $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
836 unlink $filename or warn "Can't delete $filename: $!";
840 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
843 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
846 }elsif ($format eq 'cch-update' || $format eq 'cch-fixed-update') {
848 my $oldAutoCommit = $FS::UID::AutoCommit;
849 local $FS::UID::AutoCommit = 0;
852 my @insert_list = ();
853 my @delete_list = ();
855 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
856 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
857 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
858 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
860 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
861 while( scalar(@list) ) {
862 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
863 unless ($files{$file}) {
864 my $vendor = $name eq 'ZIP' ? 'cch' : 'cch-zip';
865 next # update expected only for previously installed location data
866 if ( ($name eq 'PLUS4' || $name eq 'ZIP')
867 && !scalar( qsearch( { table => 'cust_tax_location',
868 hashref => { data_vendor => $vendor },
869 select => 'DISTINCT data_vendor',
874 $error = "No $name supplied";
877 my $filename = "$dir/". $files{$file};
878 open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
879 unlink $filename or warn "Can't delete $filename: $!";
881 my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
884 ) or die "can't open temp file: $!\n";
886 my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
889 ) or die "can't open temp file: $!\n";
893 $handle = $ifh if $_ =~ /"I"\s*$/;
894 $handle = $dfh if $_ =~ /"D"\s*$/;
896 $error = "bad input line: $_" unless $handle;
905 push @insert_list, $name, $ifh->filename, $import_sub;
906 unshift @delete_list, $name, $dfh->filename, $import_sub;
909 while( scalar(@insert_list) ) {
910 my ($name, $file, $import_sub) =
911 (shift @insert_list, shift @insert_list, shift @insert_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: $!";
921 $error ||= "No DETAIL supplied"
922 unless ($files{detail});
923 open my $fh, "< $dir/". $files{detail}
924 or $error ||= "Can't open DETAIL file: $!";
926 &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
929 unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
932 while( scalar(@delete_list) ) {
933 my ($name, $file, $import_sub) =
934 (shift @delete_list, shift @delete_list, shift @delete_list);
936 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
937 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
939 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
941 unlink $file or warn "Can't delete $file: $!";
945 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
948 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
952 die "Unknown format: $format";
957 =item browse_queries PARAMS
959 Returns a list consisting of a hashref suited for use as the argument
960 to qsearch, and sql query string. Each is based on the PARAMS hashref
961 of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
962 from a form. This conveniently creates the query hashref and count_query
963 string required by the browse and search elements. As a side effect,
964 the PARAMS hashref is untainted and keys with unexpected values are removed.
972 'table' => 'tax_rate',
974 'order_by' => 'ORDER BY geocode, taxclassnum',
979 if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
980 $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
982 delete $params->{data_vendor};
985 if ( $params->{geocode} =~ /^(\w+)$/ ) {
986 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
987 'geocode LIKE '. dbh->quote($1.'%');
989 delete $params->{geocode};
992 if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
993 qsearchs( 'tax_class', {'taxclassnum' => $1} )
996 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
997 ' taxclassnum = '. dbh->quote($1)
999 delete $params->{taxclassnun};
1003 if ( $params->{tax_type} =~ /^(\d+)$/ );
1004 delete $params->{tax_type}
1008 if ( $params->{tax_cat} =~ /^(\d+)$/ );
1009 delete $params->{tax_cat}
1012 my @taxclassnum = ();
1013 if ($tax_type || $tax_cat ) {
1014 my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
1015 $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
1016 @taxclassnum = map { $_->taxclassnum }
1017 qsearch({ 'table' => 'tax_class',
1019 'extra_sql' => "WHERE taxclass $compare",
1023 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
1024 join(' OR ', map { " taxclassnum = $_ " } @taxclassnum ). ' )'
1025 if ( @taxclassnum );
1027 unless ($params->{'showdisabled'}) {
1028 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
1029 "( disabled = '' OR disabled IS NULL )";
1032 $query->{extra_sql} = $extra_sql;
1034 return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
1041 Mixing automatic and manual editing works poorly at present.
1045 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base