62684cef47aec38dbe2510e5b7b594b87810ab0d
[freeside.git] / bin / fix-missing-taxes
1 #!/usr/bin/perl
2
3 =head1 fix-missing-taxes
4
5 Usage:
6   fix-missing-taxes <user> <start date>
7
8 This script fixes CCH taxes that were calculated incorrectly due to a bug 
9 in bundled package behavior in March 2014.  For all invoices since the start
10 date, it recalculates taxes on all the non-tax items, generates credits for
11 taxes that were originally overcharged, and creates new invoices for taxes
12 that were undercharged.
13
14 =cut
15
16 use FS::UID qw(adminsuidsetup dbh);
17 use FS::cust_bill;
18 use FS::Record qw(qsearch);
19 use List::Util 'sum';
20 use DateTime::Format::Natural;
21
22 use strict;
23
24 my $usage = "usage: fix-missing-taxes <user> <start date>\n" ;
25 my $user = shift or die $usage;
26 adminsuidsetup($user);
27
28 $FS::UID::AutoCommit = 0;
29
30 my $parser = DateTime::Format::Natural->new;
31 my $dt = $parser->parse_datetime(shift);
32 die $usage unless $parser->success;
33
34 my $date_filter = { _date => { op => '>=', value => $dt->epoch } };
35 my @bills = qsearch('cust_bill', $date_filter);
36
37 warn "Examining ".scalar(@bills)." invoices...\n";
38
39 my %new_tax_items; # custnum => [ new taxes to charge ]
40 my %cust_credits; # custnum => { tax billpkgnum => credit amount }
41
42 foreach my $cust_bill (@bills) {
43   my $cust_main = $cust_bill->cust_main;
44   my $custnum = $cust_main->custnum;
45   my %taxlisthash;
46   my %old_tax;
47   my @nontax_items;
48
49   foreach my $item ($cust_bill->cust_bill_pkg) {
50     if ( $item->pkgnum == 0 ) {
51       $old_tax{ $item->itemdesc } = $item;
52     } else {
53       $cust_main->_handle_taxes( \%taxlisthash, $item );
54       push @nontax_items, $item;
55     }
56   }
57   my $tax_lines = $cust_main->calculate_taxes(
58     \@nontax_items,
59     \%taxlisthash,
60     $cust_bill->_date
61   );
62
63   my %new_tax = map { $_->itemdesc, $_ } @$tax_lines;
64   my %all = (%old_tax, %new_tax);
65   foreach my $taxname (keys(%all)) {
66     my $delta = sprintf('%.2f',
67                   ($new_tax{$taxname} ? $new_tax{$taxname}->setup : 0) -
68                   ($old_tax{$taxname} ? $old_tax{$taxname}->setup : 0)
69                 );
70     if ( $delta >= 0.01 ) {
71       # create a tax adjustment
72       $new_tax_items{$custnum} ||= [];
73       my $item = $new_tax{$taxname};
74       foreach (@{ $item->cust_bill_pkg_tax_rate_location }) {
75         $_->set('amount',
76           sprintf('%.2f', $_->get('amount') * $delta / $item->get('setup'))
77         );
78       }
79       $item->set('setup', $delta);
80       push @{ $new_tax_items{$custnum} }, $new_tax{$taxname};
81     } elsif ( $delta <= -0.01 ) {
82       my $old_tax_item = $old_tax{$taxname};
83       $cust_credits{$custnum} ||= {};
84       $cust_credits{$custnum}{ $old_tax_item->billpkgnum } = -1 * $delta;
85     }
86   }
87 }
88
89 my $num_bills = 0;
90 my $amt_billed = 0;
91 # create new invoices for those that need them
92 foreach my $custnum (keys %new_tax_items) {
93   my $cust_main = FS::cust_main->by_key($custnum);
94   my @cust_bill = $cust_main->cust_bill;
95   my $balance = $cust_main->balance;
96   my $previous_bill = $cust_bill[-1] if @cust_bill;
97   my $previous_balance = 0;
98   if ( $previous_bill ) {
99     $previous_balance = $previous_bill->billing_balance
100                       + $previous_bill->charged;
101   }
102
103   my $lines = $new_tax_items{$custnum};
104   my $total = sum( map { $_->setup } @$lines);
105   my $new_bill = FS::cust_bill->new({
106       'custnum'           => $custnum,
107       '_date'             => $^T,
108       'charged'           => sprintf('%.2f', $total),
109       'billing_balance'   => $balance,
110       'previous_balance'  => $previous_balance,
111       'cust_bill_pkg'     => $lines,
112   });
113   my $error = $new_bill->insert;
114   die "error billing cust#$custnum\n" if $error;
115   $num_bills++;
116   $amt_billed += $total;
117 }
118 print "Created $num_bills bills for a total of \$$amt_billed.\n";
119
120 my $credit_reason = FS::reason->new_or_existing( 
121   reason  => 'Sales tax correction',
122   class   => 'R',
123   type    => 'Credit',
124 );
125
126 my $num_credits = 0;
127 my $amt_credited = 0;
128 # create credits for those that need them
129 foreach my $custnum (keys %cust_credits) {
130   my $cust_main = FS::cust_main->by_key($custnum);
131   my $lines = $cust_credits{$custnum};
132   my @billpkgnums = keys %$lines;
133   my @amounts = values %$lines;
134   my $total = sprintf('%.2f', sum(@amounts));
135   next if $total < 0.01;
136   my $error = FS::cust_credit->credit_lineitems(
137     'custnum'     => $custnum,
138     'billpkgnums' => \@billpkgnums,
139     'setuprecurs' => [ map {'setup'} @billpkgnums ],
140     'amounts'     => \@amounts,,
141     'apply'       => 1,
142     'amount'      => $total,
143     'reasonnum'   => $credit_reason->reasonnum,
144   );
145   die "error crediting cust#$custnum\n" if $error;
146   $num_credits++;
147   $amt_credited += $total;
148 }
149 print "Created $num_credits credits for a total of \$$amt_credited.\n";
150
151 dbh->commit;