1 package FS::TaxEngine::cch;
5 use base 'FS::TaxEngine';
6 use FS::Record qw(dbh qsearch qsearchs);
11 FS::TaxEngine::cch CCH published tax tables. Uses multiple tables:
12 - tax_rate: definition of specific taxes, based on tax class and geocode.
13 - cust_tax_location: definition of geocodes, using zip+4 codes.
14 - tax_class: definition of tax classes.
15 - part_pkg_taxproduct: definition of taxable products (foreign key in
16 part_pkg.taxproductnum and the "usage_taxproductnum_*" part_pkg options).
17 The 'taxproduct' string in this table can implicitly include other
19 - part_pkg_taxrate: links (geocode, taxproductnum) of a sold product to a
20 tax class. Many records here have partial-length geocodes which act
22 - part_pkg_taxoverride: manual link from a part_pkg to a specific tax class.
31 my ($self, $cust_bill_pkg, %options) = @_;
33 my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
34 my $location = $options{location} || $cust_bill_pkg->tax_location;
36 push @{ $self->{items} }, $cust_bill_pkg;
38 my $conf = FS::Conf->new;
41 push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
43 push @classes, 'setup' if ($cust_bill_pkg->setup && !$self->{cancel});
44 push @classes, 'recur' if ($cust_bill_pkg->recur && !$self->{cancel});
48 my $exempt = $conf->exists('cust_class-tax_exempt')
49 ? ( $self->cust_class ? $self->cust_class->tax : '' )
50 : $self->{cust_main}->tax;
51 # standardize this just to be sure
52 $exempt = ($exempt eq 'Y') ? 'Y' : '';
56 foreach my $class (@classes) {
57 my $err_or_ref = $self->_gather_taxes( $part_item, $class, $location );
58 return $err_or_ref unless ref($err_or_ref);
59 $taxes_for_class{$class} = $err_or_ref;
61 unless (exists $taxes_for_class{''}) {
62 my $err_or_ref = $self->_gather_taxes( $part_item, '', $location );
63 return $err_or_ref unless ref($err_or_ref);
64 $taxes_for_class{''} = $err_or_ref;
69 my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; # grrr
70 foreach my $key (keys %tax_cust_bill_pkg) {
71 # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
72 # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
74 # $taxes_for_class{$key} is an arrayref of tax_rate objects that
75 # apply to $key-class charges.
76 my @taxes = @{ $taxes_for_class{$key} || [] };
77 my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
79 my %localtaxlisthash = ();
80 foreach my $tax ( @taxes ) {
82 my $taxnum = $tax->taxnum;
83 $self->{taxes}{$taxnum} ||= [ $tax ];
84 push @{ $self->{taxes}{$taxnum} }, $tax_cust_bill_pkg;
86 $localtaxlisthash{ $taxnum } ||= [ $tax ];
87 push @{ $localtaxlisthash{$taxnum} }, $tax_cust_bill_pkg;
91 warn "finding taxed taxes...\n" if $DEBUG > 2;
92 foreach my $taxnum ( keys %localtaxlisthash ) {
93 my $tax_object = shift @{ $localtaxlisthash{$taxnum} };
95 foreach my $tot ( $tax_object->tax_on_tax( $location ) ) {
96 my $totnum = $tot->taxnum;
98 # I'm not sure why, but for some reason we only add ToT if that
99 # tax_rate already applies to a non-tax item on the same invoice.
100 next unless exists( $localtaxlisthash{ $totnum } );
101 warn "adding #$totnum to taxed taxes\n" if $DEBUG > 2;
102 # calculate the tax amount that the tax_on_tax will apply to
104 $self->taxline( 'tax' => $tax_object,
105 'sales' => $localtaxlisthash{$taxnum}
107 return $taxline unless ref $taxline;
108 # and append it to the list of taxable items
109 $self->{taxes}->{$totnum} ||= [ $tot ];
110 push @{ $self->{taxes}->{$totnum} }, $taxline->setup;
112 } # foreach $tot (tax-on-tax)
114 } # foreach $key (i.e. usage class)
117 sub _gather_taxes { # interface for this sucks
119 my $part_item = shift;
121 my $location = shift;
123 my $geocode = $location->geocode('cch');
125 my @taxes = $part_item->tax_rates('cch', $geocode, $class);
128 join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
136 # FS::tax_rate::taxline() ridiculously returns a description and amount
137 # instead of a real line item. Fix that here.
139 # XXX eventually move the code from tax_rate to here
140 # but that's not necessary yet
141 my ($self, %opt) = @_;
142 my $tax_object = $opt{tax};
143 my $taxables = $opt{sales};
144 my $hashref = $tax_object->taxline_cch($taxables);
145 return $hashref unless ref $hashref; # it's an error message
147 my $tax_amount = sprintf('%.2f', $hashref->{amount});
148 my $tax_item = FS::cust_bill_pkg->new({
149 'itemdesc' => $hashref->{name},
154 'setup' => $tax_amount,
156 my $tax_link = FS::cust_bill_pkg_tax_rate_location->new({
157 'taxnum' => $tax_object->taxnum,
158 'taxtype' => ref($tax_object), #redundant
159 'amount' => $tax_amount,
160 'locationtaxid' => $tax_object->location,
161 'taxratelocationnum' =>
162 $tax_object->tax_rate_location->taxratelocationnum,
163 'tax_cust_bill_pkg' => $tax_item,
164 # XXX still need to get taxable_cust_bill_pkg in here
165 # but that requires messing around in the taxline code
167 $tax_item->set('cust_bill_pkg_tax_rate_location', [ $tax_link ]);
172 sub cust_tax_locations {
174 my $location = shift;
175 $location = FS::cust_location->new($location) if ref($location) eq 'HASH';
177 # limit to CCH zip code prefix records, not zip+4 range records
178 my $hashref = { 'data_vendor' => 'cch-zip' };
179 if ( $location->country eq 'CA' ) {
180 # weird CCH convention: treat Canadian provinces as localities, using
181 # their one-letter postal codes.
182 $hashref->{zip} = substr($location->zip, 0, 1);
183 } elsif ( $location->country eq 'US' ) {
184 $hashref->{zip} = substr($location->zip, 0, 5);
189 return qsearch('cust_tax_location', $hashref);
196 manual_tax_location => 1,
197 rate_table => 'tax_rate',
198 link_table => 'cust_bill_pkg_tax_rate_location',