From faa92dd0fd0b875886378de7c91c59a4049f1168 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 11 Mar 2014 10:47:17 -0700 Subject: [PATCH] fix tax calculation on bundled packages, fallout from #25899 --- FS/FS/cust_bill_pkg.pm | 4 +- FS/FS/cust_credit.pm | 9 +-- bin/fix-missing-taxes | 151 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 bin/fix-missing-taxes diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 066ddf160..ef9c01a32 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -982,8 +982,8 @@ charge. If called on a tax line, returns nothing. sub part_X { my $self = shift; - if ( $self->override_pkgpart ) { - return FS::part_pkg->by_key($self->override_pkgpart); + if ( $self->pkgpart_override ) { + return FS::part_pkg->by_key($self->pkgpart_override); } elsif ( $self->pkgnum ) { return $self->cust_pkg->part_pkg; } elsif ( $self->feepart ) { diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 58bd475b1..156ba5fd6 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -22,6 +22,7 @@ use FS::cust_event; use FS::agent; use FS::sales; use FS::cust_credit_void; +use FS::cust_bill_pkg; use FS::upgrade_journal; $me = '[ FS::cust_credit ]'; @@ -689,7 +690,6 @@ Example: =cut #maybe i should just be an insert with extra args instead of a class method -use FS::cust_bill_pkg; sub credit_lineitems { my( $class, %arg ) = @_; my $curuser = $FS::CurrentUser::CurrentUser; @@ -815,9 +815,10 @@ sub credit_lineitems { # recalculate taxes with new amounts $taxlisthash{$invnum} ||= {}; - my $part_pkg = $cust_bill_pkg->part_pkg - if $cust_bill_pkg->pkgpart_override; - $cust_main->_handle_taxes( $taxlisthash{$invnum}, $cust_bill_pkg ); + if ( $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart ) { + $cust_main->_handle_taxes( $taxlisthash{$invnum}, $cust_bill_pkg ); + } # otherwise the item itself is a tax, and assume the caller knows + # what they're doing } ### diff --git a/bin/fix-missing-taxes b/bin/fix-missing-taxes new file mode 100644 index 000000000..62684cef4 --- /dev/null +++ b/bin/fix-missing-taxes @@ -0,0 +1,151 @@ +#!/usr/bin/perl + +=head1 fix-missing-taxes + +Usage: + fix-missing-taxes + +This script fixes CCH taxes that were calculated incorrectly due to a bug +in bundled package behavior in March 2014. For all invoices since the start +date, it recalculates taxes on all the non-tax items, generates credits for +taxes that were originally overcharged, and creates new invoices for taxes +that were undercharged. + +=cut + +use FS::UID qw(adminsuidsetup dbh); +use FS::cust_bill; +use FS::Record qw(qsearch); +use List::Util 'sum'; +use DateTime::Format::Natural; + +use strict; + +my $usage = "usage: fix-missing-taxes \n" ; +my $user = shift or die $usage; +adminsuidsetup($user); + +$FS::UID::AutoCommit = 0; + +my $parser = DateTime::Format::Natural->new; +my $dt = $parser->parse_datetime(shift); +die $usage unless $parser->success; + +my $date_filter = { _date => { op => '>=', value => $dt->epoch } }; +my @bills = qsearch('cust_bill', $date_filter); + +warn "Examining ".scalar(@bills)." invoices...\n"; + +my %new_tax_items; # custnum => [ new taxes to charge ] +my %cust_credits; # custnum => { tax billpkgnum => credit amount } + +foreach my $cust_bill (@bills) { + my $cust_main = $cust_bill->cust_main; + my $custnum = $cust_main->custnum; + my %taxlisthash; + my %old_tax; + my @nontax_items; + + foreach my $item ($cust_bill->cust_bill_pkg) { + if ( $item->pkgnum == 0 ) { + $old_tax{ $item->itemdesc } = $item; + } else { + $cust_main->_handle_taxes( \%taxlisthash, $item ); + push @nontax_items, $item; + } + } + my $tax_lines = $cust_main->calculate_taxes( + \@nontax_items, + \%taxlisthash, + $cust_bill->_date + ); + + my %new_tax = map { $_->itemdesc, $_ } @$tax_lines; + my %all = (%old_tax, %new_tax); + foreach my $taxname (keys(%all)) { + my $delta = sprintf('%.2f', + ($new_tax{$taxname} ? $new_tax{$taxname}->setup : 0) - + ($old_tax{$taxname} ? $old_tax{$taxname}->setup : 0) + ); + if ( $delta >= 0.01 ) { + # create a tax adjustment + $new_tax_items{$custnum} ||= []; + my $item = $new_tax{$taxname}; + foreach (@{ $item->cust_bill_pkg_tax_rate_location }) { + $_->set('amount', + sprintf('%.2f', $_->get('amount') * $delta / $item->get('setup')) + ); + } + $item->set('setup', $delta); + push @{ $new_tax_items{$custnum} }, $new_tax{$taxname}; + } elsif ( $delta <= -0.01 ) { + my $old_tax_item = $old_tax{$taxname}; + $cust_credits{$custnum} ||= {}; + $cust_credits{$custnum}{ $old_tax_item->billpkgnum } = -1 * $delta; + } + } +} + +my $num_bills = 0; +my $amt_billed = 0; +# create new invoices for those that need them +foreach my $custnum (keys %new_tax_items) { + my $cust_main = FS::cust_main->by_key($custnum); + my @cust_bill = $cust_main->cust_bill; + my $balance = $cust_main->balance; + my $previous_bill = $cust_bill[-1] if @cust_bill; + my $previous_balance = 0; + if ( $previous_bill ) { + $previous_balance = $previous_bill->billing_balance + + $previous_bill->charged; + } + + my $lines = $new_tax_items{$custnum}; + my $total = sum( map { $_->setup } @$lines); + my $new_bill = FS::cust_bill->new({ + 'custnum' => $custnum, + '_date' => $^T, + 'charged' => sprintf('%.2f', $total), + 'billing_balance' => $balance, + 'previous_balance' => $previous_balance, + 'cust_bill_pkg' => $lines, + }); + my $error = $new_bill->insert; + die "error billing cust#$custnum\n" if $error; + $num_bills++; + $amt_billed += $total; +} +print "Created $num_bills bills for a total of \$$amt_billed.\n"; + +my $credit_reason = FS::reason->new_or_existing( + reason => 'Sales tax correction', + class => 'R', + type => 'Credit', +); + +my $num_credits = 0; +my $amt_credited = 0; +# create credits for those that need them +foreach my $custnum (keys %cust_credits) { + my $cust_main = FS::cust_main->by_key($custnum); + my $lines = $cust_credits{$custnum}; + my @billpkgnums = keys %$lines; + my @amounts = values %$lines; + my $total = sprintf('%.2f', sum(@amounts)); + next if $total < 0.01; + my $error = FS::cust_credit->credit_lineitems( + 'custnum' => $custnum, + 'billpkgnums' => \@billpkgnums, + 'setuprecurs' => [ map {'setup'} @billpkgnums ], + 'amounts' => \@amounts,, + 'apply' => 1, + 'amount' => $total, + 'reasonnum' => $credit_reason->reasonnum, + ); + die "error crediting cust#$custnum\n" if $error; + $num_credits++; + $amt_credited += $total; +} +print "Created $num_credits credits for a total of \$$amt_credited.\n"; + +dbh->commit; -- 2.11.0