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";
389 # until someone needs to track these in freeside
396 if ($self->maxtype != 0 && $self->maxtype != 9) {
397 return $self->_fatal_or_null( 'tax with "'.
398 $self->maxtype_name. '" threshold'
402 if ($self->maxtype == 9) {
404 $self->_fatal_or_null( 'tax with "'. $self->maxtype_name. '" threshold' );
408 # we treat gross revenue as gross receipts and expect the tax data
409 # to DTRT (i.e. tax on tax rules)
410 if ($self->basetype != 0 && $self->basetype != 1 &&
411 $self->basetype != 5 && $self->basetype != 6 &&
412 $self->basetype != 7 && $self->basetype != 8 &&
413 $self->basetype != 14
416 $self->_fatal_or_null( 'tax with "'. $self->basetype_name. '" basis' );
419 unless ($self->setuptax =~ /^Y$/i) {
420 $taxable_charged += $_->setup foreach @cust_bill_pkg;
422 unless ($self->recurtax =~ /^Y$/i) {
423 $taxable_charged += $_->recur foreach @cust_bill_pkg;
426 my $taxable_units = 0;
427 unless ($self->recurtax =~ /^Y$/i) {
428 if ($self->unittype == 0) {
430 foreach (@cust_bill_pkg) {
431 $taxable_units += $_->units
432 unless $seen{$_->pkgnum};
435 }elsif ($self->unittype == 1) {
436 return $self->_fatal_or_null( 'fee with minute unit type' );
437 }elsif ($self->unittype == 2) {
440 return $self->_fatal_or_null( 'unknown unit type in tax'. $self->taxnum );
445 # XXX insert exemption handling here
447 # the tax or fee is applied to taxbase or feebase and then
448 # the excessrate or excess fee is applied to taxmax or feemax
451 $amount += $taxable_charged * $self->tax;
452 $amount += $taxable_units * $self->fee;
454 warn "calculated taxes as [ $name, $amount ]\n"
465 my ($self, $error) = @_;
467 my $conf = new FS::Conf;
469 $error = "fatal: can't yet handle ". $error;
470 my $name = $self->taxname;
471 $name = 'Other surcharges'
472 if ($self->passtype == 2);
474 if ($conf->exists('ignore_incalculable_taxes')) {
476 return { name => $name, amount => 0 };
482 =item tax_on_tax CUST_MAIN
484 Returns a list of taxes which are candidates for taxing taxes for the
485 given customer (see L<FS::cust_main>)
491 my $cust_main = shift;
493 warn "looking up taxes on tax ". $self->taxnum. " for customer ".
497 my $geocode = $cust_main->geocode($self->data_vendor);
501 my $extra_sql = ' AND ('.
502 join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
507 my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
508 my $select = 'DISTINCT ON(taxclassnum) *';
510 # should qsearch preface columns with the table to facilitate joins?
511 my @taxclassnums = map { $_->taxclassnum }
512 qsearch( { 'table' => 'part_pkg_taxrate',
514 'hashref' => { 'data_vendor' => $self->data_vendor,
515 'taxclassnumtaxed' => $self->taxclassnum,
517 'extra_sql' => $extra_sql,
518 'order_by' => $order_by,
521 return () unless @taxclassnums;
524 "AND (". join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
526 qsearch({ 'table' => 'tax_rate',
527 'hashref' => { 'geocode' => $geocode, },
528 'extra_sql' => $extra_sql,
544 my ($param, $job) = @_;
546 my $fh = $param->{filehandle};
547 my $format = $param->{'format'};
555 my @column_lengths = ();
556 my @column_callbacks = ();
557 if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
558 $format =~ s/-fixed//;
559 my $date_format = sub { my $r='';
560 /^(\d{4})(\d{2})(\d{2})$/ && ($r="$1/$2/$3");
563 my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
565 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 );
566 push @column_lengths, 1 if $format eq 'cch-update';
567 push @column_callbacks, $trim foreach (@column_lengths); # 5, 6, 15, 17 esp
568 $column_callbacks[8] = $date_format;
572 my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
573 if ( $job || scalar(@column_callbacks) ) {
575 csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
576 return $error if $error;
580 if ( $format eq 'cch' || $format eq 'cch-update' ) {
581 @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
582 excessrate effective_date taxauth taxtype taxcat taxname
583 usetax useexcessrate fee unittype feemax maxtype passflag
585 push @fields, 'actionflag' if $format eq 'cch-update';
590 $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
591 $hash->{'data_vendor'} ='cch';
592 $hash->{'effective_date'} = str2time($hash->{'effective_date'});
595 join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
597 my %tax_class = ( 'data_vendor' => 'cch',
598 'taxclass' => $taxclassid,
601 my $tax_class = qsearchs( 'tax_class', \%tax_class );
602 return "Error updating tax rate: no tax class $taxclassid"
605 $hash->{'taxclassnum'} = $tax_class->taxclassnum;
607 foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
611 my %passflagmap = ( '0' => '',
615 $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
616 if exists $passflagmap{$hash->{'passflag'}};
618 foreach (keys %$hash) {
619 $hash->{$_} = substr($hash->{$_}, 0, 80)
620 if length($hash->{$_}) > 80;
623 my $actionflag = delete($hash->{'actionflag'});
625 $hash->{'taxname'} =~ s/`/'/g;
626 $hash->{'taxname'} =~ s|\\|/|g;
628 return '' if $format eq 'cch'; # but not cch-update
630 if ($actionflag eq 'I') {
631 $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
632 }elsif ($actionflag eq 'D') {
633 $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
635 return "Unexpected action flag: ". $hash->{'actionflag'};
638 delete($hash->{$_}) for keys %$hash;
644 } elsif ( $format eq 'extended' ) {
645 die "unimplemented\n";
649 die "unknown format $format";
652 eval "use Text::CSV_XS;";
655 my $csv = new Text::CSV_XS;
659 local $SIG{HUP} = 'IGNORE';
660 local $SIG{INT} = 'IGNORE';
661 local $SIG{QUIT} = 'IGNORE';
662 local $SIG{TERM} = 'IGNORE';
663 local $SIG{TSTP} = 'IGNORE';
664 local $SIG{PIPE} = 'IGNORE';
666 my $oldAutoCommit = $FS::UID::AutoCommit;
667 local $FS::UID::AutoCommit = 0;
670 while ( defined($line=<$fh>) ) {
671 $csv->parse($line) or do {
672 $dbh->rollback if $oldAutoCommit;
673 return "can't parse: ". $csv->error_input();
676 if ( $job ) { # progress bar
677 if ( time - $min_sec > $last ) {
678 my $error = $job->update_statustext(
679 int( 100 * $imported / $count )
681 die $error if $error;
686 my @columns = $csv->fields();
688 my %tax_rate = ( 'data_vendor' => $format );
689 foreach my $field ( @fields ) {
690 $tax_rate{$field} = shift @columns;
692 if ( scalar( @columns ) ) {
693 $dbh->rollback if $oldAutoCommit;
694 return "Unexpected trailing columns in line (wrong format?): $line";
697 my $error = &{$hook}(\%tax_rate);
699 $dbh->rollback if $oldAutoCommit;
703 if (scalar(keys %tax_rate)) { #inserts only, not updates for cch
705 my $tax_rate = new FS::tax_rate( \%tax_rate );
706 $error = $tax_rate->insert;
709 $dbh->rollback if $oldAutoCommit;
710 return "can't insert tax_rate for $line: $error";
719 for (grep { !exists($delete{$_}) } keys %insert) {
720 if ( $job ) { # progress bar
721 if ( time - $min_sec > $last ) {
722 my $error = $job->update_statustext(
723 int( 100 * $imported / $count )
725 die $error if $error;
730 my $tax_rate = new FS::tax_rate( $insert{$_} );
731 my $error = $tax_rate->insert;
734 $dbh->rollback if $oldAutoCommit;
735 my $hashref = $insert{$_};
736 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
737 return "can't insert tax_rate for $line: $error";
743 for (grep { exists($delete{$_}) } keys %insert) {
744 if ( $job ) { # progress bar
745 if ( time - $min_sec > $last ) {
746 my $error = $job->update_statustext(
747 int( 100 * $imported / $count )
749 die $error if $error;
754 my $old = qsearchs( 'tax_rate', $delete{$_} );
756 $dbh->rollback if $oldAutoCommit;
758 return "can't find tax_rate to replace for: ".
759 #join(" ", map { "$_ => ". $old->{$_} } @fields);
760 join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
762 my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' });
763 $new->taxnum($old->taxnum);
764 my $error = $new->replace($old);
767 $dbh->rollback if $oldAutoCommit;
768 my $hashref = $insert{$_};
769 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
770 return "can't replace tax_rate for $line: $error";
777 for (grep { !exists($insert{$_}) } keys %delete) {
778 if ( $job ) { # progress bar
779 if ( time - $min_sec > $last ) {
780 my $error = $job->update_statustext(
781 int( 100 * $imported / $count )
783 die $error if $error;
788 my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
790 $dbh->rollback if $oldAutoCommit;
791 $tax_rate = $delete{$_};
792 return "can't find tax_rate to delete for: ".
793 #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
794 join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
796 my $error = $tax_rate->delete;
799 $dbh->rollback if $oldAutoCommit;
800 my $hashref = $delete{$_};
801 $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
802 return "can't delete tax_rate for $line: $error";
808 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
810 return "Empty file!" unless ($imported || $format eq 'cch-update');
816 =item process_batch_import
818 Load a batch import as a queued JSRPC job
822 sub process_batch_import {
825 my $param = thaw(decode_base64(shift));
826 my $format = $param->{'format'}; #well... this is all cch specific
828 my $files = $param->{'uploaded_files'}
829 or die "No files provided.";
831 my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
833 if ($format eq 'cch' || $format eq 'cch-fixed') {
835 my $oldAutoCommit = $FS::UID::AutoCommit;
836 local $FS::UID::AutoCommit = 0;
839 my $have_location = 0;
841 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
842 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
843 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
844 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
845 'DETAIL', 'detail', \&FS::tax_rate::batch_import,
847 while( scalar(@list) ) {
848 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
849 unless ($files{$file}) {
850 next if $name eq 'PLUS4';
851 $error = "No $name supplied";
852 $error = "Neither PLUS4 nor ZIP supplied"
853 if ($name eq 'ZIP' && !$have_location);
856 $have_location = 1 if $name eq 'PLUS4';
857 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
858 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
859 my $filename = "$dir/". $files{$file};
860 open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
862 $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
864 unlink $filename or warn "Can't delete $filename: $!";
868 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
871 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
874 }elsif ($format eq 'cch-update' || $format eq 'cch-fixed-update') {
876 my $oldAutoCommit = $FS::UID::AutoCommit;
877 local $FS::UID::AutoCommit = 0;
880 my @insert_list = ();
881 my @delete_list = ();
883 my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
884 'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
885 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
886 'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
888 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
889 while( scalar(@list) ) {
890 my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
891 unless ($files{$file}) {
892 my $vendor = $name eq 'ZIP' ? 'cch' : 'cch-zip';
893 next # update expected only for previously installed location data
894 if ( ($name eq 'PLUS4' || $name eq 'ZIP')
895 && !scalar( qsearch( { table => 'cust_tax_location',
896 hashref => { data_vendor => $vendor },
897 select => 'DISTINCT data_vendor',
902 $error = "No $name supplied";
905 my $filename = "$dir/". $files{$file};
906 open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
907 unlink $filename or warn "Can't delete $filename: $!";
909 my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
912 ) or die "can't open temp file: $!\n";
914 my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
917 ) or die "can't open temp file: $!\n";
919 my $insert_pattern = ($format eq 'cch-update') ? qr/"I"\s*$/ : qr/I\s*$/;
920 my $delete_pattern = ($format eq 'cch-update') ? qr/"D"\s*$/ : qr/D\s*$/;
923 $handle = $ifh if $_ =~ /$insert_pattern/;
924 $handle = $dfh if $_ =~ /$delete_pattern/;
926 $error = "bad input line: $_" unless $handle;
935 push @insert_list, $name, $ifh->filename, $import_sub;
936 unshift @delete_list, $name, $dfh->filename, $import_sub;
939 while( scalar(@insert_list) ) {
940 my ($name, $file, $import_sub) =
941 (shift @insert_list, shift @insert_list, shift @insert_list);
943 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
944 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
946 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
948 unlink $file or warn "Can't delete $file: $!";
951 $error ||= "No DETAIL supplied"
952 unless ($files{detail});
953 open my $fh, "< $dir/". $files{detail}
954 or $error ||= "Can't open DETAIL file: $!";
956 &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
959 unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
962 while( scalar(@delete_list) ) {
963 my ($name, $file, $import_sub) =
964 (shift @delete_list, shift @delete_list, shift @delete_list);
966 my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
967 open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
969 &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
971 unlink $file or warn "Can't delete $file: $!";
975 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
978 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
982 die "Unknown format: $format";
987 =item browse_queries PARAMS
989 Returns a list consisting of a hashref suited for use as the argument
990 to qsearch, and sql query string. Each is based on the PARAMS hashref
991 of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
992 from a form. This conveniently creates the query hashref and count_query
993 string required by the browse and search elements. As a side effect,
994 the PARAMS hashref is untainted and keys with unexpected values are removed.
1002 'table' => 'tax_rate',
1004 'order_by' => 'ORDER BY geocode, taxclassnum',
1009 if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
1010 $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
1012 delete $params->{data_vendor};
1015 if ( $params->{geocode} =~ /^(\w+)$/ ) {
1016 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
1017 'geocode LIKE '. dbh->quote($1.'%');
1019 delete $params->{geocode};
1022 if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
1023 qsearchs( 'tax_class', {'taxclassnum' => $1} )
1026 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
1027 ' taxclassnum = '. dbh->quote($1)
1029 delete $params->{taxclassnun};
1033 if ( $params->{tax_type} =~ /^(\d+)$/ );
1034 delete $params->{tax_type}
1038 if ( $params->{tax_cat} =~ /^(\d+)$/ );
1039 delete $params->{tax_cat}
1042 my @taxclassnum = ();
1043 if ($tax_type || $tax_cat ) {
1044 my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
1045 $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
1046 @taxclassnum = map { $_->taxclassnum }
1047 qsearch({ 'table' => 'tax_class',
1049 'extra_sql' => "WHERE taxclass $compare",
1053 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
1054 join(' OR ', map { " taxclassnum = $_ " } @taxclassnum ). ' )'
1055 if ( @taxclassnum );
1057 unless ($params->{'showdisabled'}) {
1058 $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
1059 "( disabled = '' OR disabled IS NULL )";
1062 $query->{extra_sql} = $extra_sql;
1064 return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
1071 Mixing automatic and manual editing works poorly at present.
1075 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base