summaryrefslogtreecommitdiff
path: root/bin/fix-missing-taxes
blob: 62684cef47aec38dbe2510e5b7b594b87810ab0d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/perl

=head1 fix-missing-taxes

Usage:
  fix-missing-taxes <user> <start date>

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 <user> <start date>\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;