X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_main_county.pm;h=3c355e823aaaf76aab4da6e94e902906e4fbf7c6;hp=9a4990a9dd37e1de280bd9c5e1d3a2a0764a9b21;hb=674cb2d9d7105f4cc2871539b2e9f7088cdaa750;hpb=c6782ab85ea83e0c78d85b8975985aac9d467f9d diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index 9a4990a9d..3c355e823 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -1,7 +1,8 @@ package FS::cust_main_county; +use base qw( FS::Record ); use strict; -use vars qw( @ISA @EXPORT_OK $conf +use vars qw( @EXPORT_OK $conf @cust_main_county %cust_main_county $countyflag ); # $cityflag ); use Exporter; use FS::Record qw( qsearch qsearchs dbh ); @@ -11,8 +12,8 @@ use FS::cust_pkg; use FS::part_pkg; use FS::cust_tax_exempt; use FS::cust_tax_exempt_pkg; +use FS::upgrade_journal; -@ISA = qw( FS::Record ); @EXPORT_OK = qw( regionselector ); @cust_main_county = (); @@ -78,6 +79,9 @@ currently supported: =item recurtax - if 'Y', this tax does not apply to recurring fees +=item source - the tax lookup method that created this tax record. For records +created manually, this will be null. + =back =head1 METHODS @@ -132,6 +136,7 @@ sub check { || $self->ut_textn('taxname') || $self->ut_enum('setuptax', [ '', 'Y' ] ) || $self->ut_enum('recurtax', [ '', 'Y' ] ) + || $self->ut_textn('source') || $self->SUPER::check ; @@ -147,13 +152,10 @@ If the taxname field is set, it will look like If the taxclass is set, then it will be "Anytown, Alameda County, CA, US (International)". -Currently it will not contain the district, even if the city+county+state -is not unique. - -OPTIONS may contain "no_taxclass" (hides taxclass) and/or "no_city" -(hides city). It may also contain "out", in which case, if this -region (district+city+county+state+country) contains no non-zero -taxes, the label will read "Out of taxable region(s)". +OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show +those fields. It may also contain "out", in which case, if this region +(district+city+county+state+country) contains no non-zero taxes, the label +will read "Out of taxable region(s)". =cut @@ -175,12 +177,15 @@ sub label { my $label = $self->country; $label = $self->state.", $label" if $self->state; $label = $self->county." County, $label" if $self->county; - if (!$opt{no_city}) { + if ($opt{with_city}) { $label = $self->city.", $label" if $self->city; + if ($opt{with_district} and $self->district) { + $label = $self->district . ", $label"; + } } # ugly labels when taxclass and taxname are both non-null... # but this is how the tax report does it - if (!$opt{no_taxclass}) { + if ($opt{with_taxclass}) { $label = "$label (".$self->taxclass.')' if $self->taxclass; } $label = $self->taxname." ($label)" if $self->taxname; @@ -244,7 +249,7 @@ are inserted. In addition to calculating the tax for the line items, this will calculate any appropriate tax exemptions and attach them to the line items. -Options may include 'custnum' and 'invoice_date' in case the cust_bill_pkg +Options may include 'custnum' and 'invoice_time' in case the cust_bill_pkg objects belong to an invoice that hasn't been inserted yet. Options may include 'exemptions', an arrayref of L @@ -276,26 +281,30 @@ sub taxline { my $cust_bill = $taxables->[0]->cust_bill; my $custnum = $cust_bill ? $cust_bill->custnum : $opt{'custnum'}; - my $invoice_date = $cust_bill ? $cust_bill->_date : $opt{'invoice_date'}; - my $cust_main = FS::cust_main->by_key($custnum) if $custnum > 0; - if (!$cust_main) { - # better way to handle this? should we just assume that it's taxable? - die "unable to calculate taxes for an unknown customer\n"; - } + my $invoice_time = $cust_bill ? $cust_bill->_date : $opt{'invoice_time'}; + my $cust_main = FS::cust_main->by_key($custnum) if $custnum; + # (to avoid complications with estimated tax on quotations, assume it's + # taxable if there is no customer) + #if (!$cust_main) { + #die "unable to calculate taxes for an unknown customer\n"; + #} # set a flag if the customer is tax-exempt - my $exempt_cust; + my ($exempt_cust, $exempt_cust_taxname); my $conf = FS::Conf->new; - if ( $conf->exists('cust_class-tax_exempt') ) { - my $cust_class = $cust_main->cust_class; - $exempt_cust = $cust_class->tax if $cust_class; - } else { - $exempt_cust = $cust_main->tax; - } + if ( $cust_main ) { + if ( $conf->exists('cust_class-tax_exempt') ) { + my $cust_class = $cust_main->cust_class; + $exempt_cust = $cust_class->tax if $cust_class; + } else { + $exempt_cust = $cust_main->tax; + } - # set a flag if the customer is exempt from this tax here - my $exempt_cust_taxname = $cust_main->tax_exemption($self->taxname) - if $self->taxname; + # set a flag if the customer is exempt from this tax here + if ( $self->taxname ) { + $exempt_cust_taxname = $cust_main->tax_exemption($self->taxname); + } + } # Gather any exemptions that are already attached to these cust_bill_pkgs # so that we can deduct them from the customer's monthly limit. @@ -313,9 +322,14 @@ sub taxline { my @tax_location; foreach my $cust_bill_pkg (@$taxables) { + # careful... may be a cust_bill_pkg or a quotation_pkg my $cust_pkg = $cust_bill_pkg->cust_pkg; my $part_pkg = $cust_bill_pkg->part_pkg; + my $part_fee = $cust_bill_pkg->part_fee; + + my $locationnum = $cust_bill_pkg->tax_locationnum + || $cust_main->ship_locationnum; my @new_exemptions; my $taxable_charged = $cust_bill_pkg->setup + $cust_bill_pkg->recur @@ -341,8 +355,13 @@ sub taxline { } - if ( ($part_pkg->setuptax eq 'Y' or $self->setuptax eq 'Y') - and $cust_bill_pkg->setup > 0 and $taxable_charged > 0 ) { + my $setup_exempt = ( ($part_fee and not $part_fee->taxable) + or ($part_pkg and $part_pkg->setuptax) + or $self->setuptax ); + + if ( $setup_exempt + and $cust_bill_pkg->setup > 0 + and $taxable_charged > 0 ) { push @new_exemptions, FS::cust_tax_exempt_pkg->new({ amount => $cust_bill_pkg->setup, @@ -351,8 +370,14 @@ sub taxline { $taxable_charged -= $cust_bill_pkg->setup; } - if ( ($part_pkg->recurtax eq 'Y' or $self->recurtax eq 'Y') - and $cust_bill_pkg->recur > 0 and $taxable_charged > 0 ) { + + my $recur_exempt = ( ($part_fee and not $part_fee->taxable) + or ($part_pkg and $part_pkg->recurtax) + or $self->recurtax ); + + if ( $recur_exempt + and $cust_bill_pkg->recur > 0 + and $taxable_charged > 0 ) { push @new_exemptions, FS::cust_tax_exempt_pkg->new({ amount => $cust_bill_pkg->recur, @@ -363,23 +388,41 @@ sub taxline { } if ( $self->exempt_amount && $self->exempt_amount > 0 - and $taxable_charged > 0 ) { - #my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5]; - my ($mon,$year) = - (localtime( $cust_bill_pkg->sdate || $invoice_date ) )[4,5]; - $mon++; - $year += 1900; - my $freq = $cust_bill_pkg->freq; - unless ($freq) { - $freq = $part_pkg->freq || 1; # less trustworthy fallback - } - if ( $freq !~ /(\d+)$/ ) { - $dbh->rollback if $oldAutoCommit; - return "daily/weekly package definitions not (yet?)". - " compatible with monthly tax exemptions"; + and $taxable_charged > 0 + and $cust_main ) { + + # XXX monthly exemptions currently don't work on quotations + + # If the billing period extends across multiple calendar months, + # there may be several months of exemption available. + my $sdate = $cust_bill_pkg->sdate || $invoice_time; + my $start_month = (localtime($sdate))[4] + 1; + my $start_year = (localtime($sdate))[5] + 1900; + my $edate = $cust_bill_pkg->edate || $invoice_time; + my $end_month = (localtime($edate))[4] + 1; + my $end_year = (localtime($edate))[5] + 1900; + + # If the partial last month + partial first month <= one month, + # don't use the exemption in the last month + # (unless the last month is also the first month, e.g. one-time + # charges) + if ( (localtime($sdate))[3] >= (localtime($edate))[3] + and ($start_month != $end_month or $start_year != $end_year) + ) { + $end_month--; + if ( $end_month == 0 ) { + $end_year--; + $end_month = 12; + } } - my $taxable_per_month = - sprintf("%.2f", $taxable_charged / $freq ); + + # number of months of exemption available + my $freq = ($end_month - $start_month) + + ($end_year - $start_year) * 12 + + 1; + + # divide equally among all of them + my $permonth = sprintf('%.2f', $taxable_charged / $freq); #call the whole thing off if this customer has any old #exemption records... @@ -392,9 +435,15 @@ sub taxline { 'run bin/fs-migrate-cust_tax_exempt?'; } - foreach my $which_month ( 1 .. $freq ) { - - #maintain the new exemption table now + my ($mon, $year) = ($start_month, $start_year); + while ($taxable_charged > 0.005 and + ($year < $end_year or + ($year == $end_year and $mon <= $end_month) + ) + ) { + + # find the sum of the exemption used by this customer, for this tax, + # in this month my $sql = " SELECT SUM(amount) FROM cust_tax_exempt_pkg @@ -408,7 +457,7 @@ sub taxline { "; my $sth = dbh->prepare($sql) or do { $dbh->rollback if $oldAutoCommit; - return "fatal: can't lookup exising exemption: ". dbh->errstr; + return "fatal: can't lookup existing exemption: ". dbh->errstr; }; $sth->execute( $custnum, @@ -417,10 +466,11 @@ sub taxline { $mon, ) or do { $dbh->rollback if $oldAutoCommit; - return "fatal: can't lookup exising exemption: ". dbh->errstr; + return "fatal: can't lookup existing exemption: ". dbh->errstr; }; my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0; + # add any exemption we're already using for another line item foreach ( grep { $_->taxnum == $self->taxnum && $_->exempt_monthly eq 'Y' && $_->month == $mon && @@ -430,13 +480,15 @@ sub taxline { { $existing_exemption += $_->amount; } - + my $remaining_exemption = $self->exempt_amount - $existing_exemption; if ( $remaining_exemption > 0 ) { - my $addl = $remaining_exemption > $taxable_per_month - ? $taxable_per_month + my $addl = $remaining_exemption > $permonth + ? $permonth : $remaining_exemption; + $addl = $taxable_charged if $addl > $taxable_charged; + push @new_exemptions, FS::cust_tax_exempt_pkg->new({ amount => sprintf('%.2f', $addl), exempt_monthly => 'Y', @@ -445,7 +497,6 @@ sub taxline { }); $taxable_charged -= $addl; } - last if $taxable_charged < 0.005; # if they're using multiple months of exemption for a multi-month # package, then record the exemptions in separate months $mon++; @@ -454,8 +505,8 @@ sub taxline { $year++; } - } #foreach $which_month - } # if exempt_amount + } + } # if exempt_amount and $cust_main $_->taxnum($self->taxnum) foreach @new_exemptions; @@ -472,7 +523,7 @@ sub taxline { 'taxtype' => ref($self), 'cents' => $this_tax_cents, 'pkgnum' => $cust_bill_pkg->pkgnum, - 'locationnum' => $cust_bill_pkg->cust_pkg->tax_locationnum, + 'locationnum' => $locationnum, 'taxable_cust_bill_pkg' => $cust_bill_pkg, 'tax_cust_bill_pkg' => $tax_item, }); @@ -628,6 +679,31 @@ END } +sub _upgrade_data { + my $class = shift; + # assume taxes in Washington with district numbers, and null name, or + # named 'sales tax', are looked up via the wa_sales method. mark them. + my $journal = 'cust_main_county__source_wa_sales'; + if (!FS::upgrade_journal->is_done($journal)) { + my @taxes = qsearch({ + 'table' => 'cust_main_county', + 'extra_sql' => " WHERE tax > 0 AND country = 'US' AND state = 'WA'". + " AND district IS NOT NULL AND ( taxname IS NULL OR ". + " taxname ~* 'sales tax' )", + }); + if ( @taxes ) { + warn "Flagging Washington state sales taxes: ".scalar(@taxes)." records.\n"; + foreach (@taxes) { + $_->set('source', 'wa_sales'); + my $error = $_->replace; + die $error if $error; + } + } + FS::upgrade_journal->set_done($journal); + } + ''; +} + =back =head1 BUGS