--- /dev/null
+package FS::TaxEngine::cch;
+
+use strict;
+use vars qw( $DEBUG );
+use base 'FS::TaxEngine';
+use FS::Record qw(dbh qsearch qsearchs);
+use FS::Conf;
+
+=head1 SUMMARY
+
+FS::TaxEngine::cch CCH published tax tables. Uses multiple tables:
+- tax_rate: definition of specific taxes, based on tax class and geocode.
+- cust_tax_location: definition of geocodes, using zip+4 codes.
+- tax_class: definition of tax classes.
+- part_pkg_taxproduct: definition of taxable products (foreign key in
+ part_pkg.taxproductnum and the "usage_taxproductnum_*" part_pkg options).
+ The 'taxproduct' string in this table can implicitly include other
+ taxproducts.
+- part_pkg_taxrate: links (geocode, taxproductnum) of a sold product to a
+ tax class. Many records here have partial-length geocodes which act
+ as wildcards.
+- part_pkg_taxoverride: manual link from a part_pkg to a specific tax class.
+
+=cut
+
+$DEBUG = 0;
+
+my %part_pkg_cache;
+
+sub add_sale {
+ my ($self, $cust_bill_pkg, %options) = @_;
+
+ my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
+ my $location = $options{location} || $cust_bill_pkg->tax_location;
+
+ push @{ $self->{items} }, $cust_bill_pkg;
+
+ my $conf = FS::Conf->new;
+
+ my @classes;
+ push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
+ # debatable
+ push @classes, 'setup' if ($cust_bill_pkg->setup && !$self->{cancel});
+ push @classes, 'recur' if ($cust_bill_pkg->recur && !$self->{cancel});
+
+ my %taxes_for_class;
+
+ my $exempt = $conf->exists('cust_class-tax_exempt')
+ ? ( $self->cust_class ? $self->cust_class->tax : '' )
+ : $self->{cust_main}->tax;
+ # standardize this just to be sure
+ $exempt = ($exempt eq 'Y') ? 'Y' : '';
+
+ if ( !$exempt ) {
+
+ foreach my $class (@classes) {
+ my $err_or_ref = $self->_gather_taxes( $part_item, $class, $location );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes_for_class{$class} = $err_or_ref;
+ }
+ unless (exists $taxes_for_class{''}) {
+ my $err_or_ref = $self->_gather_taxes( $part_item, '', $location );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes_for_class{''} = $err_or_ref;
+ }
+
+ }
+
+ my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; # grrr
+ foreach my $key (keys %tax_cust_bill_pkg) {
+ # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
+ # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
+ # the line item.
+ # $taxes_for_class{$key} is an arrayref of tax_rate objects that
+ # apply to $key-class charges.
+ my @taxes = @{ $taxes_for_class{$key} || [] };
+ my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
+
+ my %localtaxlisthash = ();
+ foreach my $tax ( @taxes ) {
+
+ my $taxnum = $tax->taxnum;
+ $self->{taxes}{$taxnum} ||= [ $tax ];
+ push @{ $self->{taxes}{$taxnum} }, $tax_cust_bill_pkg;
+
+ $localtaxlisthash{ $taxnum } ||= [ $tax ];
+ push @{ $localtaxlisthash{$taxnum} }, $tax_cust_bill_pkg;
+
+ }
+
+ warn "finding taxed taxes...\n" if $DEBUG > 2;
+ foreach my $taxnum ( keys %localtaxlisthash ) {
+ my $tax_object = shift @{ $localtaxlisthash{$taxnum} };
+
+ foreach my $tot ( $tax_object->tax_on_tax( $location ) ) {
+ my $totnum = $tot->taxnum;
+
+ # I'm not sure why, but for some reason we only add ToT if that
+ # tax_rate already applies to a non-tax item on the same invoice.
+ next unless exists( $localtaxlisthash{ $totnum } );
+ warn "adding #$totnum to taxed taxes\n" if $DEBUG > 2;
+ # calculate the tax amount that the tax_on_tax will apply to
+ my $taxline =
+ $self->taxline( 'tax' => $tax_object,
+ 'sales' => $localtaxlisthash{$taxnum}
+ );
+ return $taxline unless ref $taxline;
+ # and append it to the list of taxable items
+ $self->{taxes}->{$totnum} ||= [ $tot ];
+ push @{ $self->{taxes}->{$totnum} }, $taxline->setup;
+
+ } # foreach $tot (tax-on-tax)
+ } # foreach $tax
+ } # foreach $key (i.e. usage class)
+}
+
+sub _gather_taxes { # interface for this sucks
+ my $self = shift;
+ my $part_item = shift;
+ my $class = shift;
+ my $location = shift;
+
+ my $geocode = $location->geocode('cch');
+
+ my @taxes = $part_item->tax_rates('cch', $geocode, $class);
+
+ warn "Found taxes ".
+ join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
+ if $DEBUG;
+
+ \@taxes;
+
+}
+
+sub taxline {
+ # FS::tax_rate::taxline() ridiculously returns a description and amount
+ # instead of a real line item. Fix that here.
+ #
+ # XXX eventually move the code from tax_rate to here
+ # but that's not necessary yet
+ my ($self, %opt) = @_;
+ my $tax_object = $opt{tax};
+ my $taxables = $opt{sales};
+ my $hashref = $tax_object->taxline_cch($taxables);
+ return $hashref unless ref $hashref; # it's an error message
+
+ my $tax_amount = sprintf('%.2f', $hashref->{amount});
+ my $tax_item = FS::cust_bill_pkg->new({
+ 'itemdesc' => $hashref->{name},
+ 'pkgnum' => 0,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'setup' => $tax_amount,
+ });
+ my $tax_link = FS::cust_bill_pkg_tax_rate_location->new({
+ 'taxnum' => $tax_object->taxnum,
+ 'taxtype' => ref($tax_object), #redundant
+ 'amount' => $tax_amount,
+ 'locationtaxid' => $tax_object->location,
+ 'taxratelocationnum' =>
+ $tax_object->tax_rate_location->taxratelocationnum,
+ 'tax_cust_bill_pkg' => $tax_item,
+ # XXX still need to get taxable_cust_bill_pkg in here
+ # but that requires messing around in the taxline code
+ });
+ $tax_item->set('cust_bill_pkg_tax_rate_location', [ $tax_link ]);
+
+ return $tax_item;
+}
+
+sub cust_tax_locations {
+ my $class = shift;
+ my $location = shift;
+ $location = FS::cust_location->new($location) if ref($location) eq 'HASH';
+
+ # limit to CCH zip code prefix records, not zip+4 range records
+ my $hashref = { 'data_vendor' => 'cch-zip' };
+ if ( $location->country eq 'CA' ) {
+ # weird CCH convention: treat Canadian provinces as localities, using
+ # their one-letter postal codes.
+ $hashref->{zip} = substr($location->zip, 0, 1);
+ } elsif ( $location->country eq 'US' ) {
+ $hashref->{zip} = substr($location->zip, 0, 5);
+ } else {
+ return ();
+ }
+
+ return qsearch('cust_tax_location', $hashref);
+}
+
+sub info {
+ +{
+ batch => 0,
+ override => 1,
+ manual_tax_location => 1,
+ rate_table => 'tax_rate',
+ link_table => 'cust_bill_pkg_tax_rate_location',
+ }
+}
+
+1;