X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_bill_pkg_tax_location.pm;h=4a641bec6a14185f9e78f3c149e7bea6c5b42684;hp=44dd6e3c4c138c66518840a6485dd08eed19f2db;hb=09af85fc0e7a48392c930c9672a99448cf9630d4;hpb=fb4ab1073f0d15d660c6cdc4e07afebf68ef3924 diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm index 44dd6e3c4..4a641bec6 100644 --- a/FS/FS/cust_bill_pkg_tax_location.pm +++ b/FS/FS/cust_bill_pkg_tax_location.pm @@ -1,14 +1,15 @@ package FS::cust_bill_pkg_tax_location; +use base qw( FS::Record ); use strict; -use base qw( FS::Record ); -use FS::Record qw( qsearch qsearchs ); +use List::Util qw(sum min); +use FS::Record qw( dbh qsearch qsearchs ); use FS::cust_bill_pkg; use FS::cust_pkg; -use FS::cust_location; use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; use FS::cust_main_county; +use FS::Log; =head1 NAME @@ -65,6 +66,11 @@ locationnum amount +=item taxable_billpkgnum + +The billpkgnum of the L that this tax was charged on. +It may specifically be on any portion of that line item (setup, recurring, +or a usage class). =back @@ -116,9 +122,10 @@ sub check { || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg', 'billpkgnum' ) || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype || $self->ut_enum('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] ) - || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' ) + || $self->ut_number('pkgnum', 'cust_pkg', 'pkgnum' ) || $self->ut_foreign_key('locationnum', 'cust_location', 'locationnum' ) || $self->ut_money('amount') + || $self->ut_foreign_keyn('taxable_billpkgnum', 'cust_bill_pkg', 'billpkgnum') ; return $error if $error; @@ -127,24 +134,27 @@ sub check { =item cust_bill_pkg -Returns the associated cust_bill_pkg object +Returns the associated cust_bill_pkg object (i.e. the tax charge). -=cut +=item taxable_cust_bill_pkg -sub cust_bill_pkg { - my $self = shift; - qsearchs( 'cust_bill_pkg', { 'billpkgnum' => $self->billpkgnum } ); -} +Returns the cust_bill_pkg object for the I charge. =item cust_location Returns the associated cust_location object +=item taxname + +Returns the tax name (for populating the itemdesc field). + =cut -sub cust_location { +sub taxname { my $self = shift; - qsearchs( 'cust_location', { 'locationnum' => $self->locationnum } ); + my $cust_main_county = FS::cust_main_county->by_key($self->taxnum) + or return ''; + $cust_main_county->taxname || 'Tax'; } =item desc @@ -202,18 +212,278 @@ sub cust_credit_bill_pkg { sub cust_main_county { my $self = shift; - my $result; - if ( $self->taxtype eq 'FS::cust_main_county' ) { - $result = qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } ); + return '' unless $self->taxtype eq 'FS::cust_main_county'; + qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } ); +} + +sub _upgrade_data { + eval { + use FS::queue; + use Date::Parse 'str2time'; + }; + my $class = shift; + my $upgrade = 'tax_location_taxable_billpkgnum'; + return if FS::upgrade_journal->is_done($upgrade); + my $job = FS::queue->new({ job => + 'FS::cust_bill_pkg_tax_location::upgrade_taxable_billpkgnum' + }); + $job->insert($class, 's' => str2time('2012-01-01')); + FS::upgrade_journal->set_done($upgrade); +} + +sub upgrade_taxable_billpkgnum { + # Associate these records to the correct taxable line items. + # The cust_bill_pkg upgrade now does this also for pre-3.0 records that + # aren't broken out by pkgnum, so we only need to deal with the case of + # multiple line items for the same pkgnum. + # Despite appearances, this has almost no relation to the upgrade in + # FS::cust_bill_pkg. + + my ($class, %opt) = @_; + my $dbh = dbh; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $log = FS::Log->new('upgrade_taxable_billpkgnum'); + + my $date_where = ''; + if ( $opt{s} ) { + $date_where .= " AND cust_bill._date >= $opt{s}"; } + if ( $opt{e} ) { + $date_where .= " AND cust_bill._date < $opt{e}"; + } + + my @need_to_upgrade = qsearch({ + select => 'cust_bill_pkg_tax_location.*', + table => 'cust_bill_pkg_tax_location', + hashref => { taxable_billpkgnum => '' }, + addl_from => 'JOIN cust_bill_pkg USING (billpkgnum)'. + 'JOIN cust_bill USING (invnum)', + extra_sql => $date_where, + }); + $log->info('Starting upgrade of '.scalar(@need_to_upgrade). + ' cust_bill_pkg_tax_location records.'); + + # keys are billpkgnums + my %cust_bill_pkg; + my %tax_location; + foreach (@need_to_upgrade) { + my $tax_billpkgnum = $_->billpkgnum; + $cust_bill_pkg{ $tax_billpkgnum } ||= FS::cust_bill_pkg->by_key($tax_billpkgnum); + $tax_location{ $tax_billpkgnum } ||= []; + push @{ $tax_location{ $tax_billpkgnum } }, $_; + } + + TAX_ITEM: foreach my $tax_item (values %cust_bill_pkg) { + my $tax_locations = $tax_location{ $tax_item->billpkgnum }; + my $invnum = $tax_item->invnum; + my $cust_bill = FS::cust_bill->by_key($tax_item->invnum); + my %tax_on_pkg; # keys are tax identifiers + TAX_LOCATION: foreach my $tax_location (@$tax_locations) { + # recapitulate the "cust_main_county $taxnum $pkgnum" tax identifier, + # in a way + my $taxid = join(' ', + $tax_location->taxtype, + $tax_location->taxnum, + $tax_location->pkgnum, + $tax_location->locationnum + ); + $tax_on_pkg{$taxid} ||= []; + push @{ $tax_on_pkg{$taxid} }, $tax_location; + } + PKGNUM: foreach my $taxid (keys %tax_on_pkg) { + my ($taxtype, $taxnum, $pkgnum, $locationnum) = split(' ', $taxid); + $log->info("tax#$taxnum, pkg#$pkgnum", object => $cust_bill); + my @pkg_items = $cust_bill->cust_bill_pkg_pkgnum($pkgnum); + if (!@pkg_items) { + # then how is there tax on it? should never happen + $log->error("no line items with pkg#$pkgnum", object => $cust_bill); + next PKGNUM; + } + my $pkg_amount = 0; + foreach my $pkg_item (@pkg_items) { + # find the taxable amount of each one + my $amount = $pkg_item->setup + $pkg_item->recur; + # subtract any exemptions that apply to this taxdef + foreach (qsearch('cust_tax_exempt_pkg', { + taxnum => $taxnum, + billpkgnum => $pkg_item->billpkgnum + }) ) + { + $amount -= $_->amount; + } + $pkg_item->set('amount' => $pkg_item->setup + $pkg_item->recur); + $pkg_amount += $amount; + } #$pkg_item + next PKGNUM if $pkg_amount == 0; # probably because it's fully exempted + # now sort them descending by taxable amount + @pkg_items = sort { $b->amount <=> $a->amount } + @pkg_items; + # and do the same with the tax links + # (there should be one per taxed item) + my @tax_links = sort { $b->amount <=> $a->amount } + @{ $tax_on_pkg{$taxid} }; + + if (scalar(@tax_links) == scalar(@pkg_items)) { + # the relatively simple case: they match 1:1 + for my $i (0 .. scalar(@tax_links) - 1) { + $tax_links[$i]->set('taxable_billpkgnum', + $pkg_items[$i]->billpkgnum); + my $error = $tax_links[$i]->replace; + if ( $error ) { + $log->error("failed to set taxable_billpkgnum in tax on pkg#$pkgnum", + object => $cust_bill); + next PKGNUM; + } + } #for $i + } else { + # the more complicated case + $log->warning("mismatched charges and tax links in pkg#$pkgnum", + object => $cust_bill); + my $tax_amount = sum(map {$_->amount} @tax_links); + # remove all tax link records and recreate them to be 1:1 with + # taxable items + my (%billpaynum, %creditbillnum); + my $link_type; + foreach my $tax_link (@tax_links) { + $link_type ||= ref($tax_link); + my $error = $tax_link->delete; + if ( $error ) { + $log->error("error unlinking tax#$taxnum pkg#$pkgnum", + object => $cust_bill); + next PKGNUM; + } + my $pkey = $tax_link->primary_key; + # also remove all applications that reference this tax link + # (they will be applications to the tax item) + my %hash = ($pkey => $tax_link->get($pkey)); + foreach (qsearch('cust_bill_pay_pkg', \%hash)) { + $billpaynum{$_->billpaynum} += $_->amount; + my $error = $_->delete; + die "error unapplying payment: $error" if ( $error ); + } + foreach (qsearch('cust_credit_bill_pkg', \%hash)) { + $creditbillnum{$_->creditbillnum} += $_->amount; + my $error = $_->delete; + die "error unapplying credit: $error" if ( $error ); + } + } + @tax_links = (); + my $cents_remaining = int(100 * $tax_amount); + foreach my $pkg_item (@pkg_items) { + my $cents = int(100 * $pkg_item->amount * $tax_amount / $pkg_amount); + my $tax_link = $link_type->new({ + taxable_billpkgnum => $pkg_item->billpkgnum, + billpkgnum => $tax_item->billpkgnum, + taxnum => $taxnum, + taxtype => $taxtype, + pkgnum => $pkgnum, + locationnum => $locationnum, + cents => $cents, + }); + push @tax_links, $tax_link; + $cents_remaining -= $cents; + } + my $nlinks = scalar @tax_links; + my $i = 0; + while ($cents_remaining) { + $tax_links[$i % $nlinks]->set('cents' => + $tax_links[$i % $nlinks]->cents + 1 + ); + $cents_remaining--; + $i++; + } + foreach my $tax_link (@tax_links) { + $tax_link->set('amount' => sprintf('%.2f', $tax_link->cents / 100)); + my $error = $tax_link->insert; + if ( $error ) { + $log->error("error relinking tax#$taxnum pkg#$pkgnum", + object => $cust_bill); + next PKGNUM; + } + } + + $i = 0; + my $error; + my $left = 0; # the amount "left" on the last tax link after + # applying payments, but before credits, so that + # it can receive both a payment and a credit if + # necessary + # reapply payments/credits...this sucks + foreach my $billpaynum (keys %billpaynum) { + my $pay_amount = $billpaynum{$billpaynum}; + while ($i < $nlinks and $pay_amount > 0) { + my $this_amount = min($pay_amount, $tax_links[$i]->amount); + $left = $tax_links[$i]->amount - $this_amount; + my $app = FS::cust_bill_pay_pkg->new({ + billpaynum => $billpaynum, + billpkgnum => $tax_links[$i]->billpkgnum, + billpkgtaxlocationnum => $tax_links[$i]->billpkgtaxlocationnum, + amount => $this_amount, + setuprecur => 'setup', + # sdate/edate are null + }); + my $error ||= $app->insert; + $pay_amount -= $this_amount; + $i++ if $left == 0; + } + } + foreach my $creditbillnum (keys %creditbillnum) { + my $credit_amount = $creditbillnum{$creditbillnum}; + while ($i < $nlinks and $credit_amount > 0) { + my $this_amount = min($left, $credit_amount, $tax_links[$i]->amount); + $left = $credit_amount * 2; # just so it can't be selected twice + $i++ if $this_amount == $left + or $this_amount == $tax_links[$i]->amount; + my $app = FS::cust_credit_bill_pkg->new({ + creditbillnum => $creditbillnum, + billpkgnum => $tax_links[$i]->billpkgnum, + billpkgtaxlocationnum => $tax_links[$i]->billpkgtaxlocationnum, + amount => $this_amount, + setuprecur => 'setup', + # sdate/edate are null + }); + my $error ||= $app->insert; + $credit_amount -= $this_amount; + } + } + if ( $error ) { + # we've just unapplied a bunch of stuff, so if it won't reapply + # we really need to revert the whole transaction + die "error reapplying payments/credits: $error; upgrade halted"; + } + } # scalar(@tax_links) ?= scalar(@pkg_items) + } #taxnum/pkgnum + } #TAX_ITEM + + $log->info('finish'); + + $dbh->commit if $oldAutoCommit; + return; } +=cut + =back =head1 BUGS -The presense of FS::cust_main_county::delete makes the cust_main_county method -unreliable +The presence of FS::cust_main_county::delete makes the cust_main_county method +unreliable. + +Pre-3.0 versions of Freeside would only create one cust_bill_pkg_tax_location +per tax definition (taxtype/taxnum) per invoice. The pkgnum and locationnum +fields were arbitrarily set to those of the first line item subject to the +tax. This created problems if the tax contribution of each line item ever +needed to be determined (for example, when applying credits). For several +months in 2012, this was changed to create one record per tax definition +per I per invoice, which was still not specific enough to identify +a line item. + +The current behavior is to create one record per tax definition per taxable +line item, and to store the billpkgnum of the taxed line item in the record. +The upgrade will try to convert existing records to the new format, but this +is not perfectly reliable. =head1 SEE ALSO