From 7516e3da0f17eeecba27219ef96a8b5f46af2083 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 31 Oct 2014 15:45:50 -0700 Subject: tax engine refactoring for Avalara and Billsoft tax vendors, #25718 --- FS/FS/TaxEngine.pm | 239 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 FS/FS/TaxEngine.pm (limited to 'FS/FS/TaxEngine.pm') diff --git a/FS/FS/TaxEngine.pm b/FS/FS/TaxEngine.pm new file mode 100644 index 0000000..a146c54 --- /dev/null +++ b/FS/FS/TaxEngine.pm @@ -0,0 +1,239 @@ +package FS::TaxEngine; + +use strict; +use vars qw( $DEBUG ); +use FS::Conf; +use FS::Record qw(qsearch qsearchs); + +$DEBUG = 0; + +=head1 NAME + +FS::TaxEngine - Base class for tax calculation engines. + +=head1 USAGE + +1. At the start of creating an invoice, create an FS::TaxEngine object. +2. Each time a sale item is added to the invoice, call C on the + TaxEngine. + +- If the TaxEngine is "batch" style (Billsoft): +3. Set the "pending" flag on the invoice. +4. Insert the invoice and its line items. +5. After creating all invoices for the day, call + FS::TaxEngine::process_tax_batch. This will create the tax items for + all of the pending invoices, clear the "pending" flag, and call + C on each of the billed customers. + +- If not (the internal tax system, CCH): +3. After adding all sale items, call C on the TaxEngine to + produce a list of tax line items. +4. Append the tax line items to the invoice. +5. Insert the invoice. + +=head1 CLASS METHODS + +=over 4 + +=item new 'cust_main' => CUST_MAIN, 'invoice_time' => TIME, OPTIONS... + +Creates an L object. The subclass will be chosen by the +'enable_taxproducts' configuration setting. + +CUST_MAIN and TIME are required. OPTIONS can include "cancel" => 1 to +indicate that the package is being billed on cancellation. + +=cut + +sub new { + my $class = shift; + my %opt = @_; + if ($class eq 'FS::TaxEngine') { + my $conf = FS::Conf->new; + my $subclass = $conf->config('enable_taxproducts') || 'internal'; + $class .= "::$subclass"; + local $@; + eval "use $class"; + die "couldn't load $class: $@\n" if $@; + } + my $self = { items => [], taxes => {}, %opt }; + bless $self, $class; +} + +=item info + +Returns a hashref of metadata about this tax method, including: +- batch: whether this is a batch-style engine (requires different usage) +- override: whether this engine uses tax overrides +- manual_tax_location: whether this engine requires the user to select a "tax + location" separate from the address/city/state/zip fields +- rate_table: the table that stores the tax rates + (the 'taxline' method of that class will be used to calculate line-item + taxes) +- link_table: the table that links L records for taxes + to the C entry that generated them, and to the item they + represent tax on. + +=back + +=head1 METHODS + +=over 4 + +=item add_sale CUST_BILL_PKG + +Adds the CUST_BILL_PKG object as a taxable sale on this invoice. + +=item calculate_taxes CUST_BILL + +Calculates the taxes on the taxable sales and returns a list of +L objects to add to the invoice. There is a base +implementation of this, which calls the C method to calculate +each individual tax. + +=cut + +sub calculate_taxes { + my $self = shift; + my $conf = FS::Conf->new; + + my $cust_bill = shift; + + my @tax_line_items; + # keys are tax names (as printed on invoices / itemdesc ) + # values are arrayrefs of taxlines + my %taxname; + + # keys are taxnums + # values are (cumulative) amounts + my %tax_amount; + + # keys are taxnums + # values are arrayrefs of cust_tax_exempt_pkg objects + my %tax_exemption; + + # For each distinct tax rate definition, calculate the tax and exemptions. + foreach my $taxnum ( keys %{ $self->{taxes} } ) { + + my $taxables = $self->{taxes}{$taxnum}; + my $tax_object = shift @$taxables; + # $tax_object is a cust_main_county or tax_rate + # (with billpkgnum, pkgnum, locationnum set) + # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects + # (setup, recurring, usage classes) + + my $taxline = $self->taxline('tax' => $tax_object, 'sales' => $taxables); + # taxline methods are now required to return real line items + # with their link records + die $taxline unless ref($taxline); + + push @{ $taxname{ $taxline->itemdesc } }, $taxline; + + } #foreach $taxnum + + my $link_table = $self->info->{link_table}; + # For each distinct tax name (the values set as $taxline->itemdesc), + # create a consolidated tax item with the total amount and all the links + # of all tax items that share that name. + foreach my $taxname ( keys %taxname ) { + my @tax_links; + my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({ + 'invnum' => $cust_bill->invnum, + 'pkgnum' => 0, + 'recur' => 0, + 'sdate' => '', + 'edate' => '', + 'itemdesc' => $taxname, + $link_table => \@tax_links, + }); + + my $tax_total = 0; + warn "adding $taxname\n" if $DEBUG > 1; + + foreach my $taxitem ( @{ $taxname{$taxname} } ) { + # then we need to transfer the amount and the links from the + # line item to the new one we're creating. + $tax_total += $taxitem->setup; + foreach my $link ( @{ $taxitem->get($link_table) } ) { + $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg); + push @tax_links, $link; + } + } # foreach $taxitem + next unless $tax_total; + + # we should really neverround this up...I guess it's okay if taxline + # already returns amounts with 2 decimal places + $tax_total = sprintf('%.2f', $tax_total ); + $tax_cust_bill_pkg->set('setup', $tax_total); + + my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname, + 'disabled' => '', + }, + ); + + my @display = (); + if ( $pkg_category and + $conf->config('invoice_latexsummary') || + $conf->config('invoice_htmlsummary') + ) + { + my %hash = ( 'section' => $pkg_category->categoryname ); + push @display, new FS::cust_bill_pkg_display { type => 'S', %hash }; + } + $tax_cust_bill_pkg->set('display', \@display); + + push @tax_line_items, $tax_cust_bill_pkg; + } + + \@tax_line_items; +} + +=head1 CLASS METHODS + +=item cust_tax_locations LOCATION + +Given an L object (or a hash of location fields), +returns a list of all tax jurisdiction locations that could possibly +match it. This is meant for interactive use: the location editing UI +displays the candidate locations to the user so they can choose the +best match. + +=cut + +sub cust_tax_locations { + (); +} # shouldn't even get called unless info->{manual_tax_location} is true + +=item add_taxproduct DESCRIPTION + +If the module allows manually adding tax products (categories of taxable +items/services), this method will be called to do it. (If not, the UI in +browse/part_pkg_taxproduct/* should prevent adding an unlisted tax product. +That is the default behavior, so by default this method simply fails.) + +DESCRIPTION is the contents of the taxproduct_description form input, which +will normally be filled in by browse/part_pkg_taxproduct/*. + +Must return the newly inserted part_pkg_taxproduct object on success, or +a string on failure. + +=cut + +sub add_taxproduct { + my $class = shift; + "$class does not allow manually adding taxproducts"; +} + +=item transfer_batch (batch-style only) + +Submits the pending transaction batch for processing, receives the +results, and appends the calculated taxes to all invoices that were +included in the batch. Then clears their pending flags, and queues +a job to run C on each affected +customer. + +=back + +=cut + +1; -- cgit v1.1