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 return [$name, $amount] # we always know how to handle disabled taxes
375 my $taxable_charged = 0;
376 my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; }
379 warn "calculating taxes for ". $self->taxnum. " on ".
380 join (",", map { $_->pkgnum } @cust_bill_pkg)
383 if ($self->passflag eq 'N') {
384 return "fatal: can't (yet) handle taxes not passed to the customer";
387 if ($self->maxtype != 0 && $self->maxtype != 9) {
388 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
392 if ($self->maxtype == 9) {
393 return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
394 '" threshold'; # "texas" tax
397 # we treat gross revenue as gross receipts and expect the tax data
398 # to DTRT (i.e. tax on tax rules)
399 if ($self->basetype != 0 && $self->basetype != 1 &&
400 $self->basetype != 6 && $self->basetype != 7 &&
401 $self->basetype != 8 && $self->basetype != 14
403 return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name.
407 unless ($self->setuptax =~ /^Y$/i) {
408 $taxable_charged += $_->setup foreach @cust_bill_pkg;
410 unless ($self->recurtax =~ /^Y$/i) {
411 $taxable_charged += $_->recur foreach @cust_bill_pkg;
414 my $taxable_units = 0;
415 unless ($self->recurtax =~ /^Y$/i) {
416 if ($self->unittype == 0) {
418 foreach (@cust_bill_pkg) {
419 $taxable_units += $_->units
420 unless $seen{$_->pkgnum};
423 }elsif ($self->unittype == 1) {
424 return qq!fatal: can't (yet) handle fee with minute unit type!;
425 }elsif ($self->unittype == 2) {
428 return qq!fatal: can't (yet) handle unknown unit type in tax!.
434 # XXX insert exemption handling here
436 # the tax or fee is applied to taxbase or feebase and then
437 # the excessrate or excess fee is applied to taxmax or feemax
440 $amount += $taxable_charged * $self->tax;
441 $amount += $taxable_units * $self->fee;
443 warn "calculated taxes as [ $name, $amount ]\n"
446 return [$name, $amount];
450 =item tax_on_tax CUST_MAIN
452 Returns a list of taxes which are candidates for taxing taxes for the
453 given customer (see L<FS::cust_main>)
459 my $cust_main = shift;
461 warn "looking up taxes on tax ". $self->taxnum. " for customer ".
465 my $geocode = $cust_main->geocode($self->data_vendor);
469 my $extra_sql = ' AND ('.
470 join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
475 my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
476 my $select = 'DISTINCT ON(taxclassnum) *';
478 # should qsearch preface columns with the table to facilitate joins?
479 my @taxclassnums = map { $_->taxclassnum }
480 qsearch( { 'table' => 'part_pkg_taxrate',
482 'hashref' => { 'data_vendor' => $self->data_vendor,
483 'taxclassnumtaxed' => $self->taxclassnum,
485 'extra_sql' => $extra_sql,
486 'order_by' => $order_by,
489 return () unless @taxclassnums;
492 "AND (". join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
494 qsearch({ 'table' => 'tax_rate',
495 'hashref' => { 'geocode' => $geocode, },
496 'extra_sql' => $extra_sql,
512 my ($param, $job) = @_;
514 my $fh = $param->{filehandle};
515 my $format = $param->{'format'};
523 my @column_lengths = ();
524 my @column_callbacks = ();
525 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
526 $format =~ s/-fixed//;
527 my $date_format = sub { my $r='';
528 /^(\d{4})(\d{2})(\d{2})$/ && ($r="$1/$2/$3");
531 $column_callbacks[8] = $date_format;
532 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 );
533 push @column_lengths, 1 if $format eq 'cch-update';
537 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
538 if ( $job || scalar(@column_callbacks) ) {
540 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
541 return $error if $error;
545 if ( $format eq 'cch' || $format eq 'cch-update' ) {
546 @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
547 excessrate effective_date taxauth taxtype taxcat taxname
548 usetax useexcessrate fee unittype feemax maxtype passflag
550 push @fields, 'actionflag' if $format eq 'cch-update';
555 $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
556 $hash->{'data_vendor'} ='cch';
557 $hash->{'effective_date'} = str2time($hash->{'effective_date'});
560 join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
562 my %tax_class = ( 'data_vendor' => 'cch',
563 'taxclass' => $taxclassid,
566 my $tax_class = qsearchs( 'tax_class', \%tax_class );
567 return "Error updating tax rate: no tax class $taxclassid"
570 $hash->{'taxclassnum'} = $tax_class->taxclassnum;
572 foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
576 my %passflagmap = ( '0' => '',
580 $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
581 if exists $passflagmap{$hash->{'passflag'}};
583 foreach (keys %$hash) {
584 $hash->{$_} = substr($hash->{$_}, 0, 80)
585 if length($hash->{$_}) > 80;
588 my $actionflag = delete($hash->{'actionflag'});
590 $hash->{'taxname'} =~ s/`/'/g;
591 $hash->{'taxname'} =~ s|\\|/|g;
593 return '' if $format eq 'cch'; # but not cch-update
595 if ($actionflag eq 'I') {
596 $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
597 }elsif ($actionflag eq 'D') {
598 $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
600 return "Unexpected action flag: ". $hash->{'actionflag'};
603 delete($hash->{$_}) for keys %$hash;
609 } elsif ( $format eq 'extended' ) {
610 die "unimplemented\n";
614 die "unknown format $format";
617 eval "use Text::CSV_XS;";
620 my $csv = new Text::CSV_XS;
624 local $SIG{HUP} = 'IGNORE';
625 local $SIG{INT} = 'IGNORE';
626 local $SIG{QUIT} = 'IGNORE';
627 local $SIG{TERM} = 'IGNORE';
628 local $SIG{TSTP} = 'IGNORE';
629 local $SIG{PIPE} = 'IGNORE';
631 my $oldAutoCommit = $FS::UID::AutoCommit;
632 local $FS::UID::AutoCommit = 0;
635 while ( defined($line=<$fh>) ) {
636 $csv->parse($line) or do {
637 $dbh->rollback if $oldAutoCommit;
638 return "can't parse: ". $csv->error_input();
641 if ( $job ) { # progress bar
642 if ( time - $min_sec > $last ) {
643 my $error = $job->update_statustext(
644 int( 100 * $imported / $count )
646 die $error if $error;
651 my @columns = $csv->fields();
653 my %tax_rate = ( 'data_vendor' => $format );
654 foreach my $field ( @fields ) {
655 $tax_rate{$field} = shift @columns;
657 if ( scalar( @columns ) ) {
658 $dbh->rollback if $oldAutoCommit;
659 return "Unexpected trailing columns in line (wrong format?): $line";
662 my $error = &{$hook}(\%tax_rate);
664 $dbh->rollback if $oldAutoCommit;
668 if (scalar(keys %tax_rate)) { #inserts only, not updates for cch
670 my $tax_rate = new FS::tax_rate( \%tax_rate );
671 $error = $tax_rate->insert;
674 $dbh->rollback if $oldAutoCommit;
675 return "can't insert tax_rate for $line: $error";
684 for (grep { !exists($delete{$_}) } keys %insert) {
685 if ( $job ) { # progress bar
686 if ( time - $min_sec > $last ) {
687 my $error = $job->update_statustext(
688 int( 100 * $imported / $count )
690 die $error if $error;
695 my $tax_rate = new FS::tax_rate( $insert{$_} );
696 my $error = $tax_rate->insert;
699 $dbh->rollback if $oldAutoCommit;
700 my $hashref = $insert{$_};
701 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
702 return "can't insert tax_rate for $line: $error";
708 for (grep { exists($delete{$_}) } keys %insert) {
709 if ( $job ) { # progress bar
710 if ( time - $min_sec > $last ) {
711 my $error = $job->update_statustext(
712 int( 100 * $imported / $count )
714 die $error if $error;
719 my $old = qsearchs( 'tax_rate', $delete{$_} );
721 $dbh->rollback if $oldAutoCommit;
723 return "can't find tax_rate to replace for: ".
724 #join(" ", map { "$_ => ". $old->{$_} } @fields);
725 join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
727 my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' });
728 $new->taxnum($old->taxnum);
729 my $error = $new->replace($old);
732 $dbh->rollback if $oldAutoCommit;
733 my $hashref = $insert{$_};
734 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
735 return "can't replace tax_rate for $line: $error";
742 for (grep { !exists($insert{$_}) } keys %delete) {
743 if ( $job ) { # progress bar
744 if ( time - $min_sec > $last ) {
745 my $error = $job->update_statustext(
746 int( 100 * $imported / $count )
748 die $error if $error;
753 my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
755 $dbh->rollback if $oldAutoCommit;
756 $tax_rate = $delete{$_};
757 return "can't find tax_rate to delete for: ".
758 #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
759 join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
761 my $error = $tax_rate->delete;
764 $dbh->rollback if $oldAutoCommit;
765 my $hashref = $delete{$_};
766 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
767 return "can't delete tax_rate for $line: $error";
773 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
775 return "Empty file!" unless ($imported || $format eq 'cch-update');
783 Load a batch import as a queued JSRPC job
790 my $param = thaw(decode_base64(shift));
791 my $format = $param->{'format'}; #well... this is all cch specific
793 my $files = $param->{'uploaded_files'}
794 or die "No files provided.";
796 my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
798 if ($format eq 'cch' || $format eq 'cch-fixed') {
800 my $oldAutoCommit = $FS::UID::AutoCommit;
801 local $FS::UID::AutoCommit = 0;
804 my $have_location = 0;
806 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
807 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
808 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
809 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
810 'DETAIL', 'detail', \&FS::tax_rate::batch_import,
812 while( scalar(@list) ) {
813 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
814 unless ($files{$file}) {
815 next if $name eq 'PLUS4';
816 $error = "No $name supplied";
817 $error = "Neither PLUS4 nor ZIP supplied"
818 if ($name eq 'ZIP' && !$have_location);
821 $have_location = 1 if $name eq 'PLUS4';
822 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
823 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
824 my $filename = "$dir/". $files{$file};
825 open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
827 $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
829 unlink $filename or warn "Can't delete $filename: $!";
833 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
836 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
839 }elsif ($format eq 'cch-update' || $format eq 'cch-fixed-update') {
841 my $oldAutoCommit = $FS::UID::AutoCommit;
842 local $FS::UID::AutoCommit = 0;
845 my @insert_list = ();
846 my @delete_list = ();
848 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
849 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
850 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
851 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
853 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
854 while( scalar(@list) ) {
855 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
856 unless ($files{$file}) {
857 my $vendor = $name eq 'ZIP' ? 'cch' : 'cch-zip';
858 next # update expected only for previously installed location data
859 if ( ($name eq 'PLUS4' || $name eq 'ZIP')
860 && !scalar( qsearch( { table => 'cust_tax_location',
861 hashref => { data_vendor => $vendor },
862 select => 'DISTINCT data_vendor',
867 $error = "No $name supplied";
870 my $filename = "$dir/". $files{$file};
871 open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
872 unlink $filename or warn "Can't delete $filename: $!";
874 my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
877 ) or die "can't open temp file: $!\n";
879 my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
882 ) or die "can't open temp file: $!\n";
886 $handle = $ifh if $_ =~ /"I"\s*$/;
887 $handle = $dfh if $_ =~ /"D"\s*$/;
889 $error = "bad input line: $_" unless $handle;
898 push @insert_list, $name, $ifh->filename, $import_sub;
899 unshift @delete_list, $name, $dfh->filename, $import_sub;
902 while( scalar(@insert_list) ) {
903 my ($name, $file, $import_sub) =
904 (shift @insert_list, shift @insert_list, shift @insert_list);
906 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
907 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
909 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
911 unlink $file or warn "Can't delete $file: $!";
914 $error ||= "No DETAIL supplied"
915 unless ($files{detail});
916 open my $fh, "< $dir/". $files{detail}
917 or $error ||= "Can't open DETAIL file: $!";
919 &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
922 unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
925 while( scalar(@delete_list) ) {
926 my ($name, $file, $import_sub) =
927 (shift @delete_list, shift @delete_list, shift @delete_list);
929 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
930 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
932 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
934 unlink $file or warn "Can't delete $file: $!";
938 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
941 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
945 die "Unknown format: $format";
950 =item browse_queries PARAMS
952 Returns a list consisting of a hashref suited for use as the argument
953 to qsearch, and sql query string. Each is based on the PARAMS hashref
954 of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
955 from a form. This conveniently creates the query hashref and count_query
956 string required by the browse and search elements. As a side effect,
957 the PARAMS hashref is untainted and keys with unexpected values are removed.
965 'table' => 'tax_rate',
967 'order_by' => 'ORDER BY geocode, taxclassnum',
972 if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
973 $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
975 delete $params->{data_vendor};
978 if ( $params->{geocode} =~ /^(\w+)$/ ) {
979 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
980 'geocode LIKE '. dbh->quote($1.'%');
982 delete $params->{geocode};
985 if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
986 qsearchs( 'tax_class', {'taxclassnum' => $1} )
989 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
990 ' taxclassnum = '. dbh->quote($1)
992 delete $params->{taxclassnun};
996 if ( $params->{tax_type} =~ /^(\d+)$/ );
997 delete $params->{tax_type}
1001 if ( $params->{tax_cat} =~ /^(\d+)$/ );
1002 delete $params->{tax_cat}
1005 my @taxclassnum = ();
1006 if ($tax_type || $tax_cat ) {
1007 my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
1008 $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
1009 @taxclassnum = map { $_->taxclassnum }
1010 qsearch({ 'table' => 'tax_class',
1012 'extra_sql' => "WHERE taxclass $compare",
1016 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
1017 join(' OR ', map { " taxclassnum = $_ " } @taxclassnum ). ' )'
1018 if ( @taxclassnum );
1020 unless ($params->{'showdisabled'}) {
1021 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
1022 "( disabled = '' OR disabled IS NULL )";
1025 $query->{extra_sql} = $extra_sql;
1027 return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
1034 Mixing automatic and manual editing works poorly at present.
1038 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base