1 package FS::cust_main_county;
2 use base qw( FS::Record );
5 use vars qw( @EXPORT_OK $conf
6 @cust_main_county %cust_main_county $countyflag ); # $cityflag );
8 use FS::Record qw( qsearch qsearchs dbh );
13 use FS::cust_tax_exempt;
14 use FS::cust_tax_exempt_pkg;
15 use FS::upgrade_journal;
17 @EXPORT_OK = qw( regionselector );
19 @cust_main_county = ();
23 #ask FS::UID to run this stuff for us later
24 $FS::UID::callback{'FS::cust_main_county'} = sub {
30 FS::cust_main_county - Object methods for cust_main_county objects
34 use FS::cust_main_county;
36 $record = new FS::cust_main_county \%hash;
37 $record = new FS::cust_main_county { 'column' => 'value' };
39 $error = $record->insert;
41 $error = $new_record->replace($old_record);
43 $error = $record->delete;
45 $error = $record->check;
47 ($county_html, $state_html, $country_html) =
48 FS::cust_main_county::regionselector( $county, $state, $country );
52 An FS::cust_main_county object represents a tax rate, defined by locale.
53 FS::cust_main_county inherits from FS::Record. The following fields are
58 =item taxnum - primary key (assigned automatically for new tax rates)
60 =item district - tax district (optional)
70 =item tax - percentage
76 =item taxname - if defined, printed on invoices instead of "Tax"
78 =item setuptax - if 'Y', this tax does not apply to setup fees
80 =item recurtax - if 'Y', this tax does not apply to recurring fees
82 =item source - the tax lookup method that created this tax record. For records
83 created manually, this will be null.
93 Creates a new tax rate. To add the tax rate to the database, see L<"insert">.
97 sub table { 'cust_main_county'; }
101 Adds this tax rate to the database. If there is an error, returns the error,
102 otherwise returns false.
106 Deletes this tax rate from the database. If there is an error, returns the
107 error, otherwise returns false.
109 =item replace OLD_RECORD
111 Replaces the OLD_RECORD with this one in the database. If there is an error,
112 returns the error, otherwise returns false.
116 Checks all fields to make sure this is a valid tax rate. If there is an error,
117 returns the error, otherwise returns false. Called by the insert and replace
125 $self->trim_whitespace(qw(district city county state country));
126 $self->set('city', uc($self->get('city'))); # also county?
128 $self->exempt_amount(0) unless $self->exempt_amount;
130 $self->ut_numbern('taxnum')
131 || $self->ut_alphan('district')
132 || $self->ut_textn('city')
133 || $self->ut_textn('county')
134 || $self->ut_anything('state')
135 || $self->ut_text('country')
136 || $self->ut_float('tax')
137 || $self->ut_textn('taxclass') # ...
138 || $self->ut_money('exempt_amount')
139 || $self->ut_textn('taxname')
140 || $self->ut_enum('setuptax', [ '', 'Y' ] )
141 || $self->ut_enum('recurtax', [ '', 'Y' ] )
142 || $self->ut_textn('source')
143 || $self->SUPER::check
150 Returns a label looking like "Anytown, Alameda County, CA, US".
152 If the taxname field is set, it will look like
153 "CA Sales Tax (Anytown, Alameda County, CA, US)".
155 If the taxclass is set, then it will be
156 "Anytown, Alameda County, CA, US (International)".
158 OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show
159 those fields. It may also contain "out", in which case, if this region
160 (district+city+county+state+country) contains no non-zero taxes, the label
161 will read "Out of taxable region(s)".
166 my ($self, %opt) = @_;
169 and !defined(qsearchs('cust_main_county', {
170 'district' => $self->district,
171 'city' => $self->city,
172 'county' => $self->county,
173 'state' => $self->state,
174 'country' => $self->country,
175 'tax' => { op => '>', value => 0 },
178 return 'Out of taxable region(s)';
180 my $label = $self->country;
181 $label = $self->state.", $label" if $self->state;
182 $label = $self->county." County, $label" if $self->county;
183 if ($opt{with_city}) {
184 $label = $self->city.", $label" if $self->city;
185 if ($opt{with_district} and $self->district) {
186 $label = $self->district . ", $label";
189 # ugly labels when taxclass and taxname are both non-null...
190 # but this is how the tax report does it
191 if ($opt{with_taxclass}) {
192 $label = "$label (".$self->taxclass.')' if $self->taxclass;
194 $label = $self->taxname." ($label)" if $self->taxname;
199 =item sql_taxclass_sameregion
201 Returns an SQL WHERE fragment or the empty string to search for entries
202 with different tax classes.
206 #hmm, description above could be better...
208 sub sql_taxclass_sameregion {
211 my $same_query = 'SELECT DISTINCT taxclass FROM cust_main_county '.
212 ' WHERE taxnum != ? AND country = ?';
213 my @same_param = ( 'taxnum', 'country' );
214 foreach my $opt_field (qw( state county )) {
215 if ( $self->$opt_field() ) {
216 $same_query .= " AND $opt_field = ?";
217 push @same_param, $opt_field;
219 $same_query .= " AND $opt_field IS NULL";
223 my @taxclasses = $self->_list_sql( \@same_param, $same_query );
225 return '' unless scalar(@taxclasses);
227 '( taxclass IS NULL OR ( '. #only if !$self->taxclass ??
228 join(' AND ', map { 'taxclass != '.dbh->quote($_) } @taxclasses ).
233 my( $self, $param, $sql ) = @_;
234 my $sth = dbh->prepare($sql) or die dbh->errstr;
235 $sth->execute( map $self->$_(), @$param )
236 or die "Unexpected error executing statement $sql: ". $sth->errstr;
237 map $_->[0], @{ $sth->fetchall_arrayref };
240 =item taxline TAXABLES_ARRAYREF, [ OPTION => VALUE ... ]
242 Takes an arrayref of L<FS::cust_bill_pkg> objects representing taxable
243 line items, and returns a new L<FS::cust_bill_pkg> object representing
244 the tax on them under this tax rate.
246 This will have a pseudo-field, "cust_bill_pkg_tax_location", containing
247 an arrayref of L<FS::cust_bill_pkg_tax_location> objects. Each of these
248 will in turn have a "taxable_cust_bill_pkg" pseudo-field linking it to one
249 of the taxable items. All of these links must be resolved as the objects
252 In addition to calculating the tax for the line items, this will calculate
253 any appropriate tax exemptions and attach them to the line items.
255 Options may include 'custnum' and 'invoice_time' in case the cust_bill_pkg
256 objects belong to an invoice that hasn't been inserted yet.
258 Options may include 'exemptions', an arrayref of L<FS::cust_tax_exempt_pkg>
259 objects belonging to the same customer, to be counted against the monthly
260 tax exemption limit if there is one.
264 # XXX change tax_rate.pm to work like this
267 my( $self, $taxables, %opt ) = @_;
268 return 'taxline called with no line items' unless @$taxables;
270 local $SIG{HUP} = 'IGNORE';
271 local $SIG{INT} = 'IGNORE';
272 local $SIG{QUIT} = 'IGNORE';
273 local $SIG{TERM} = 'IGNORE';
274 local $SIG{TSTP} = 'IGNORE';
275 local $SIG{PIPE} = 'IGNORE';
277 my $oldAutoCommit = $FS::UID::AutoCommit;
278 local $FS::UID::AutoCommit = 0;
281 my $name = $self->taxname || 'Tax';
282 my $taxable_cents = 0;
285 my $cust_bill = $taxables->[0]->cust_bill;
286 my $custnum = $cust_bill ? $cust_bill->custnum : $opt{'custnum'};
287 my $invoice_time = $cust_bill ? $cust_bill->_date : $opt{'invoice_time'};
288 my $cust_main = FS::cust_main->by_key($custnum) if $custnum;
289 # (to avoid complications with estimated tax on quotations, assume it's
290 # taxable if there is no customer)
292 #die "unable to calculate taxes for an unknown customer\n";
295 # set a flag if the customer is tax-exempt
296 my ($exempt_cust, $exempt_cust_taxname);
297 my $conf = FS::Conf->new;
299 if ( $conf->exists('cust_class-tax_exempt') ) {
300 my $cust_class = $cust_main->cust_class;
301 $exempt_cust = $cust_class->tax if $cust_class;
303 $exempt_cust = $cust_main->tax;
306 # set a flag if the customer is exempt from this tax here
307 if ( $self->taxname ) {
308 $exempt_cust_taxname = $cust_main->tax_exemption($self->taxname);
312 # Gather any exemptions that are already attached to these cust_bill_pkgs
313 # so that we can deduct them from the customer's monthly limit.
314 my @existing_exemptions = @{ $opt{'exemptions'} };
315 push @existing_exemptions, @{ $_->cust_tax_exempt_pkg }
318 my $tax_item = FS::cust_bill_pkg->new({
327 foreach my $cust_bill_pkg (@$taxables) {
328 # careful... may be a cust_bill_pkg or a quotation_pkg
330 my $cust_pkg = $cust_bill_pkg->cust_pkg;
331 my $part_pkg = $cust_bill_pkg->part_pkg;
332 my $part_fee = $cust_bill_pkg->part_fee;
334 my $locationnum = $cust_bill_pkg->tax_locationnum
335 || $cust_main->ship_locationnum;
338 my $taxable_charged = $cust_bill_pkg->setup + $cust_bill_pkg->recur
339 or next; # don't create zero-amount exemptions
341 # XXX the following procedure should probably be in cust_bill_pkg
343 if ( $exempt_cust ) {
345 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
346 amount => $taxable_charged,
349 $taxable_charged = 0;
351 } elsif ( $exempt_cust_taxname ) {
353 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
354 amount => $taxable_charged,
355 exempt_cust_taxname => 'Y',
357 $taxable_charged = 0;
361 my $setup_exempt = ( ($part_fee and not $part_fee->taxable)
362 or ($part_pkg and $part_pkg->setuptax)
363 or $self->setuptax );
366 and $cust_bill_pkg->setup > 0
367 and $taxable_charged > 0 ) {
369 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
370 amount => $cust_bill_pkg->setup,
373 $taxable_charged -= $cust_bill_pkg->setup;
377 my $recur_exempt = ( ($part_fee and not $part_fee->taxable)
378 or ($part_pkg and $part_pkg->recurtax)
379 or $self->recurtax );
382 and $cust_bill_pkg->recur > 0
383 and $taxable_charged > 0 ) {
385 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
386 amount => $cust_bill_pkg->recur,
389 $taxable_charged -= $cust_bill_pkg->recur;
393 if ( $self->exempt_amount && $self->exempt_amount > 0
394 and $taxable_charged > 0
397 # XXX monthly exemptions currently don't work on quotations
399 # If the billing period extends across multiple calendar months,
400 # there may be several months of exemption available.
401 my $sdate = $cust_bill_pkg->sdate || $invoice_time;
402 my $start_month = (localtime($sdate))[4] + 1;
403 my $start_year = (localtime($sdate))[5] + 1900;
404 my $edate = $cust_bill_pkg->edate || $invoice_time;
405 my $end_month = (localtime($edate))[4] + 1;
406 my $end_year = (localtime($edate))[5] + 1900;
408 # If the partial last month + partial first month <= one month,
409 # don't use the exemption in the last month
410 # (unless the last month is also the first month, e.g. one-time
412 if ( (localtime($sdate))[3] >= (localtime($edate))[3]
413 and ($start_month != $end_month or $start_year != $end_year)
416 if ( $end_month == 0 ) {
422 # number of months of exemption available
423 my $freq = ($end_month - $start_month) +
424 ($end_year - $start_year) * 12 +
427 # divide equally among all of them
428 my $permonth = sprintf('%.2f', $taxable_charged / $freq);
430 #call the whole thing off if this customer has any old
431 #exemption records...
432 my @cust_tax_exempt =
433 qsearch( 'cust_tax_exempt' => { custnum=> $custnum } );
434 if ( @cust_tax_exempt ) {
435 $dbh->rollback if $oldAutoCommit;
437 'this customer still has old-style tax exemption records; '.
438 'run bin/fs-migrate-cust_tax_exempt?';
441 my ($mon, $year) = ($start_month, $start_year);
442 while ($taxable_charged > 0.005 and
443 ($year < $end_year or
444 ($year == $end_year and $mon <= $end_month)
448 # find the sum of the exemption used by this customer, for this tax,
452 FROM cust_tax_exempt_pkg
453 LEFT JOIN cust_bill_pkg USING ( billpkgnum )
454 LEFT JOIN cust_bill USING ( invnum )
459 AND exempt_monthly = 'Y'
461 my $sth = dbh->prepare($sql) or do {
462 $dbh->rollback if $oldAutoCommit;
463 return "fatal: can't lookup existing exemption: ". dbh->errstr;
471 $dbh->rollback if $oldAutoCommit;
472 return "fatal: can't lookup existing exemption: ". dbh->errstr;
474 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
476 # add any exemption we're already using for another line item
477 foreach ( grep { $_->taxnum == $self->taxnum &&
478 $_->exempt_monthly eq 'Y' &&
481 } @existing_exemptions
484 $existing_exemption += $_->amount;
487 my $remaining_exemption =
488 $self->exempt_amount - $existing_exemption;
489 if ( $remaining_exemption > 0 ) {
490 my $addl = $remaining_exemption > $permonth
492 : $remaining_exemption;
493 $addl = $taxable_charged if $addl > $taxable_charged;
495 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
496 amount => sprintf('%.2f', $addl),
497 exempt_monthly => 'Y',
501 $taxable_charged -= $addl;
503 # if they're using multiple months of exemption for a multi-month
504 # package, then record the exemptions in separate months
512 } # if exempt_amount and $cust_main
514 $_->taxnum($self->taxnum) foreach @new_exemptions;
516 # attach them to the line item
517 push @{ $cust_bill_pkg->cust_tax_exempt_pkg }, @new_exemptions;
518 push @existing_exemptions, @new_exemptions;
520 $taxable_charged = sprintf( "%.2f", $taxable_charged);
521 next if $taxable_charged == 0;
523 my $this_tax_cents = int($taxable_charged * $self->tax);
524 my $location = FS::cust_bill_pkg_tax_location->new({
525 'taxnum' => $self->taxnum,
526 'taxtype' => ref($self),
527 'cents' => $this_tax_cents,
528 'pkgnum' => $cust_bill_pkg->pkgnum,
529 'locationnum' => $locationnum,
530 'taxable_cust_bill_pkg' => $cust_bill_pkg,
531 'tax_cust_bill_pkg' => $tax_item,
533 push @tax_location, $location;
535 $taxable_cents += $taxable_charged;
536 $tax_cents += $this_tax_cents;
537 } #foreach $cust_bill_pkg
539 # now round and distribute
540 my $extra_cents = sprintf('%.2f', $taxable_cents * $self->tax / 100) * 100
542 # make sure we have an integer
543 $extra_cents = sprintf('%.0f', $extra_cents);
544 if ( $extra_cents < 0 ) {
545 die "nonsense extra_cents value $extra_cents";
547 $tax_cents += $extra_cents;
549 foreach (@tax_location) { # can never require more than a single pass, yes?
550 my $cents = $_->get('cents');
551 if ( $extra_cents > 0 ) {
555 $_->set('amount', sprintf('%.2f', $cents/100));
557 $tax_item->set('setup' => sprintf('%.2f', $tax_cents / 100));
558 $tax_item->set('cust_bill_pkg_tax_location', \@tax_location);
569 =item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
574 my ( $selected_county, $selected_state, $selected_country,
575 $prefix, $onchange, $disabled ) = @_;
577 $prefix = '' unless defined $prefix;
581 # unless ( @cust_main_county ) { #cache
582 @cust_main_county = qsearch('cust_main_county', {} );
583 foreach my $c ( @cust_main_county ) {
584 $countyflag=1 if $c->county;
585 #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
586 $cust_main_county{$c->country}{$c->state}{$c->county} = 1;
589 $countyflag=1 if $selected_county;
591 my $script_html = <<END;
593 function opt(what,value,text) {
594 var optionName = new Option(text, value, false, false);
595 var length = what.length;
596 what.options[length] = optionName;
598 function ${prefix}country_changed(what) {
599 country = what.options[what.selectedIndex].text;
600 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
601 what.form.${prefix}state.options[i] = null;
603 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
605 foreach my $country ( sort keys %cust_main_county ) {
606 $script_html .= "\nif ( country == \"$country\" ) {\n";
607 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
608 ( my $dstate = $state ) =~ s/[\n\r]//g;
609 my $text = $dstate || '(n/a)';
610 $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
612 $script_html .= "}\n";
615 $script_html .= <<END;
617 function ${prefix}state_changed(what) {
621 $script_html .= <<END;
622 state = what.options[what.selectedIndex].text;
623 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
624 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
625 what.form.${prefix}county.options[i] = null;
628 foreach my $country ( sort keys %cust_main_county ) {
629 $script_html .= "\nif ( country == \"$country\" ) {\n";
630 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
631 $script_html .= "\nif ( state == \"$state\" ) {\n";
632 #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
633 foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
634 my $text = $county || '(n/a)';
636 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
638 $script_html .= "}\n";
640 $script_html .= "}\n";
644 $script_html .= <<END;
649 my $county_html = $script_html;
651 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
652 $county_html .= '</SELECT>';
655 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
658 my $state_html = qq!<SELECT NAME="${prefix}state" !.
659 qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
660 foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
661 my $text = $state || '(n/a)';
662 my $selected = $state eq $selected_state ? 'SELECTED' : '';
663 $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
665 $state_html .= '</SELECT>';
667 $state_html .= '</SELECT>';
669 my $country_html = qq!<SELECT NAME="${prefix}country" !.
670 qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
671 my $countrydefault = $conf->config('countrydefault') || 'US';
672 foreach my $country (
673 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
674 keys %cust_main_county
676 my $selected = $country eq $selected_country ? ' SELECTED' : '';
677 $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
679 $country_html .= '</SELECT>';
681 ($county_html, $state_html, $country_html);
686 # for internal use: takes another cust_main_county object, transfers
687 # all existing references to this record to that one, and deletes this
690 my $other = shift or die "record to merge into must be provided";
691 my $new_taxnum = $other->taxnum;
692 my $old_taxnum = $record->taxnum;
693 if ($other->tax != $record->tax or
694 $other->exempt_amount != $record->exempt_amount) {
695 # don't assume these are the same.
696 warn "Found duplicate taxes (#$new_taxnum and #$old_taxnum) but they have different rates and can't be merged.\n";
698 warn "Merging tax #$old_taxnum into #$new_taxnum\n";
699 foreach my $table (qw(
700 cust_bill_pkg_tax_location
701 cust_bill_pkg_tax_location_void
703 cust_tax_exempt_pkg_void
705 foreach my $row (qsearch($table, { 'taxnum' => $old_taxnum })) {
706 $row->set('taxnum' => $new_taxnum);
707 my $error = $row->replace;
708 die $error if $error;
711 my $error = $record->delete;
712 die $error if $error;
718 # assume taxes in Washington with district numbers, and null name, or
719 # named 'sales tax', are looked up via the wa_sales method. mark them.
720 my $journal = 'cust_main_county__source_wa_sales_201611';
721 if (!FS::upgrade_journal->is_done($journal)) {
722 my @taxes = qsearch({
723 'table' => 'cust_main_county',
724 'extra_sql' => " WHERE tax > 0 AND country = 'US' AND state = 'WA'".
725 " AND district IS NOT NULL AND ( taxname IS NULL OR ".
726 " taxname ~* 'sales tax' )",
729 warn "Flagging Washington state sales taxes: ".scalar(@taxes)." records.\n";
731 $_->set('source', 'wa_sales');
732 my $error = $_->replace;
733 die $error if $error;
736 FS::upgrade_journal->set_done($journal);
738 my @key_fields = (qw(city county state country district taxname taxclass));
740 # trim whitespace and convert to uppercase in the 'city' field.
741 foreach my $record (qsearch({
742 table => 'cust_main_county',
743 extra_sql => " WHERE city LIKE ' %' OR city LIKE '% ' OR city != UPPER(city)",
745 # any with-trailing-space records probably duplicate other records
746 # from the same city, and if we just fix the record in place, we'll
747 # create an exact duplicate.
748 # so find the record this one would duplicate, and merge them.
749 $record->check; # trims whitespace
750 my %match = map { $_ => $record->get($_) } @key_fields;
751 my $other = qsearchs('cust_main_county', \%match);
753 $record->_merge_into($other);
755 # else there is no record this one duplicates, so just fix it
756 my $error = $record->replace;
757 die $error if $error;
761 # separate wa_sales taxes by tax class as needed
762 my $district_taxname = $conf->config('tax_district_taxname');
763 $journal = 'cust_main_county__district_taxclass';
764 if (!FS::upgrade_journal->is_done($journal)
765 and $conf->exists('enable_taxclasses')) {
766 eval "use FS::part_pkg_taxclass";
767 my @taxes = qsearch({
768 'table' => 'cust_main_county',
769 'extra_sql' => " WHERE tax > 0 AND country = 'US' AND state = 'WA'".
770 " AND district IS NOT NULL AND source = 'wa_sales'".
771 " AND taxclass IS NULL"
773 my @classes = FS::part_pkg_taxclass->taxclass_names;
775 warn "Separating WA sales taxes: ".scalar(@taxes)." records.\n";
776 foreach my $oldtax (@taxes) {
778 my $taxnum = $oldtax->taxnum;
779 warn "Separating tax #$taxnum into classes\n";
780 foreach my $taxclass (@classes) {
781 # ensure that we end up with a single copy of the tax in this
782 # jurisdiction+class. there may already be one (or more) there.
783 # if so, they all represent the same tax; merge them together.
787 'city' => $oldtax->city,
788 'district' => $oldtax->district,
789 'taxclass' => $taxclass,
790 'source' => 'wa_sales',
792 my @taxes_in_class = qsearch('cust_main_county', {
794 'tax' => { op => '>', value => 0 },
798 my $newtax = shift @taxes_in_class;
800 foreach (@taxes_in_class) {
801 # allow the merge, even if this somehow differs.
802 $_->set('tax', $newtax->tax);
803 $_->_merge_into($newtax);
806 $newtax ||= FS::cust_main_county->new(\%newtax_hash);
807 # copy properties from the pre-split tax
808 $newtax->set('tax', $oldtax->tax);
809 $newtax->set('setuptax', $oldtax->setuptax);
810 $newtax->set('recurtax', $oldtax->recurtax);
811 # and assign the defined tax name
812 $newtax->set('taxname', $district_taxname);
813 $error = ($newtax->taxnum ? $newtax->replace : $newtax->insert);
814 die "splitting taxnum ".$oldtax->taxnum.": $error\n" if $error;
815 } # foreach $taxclass
816 $oldtax->set('tax', 0);
817 $error = $oldtax->replace;
818 die "splitting taxnum ".$oldtax->taxnum.": $error\n" if $error;
821 FS::upgrade_journal->set_done($journal);
824 # also ensure they all have the chosen taxname now
825 if ($district_taxname) {
826 my @taxes = qsearch('cust_main_county', {
827 'source' => 'wa_sales',
828 'taxname' => { op => '!=', value => $district_taxname }
831 warn "Renaming WA sales taxes: ".scalar(@taxes)." records.\n";
832 foreach my $tax (@taxes) {
833 $tax->set('taxname', $district_taxname);
834 my $error = $tax->replace;
835 die "renaming taxnum ".$tax->taxnum.": $error\n" if $error;
840 # remove duplicates (except disabled records)
841 my @duplicate_sets = qsearch({
842 table => 'cust_main_county',
843 select => FS::Record::group_concat_sql('taxnum', ',') . ' AS taxnums, ' .
844 join(',', @key_fields),
845 extra_sql => ' WHERE tax > 0
846 GROUP BY city, county, state, country, district, taxname, taxclass
849 warn "Found ".scalar(@duplicate_sets)." set(s) of duplicate tax definitions\n"
851 foreach my $set (@duplicate_sets) {
852 my @taxnums = split(',', $set->get('taxnums'));
853 my $first = FS::cust_main_county->by_key(shift @taxnums);
854 foreach my $taxnum (@taxnums) {
855 my $record = FS::cust_main_county->by_key($taxnum);
856 $record->_merge_into($first);
868 regionselector? putting web ui components in here? they should probably live
873 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base