summaryrefslogtreecommitdiff
path: root/FS/FS/TaxEngine.pm
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2014-10-31 15:45:50 -0700
committerMark Wells <mark@freeside.biz>2014-10-31 15:45:50 -0700
commit7516e3da0f17eeecba27219ef96a8b5f46af2083 (patch)
tree772fe13627910a7d0774871633697f2a4d1c6faf /FS/FS/TaxEngine.pm
parentf31a9212ab3835b815aa87a86cca3b19babcaaff (diff)
tax engine refactoring for Avalara and Billsoft tax vendors, #25718
Diffstat (limited to 'FS/FS/TaxEngine.pm')
-rw-r--r--FS/FS/TaxEngine.pm239
1 files changed, 239 insertions, 0 deletions
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<add_sale> 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<collect> on each of the billed customers.
+
+- If not (the internal tax system, CCH):
+3. After adding all sale items, call C<calculate_taxes> 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<FS::TaxEngine> 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<FS::cust_bill_pkg> records for taxes
+ to the C<rate_table> 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<FS::cust_bill_pkg> objects to add to the invoice. There is a base
+implementation of this, which calls the C<taxline> 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<FS::cust_location> 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<FS::cust_main::Billing::collect> on each affected
+customer.
+
+=back
+
+=cut
+
+1;