[freeside-commits] branch master updated. e5b2660aefd03bc4b32386e1c38c53814c002c8d

Ivan ivan at 420.am
Tue Jun 20 16:32:02 PDT 2017


The branch, master has been updated
       via  e5b2660aefd03bc4b32386e1c38c53814c002c8d (commit)
      from  b992d86235de93c9e3149b99d0f75c193b50e32f (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit e5b2660aefd03bc4b32386e1c38c53814c002c8d
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Tue Jun 20 16:32:00 2017 -0700

    Compliance Solutions <http://csilongwood.com/> integration, RT#75262

diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index a53fedb..5ba6fce 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2468,7 +2468,7 @@ and customer address. Include units.',
     'section'     => 'taxation',
     'description' => 'Tax data vendor you are using.',
     'type'        => 'select',
-    'select_enum' => [ '', 'cch', 'billsoft', 'avalara', 'suretax' ],
+    'select_enum' => [ '', 'cch', 'billsoft', 'avalara', 'suretax', 'compliance_solutions' ],
   },
 
   {
@@ -2541,6 +2541,20 @@ and customer address. Include units.',
     'per_agent'   => 1,
   },
 
+  {
+    'key'         => 'compliance_solutions-access_code',
+    'section'     => 'taxation',
+    'description' => 'Access code for <a href="http://csilongwood.com/">Compliance Solutions</a> tax rating service',
+    'type'        => 'text',
+  },
+  {
+    'key'         => 'compliance_solutions-regulatory_code',
+    'section'     => 'taxation',
+    'description' => 'Compliance Solutions regulatory status.',
+    'type'        => 'select',
+    'select_enum' => [ '', 'ILEC', 'IXC', 'CLEC', 'VOIP', 'ISP', 'Wireless' ],
+    'per_agent'   => 1,
+  },
 
   {
     'key'         => 'welcome_msgnum',
diff --git a/FS/FS/TaxEngine.pm b/FS/FS/TaxEngine.pm
index 4560142..e92bf76 100644
--- a/FS/FS/TaxEngine.pm
+++ b/FS/FS/TaxEngine.pm
@@ -124,6 +124,8 @@ sub calculate_taxes {
   if ( !@raw_taxlines ) {
     return;
   } elsif ( !ref $raw_taxlines[0] ) { # error message
+    #this isn't actually handled by our caller... better for make_taxlines to 
+    # die, that'll be caught be the eval around us in cust_main/Billing.pm
     return $raw_taxlines[0];
   }
 
@@ -296,7 +298,10 @@ a string on failure.
 
 sub add_taxproduct {
   my $class = shift;
-  "$class does not allow manually adding taxproducts";
+  #my $classname = ref($class);
+  #my $vendor = (split('::',$classname))[2];
+  my $vendor = ref($class) || $class;
+  "$vendor does not allow manually adding taxproducts";
 }
 
 =item transfer_batch (batch-style only)
diff --git a/FS/FS/TaxEngine/compliance_solutions.pm b/FS/FS/TaxEngine/compliance_solutions.pm
new file mode 100644
index 0000000..92ca2ce
--- /dev/null
+++ b/FS/FS/TaxEngine/compliance_solutions.pm
@@ -0,0 +1,330 @@
+package FS::TaxEngine::compliance_solutions;
+
+#some false laziness w/ suretax... uses/based on cch data?  or just imitating
+# parts of their interface?
+
+use strict;
+use base qw( FS::TaxEngine );
+use FS::Conf;
+use FS::Record qw( dbh ); #qw( qsearch qsearchs dbh);
+use Data::Dumper;
+use Date::Format;
+use Cpanel::JSON::XS;
+use SOAP::Lite;
+
+our $DEBUG = 1; # prints progress messages
+   $DEBUG = 2; # prints decoded request and response (noisy, be careful)
+#   $DEBUG = 3; # prints raw response from the API, ridiculously unreadable
+
+our $json = Cpanel::JSON::XS->new->pretty(1);
+
+our %taxproduct_cache;
+
+our $conf;
+
+FS::UID->install_callback( sub {
+    $conf = FS::Conf->new;
+    # should we enable conf caching here?
+});
+
+our %REGCODE = ( # can be selected per agent
+#  ''          => '99',
+  'ILEC'      => '00',
+  'IXC'       => '01',
+  'CLEC'      => '02',
+  'VOIP'      => '03',
+  'ISP'       => '04',
+  'Wireless'  => '05',
+);
+
+sub info {
+  { batch    => 0,
+    override => 0, #?
+  }
+}
+
+sub add_sale { } # nothing to do here
+
+sub build_input {
+  my( $self, $cust_bill ) = @_;
+
+  my $cust_main = $cust_bill->cust_main;
+
+  %taxproduct_cache = ();
+
+  # assemble invoice line items 
+  my @lines = map { $self->build_input_item($_, $cust_bill, $cust_main) }
+                  $cust_bill->cust_bill_pkg;
+
+  return if !@lines;
+
+  return \@lines;
+
+}
+
+sub build_input_item {
+  my( $self, $cust_bill_pkg, $cust_bill, $cust_main ) = @_;
+
+  # get the part_pkg/fee for this line item, and the relevant part of the
+  # taxproduct cache
+  my $part_item = $cust_bill_pkg->part_X;
+  my $taxproduct_of_class = do {
+    my $part_id = $part_item->table . '#' . $part_item->get($part_item->primary_key);
+    $taxproduct_cache{$part_id} ||= {};
+  };
+
+  my @items = ();
+
+  my $recur_without_usage = $cust_bill_pkg->recur;
+
+  ###
+  # Usage charges
+  ###
+
+  # cursor all this stuff; data sets can be LARGE
+  # (if it gets really out of hand, we can also incrementally write JSON
+  # to a file)
+
+  my $details = FS::Cursor->new('cust_bill_pkg_detail', {
+      billpkgnum  => $cust_bill_pkg->billpkgnum,
+      amount      => { op => '>', value => 0 }
+  }, dbh() );
+  while ( my $cust_bill_pkg_detail = $details->fetch ) {
+
+    # look up the tax product for this class
+    my $classnum = $cust_bill_pkg_detail->classnum;
+    my $taxproduct = $taxproduct_of_class->{ $classnum } ||= do {
+      my $part_pkg_taxproduct = $part_item->taxproduct($classnum);
+      $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
+    };
+    die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
+        ", usage class $classnum\n"
+        if !$taxproduct;
+
+    my $cdrs = FS::Cursor->new('cdr', {
+        detailnum       => $cust_bill_pkg_detail->detailnum,
+        freesidestatus  => 'done',
+    }, dbh() );
+    while ( my $cdr = $cdrs->fetch ) {
+      push @items, {
+        $self->generic_item($cust_bill, $cust_main),
+        record_type   => 'C',
+        unique_id     => 'cdr ' . $cdr->acctid.
+                         ' cust_bill_pkg '.$cust_bill_pkg->billpkgnum, 
+        productcode   => substr($taxproduct,0,4),
+        servicecode   => substr($taxproduct,4,3),
+        orig_Num      => $cdr->src,
+        term_Num      => $cdr->dst,
+        bill_Num      => $cdr->charged_party,
+        charge_amount => $cdr->rated_price, # 4 decimal places
+        minutes       => sprintf('%.1f', $cdr->billsec / 60 ),
+      };
+
+    } # while ($cdrs->fetch)
+
+    # decrement the recurring charge
+    $recur_without_usage -= $cust_bill_pkg_detail->amount;
+
+  } # while ($details->fetch)
+
+  ###
+  # Recurring charge
+  ###
+
+  if ( $recur_without_usage > 0 ) {
+    my $taxproduct = $taxproduct_of_class->{ 'recur' } ||= do {
+      my $part_pkg_taxproduct = $part_item->taxproduct('recur');
+      $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
+    };
+    die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
+        " recurring charge\n"
+        if !$taxproduct;
+
+    # when billing on cancellation there are no units
+    my $units = $self->{cancel} ? 0 : $cust_bill_pkg->units;
+    unshift @items, {
+      $self->generic_item($cust_bill, $cust_main),
+      record_type     => 'S',
+      unique_id       => 'cust_bill_pkg '. $cust_bill_pkg->billpkgnum. ' recur',
+      charge_amount   => $recur_without_usage,
+      location_a      => $cust_bill_pkg->tax_location->zip,
+      productcode     => substr($taxproduct,0,4),
+      servicecode     => substr($taxproduct,4,3),
+      units           => $units,
+    };
+  }
+
+  ###
+  # Setup charge
+  ###
+
+  if ( $cust_bill_pkg->setup > 0 ) {
+    my $taxproduct = $taxproduct_of_class->{ 'setup' } ||= do {
+      my $part_pkg_taxproduct = $part_item->taxproduct('setup');
+      $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
+    };
+    die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
+        " setup charge\n"
+        if !$taxproduct;
+
+    unshift @items, {
+      $self->generic_item($cust_bill, $cust_main),
+      record_type     => 'S',
+      unique_id       => 'cust_bill_pkg '. $cust_bill_pkg->billpkgnum. ' setup',
+      charge_amount   => $cust_bill_pkg->setup,
+      location_a      => $cust_bill_pkg->tax_location->zip,
+      productcode     => substr($taxproduct,0,4),
+      servicecode     => substr($taxproduct,4,3),
+      units           => $cust_bill_pkg->units,
+    };
+  }
+
+  return @items;
+
+}
+
+sub generic_item {
+  my( $self, $cust_bill, $cust_main ) = @_;
+
+  warn 'regcode '. $self->{regcode} if $DEBUG;
+
+  (
+    account_number            => $cust_bill->custnum,
+    customer_type             => ( $cust_main->company =~ /\S/ ? '01' : '00' ),
+    invoice_date              => time2str('%Y%m%d', $cust_bill->_date),
+    invoice_number            => $cust_bill->invnum,
+    provider                  => $self->{regcode},
+    safe_harbor_override_flag => 'N',
+    exempt_code               => $cust_main->tax,
+  );
+
+}
+
+sub make_taxlines {
+  my( $self, $cust_bill ) = @_;
+
+  die "compliance_solutions-regulatory_code setting is not configured\n"
+    unless $conf->config('compliance_solutions-regulatory_code', $cust_bill->cust_main->agentnum);
+
+  $self->{regcode} = $REGCODE{ $conf->config('compliance_solutions-regulatory_code', $cust_bill->cust_main->agentnum) };
+
+  warn 'regcode '. $self->{regcode} if $DEBUG;
+
+  # assemble the request hash
+  my $input = $self->build_input($cust_bill);
+  if (!$input) {
+    warn "no taxable items in invoice; skipping Compliance Solutions request\n" if $DEBUG;
+    return;
+  }
+
+  warn "sending Compliance Solutions request\n" if $DEBUG;
+  my $request_json = $json->encode(
+    {
+      'access_code' => $conf->config('compliance_solutions-access_code'),
+      'reference'   => 'Invoice #'. $cust_bill->invnum,
+      'input'       => $input,
+    }
+  );
+  warn $request_json if $DEBUG > 1;
+
+  my $soap = SOAP::Lite->service("http://tcms1.csilongwood.com/cgi-bin/taxcalc.wsdl");
+
+  $soap->soapversion('1.2'); #service appears to be flaky with the default 1.1
+
+  my $results = $soap->tax_rate($request_json);
+
+  my %json_result = %{ $json->decode( $results ) };
+  warn Dumper(%json_result) if $DEBUG > 1;
+
+  # handle $results is empty / API/connection failure?
+
+  # status OK
+  unless ( $json_result{status} =~ /^\s*OK\s*$/i ) {
+    warn Dumper($json_result{error_codes}) unless $DEBUG > 1;
+    die 'Compliance Solutions returned status '. $json_result{status}.
+           "; see log for error_codes detail\n";
+  }
+
+  # transmission_error No errors.
+  unless ( $json_result{transmission_error} =~ /^\s*No\s+errors\.\s*$/i ) {
+    warn Dumper($json_result{error_codes}) unless $DEBUG > 1;
+    die 'Compliance Solutions returned transmission_error '. $json_result{transmission_error}.
+           "; see log for error_codes detail\n";
+  }
+
+
+  # error_codes / No errors (for all records... check them individually in loop?
+
+  my @elements = ();
+
+  #handle the response
+  foreach my $tax_data ( @{ $json_result{tax_data} } ) {
+
+    # create a tax rate location if there isn't one yet
+    my $taxname = $tax_data->{descript};
+    my $tax_rate = FS::tax_rate->new({
+        data_vendor   => 'compliance solutions',
+        taxname       => $taxname,
+        taxclassnum   => '',
+        taxauth       => $tax_data->{'taxauthtype'}, # federal / state / city / district
+        geocode       => $tax_data->{'geocode'},
+        tax           => 0, #not necessary because we query for rates on the
+        fee           => 0, # fly and only store this for the name -> code map??
+    });
+    my $error = $tax_rate->find_or_insert;
+    die "error inserting tax_rate record for '$taxname': $error\n"
+      if $error;
+    $tax_rate = $tax_rate->replace_old;
+
+    my $tax_rate_location = FS::tax_rate_location->new({
+        data_vendor => 'compliance solutions',
+        state       => $tax_data->{'state'},
+        country     => $tax_data->{'country'},
+        geocode     => $tax_data->{'geocode'},
+    });
+    $error = $tax_rate_location->find_or_insert;
+    die "error inserting tax_rate_location record: $error\n"
+      if $error;
+    $tax_rate_location = $tax_rate_location->replace_old;
+
+    #unique id: a cust_bill_pkg (setup/recur) or cdr record
+
+    my $taxable_billpkgnum = '';
+    if ( $tax_data->{'unique_id'} =~ /^cust_bill_pkg (\d+)/ ) {
+      $taxable_billpkgnum = $1;
+    } elsif ( $tax_data->{'unique_id'} =~ /^cdr (\d+) cust_bill_pkg (\d+)$/ ) {
+      $taxable_billpkgnum = $2;
+    } else {
+      die 'unparseable unique_id '. $tax_data->{'unique_id'};
+    }
+
+    push @elements, FS::cust_bill_pkg_tax_rate_location->new({
+      taxable_billpkgnum  => $taxable_billpkgnum,
+      taxnum              => $tax_rate->taxnum,
+      taxtype             => 'FS::tax_rate',
+      taxratelocationnum  => $tax_rate_location->taxratelocationnum,
+      amount              => sprintf('%.2f', $tax_data->{taxamount}),
+    });
+
+  }
+
+  return @elements;
+}
+
+sub add_taxproduct {
+  my $class = shift;
+  my $desc = shift; # tax code and description, separated by a space.
+  if ($desc =~ s/^(\w{7}+) //) {
+    my $part_pkg_taxproduct = FS::part_pkg_taxproduct->new({
+        'data_vendor' => 'compliance_solutions',
+        'taxproduct'  => $1,
+        'description' => $desc,
+    });
+    # $obj_or_error
+    return $part_pkg_taxproduct->insert || $part_pkg_taxproduct;
+  } else {
+    return "illegal compliance solutions tax code '$desc'";
+  }
+}
+
+1;
diff --git a/FS/FS/TaxEngine/suretax.pm b/FS/FS/TaxEngine/suretax.pm
index e18b668..0f6c69d 100644
--- a/FS/FS/TaxEngine/suretax.pm
+++ b/FS/FS/TaxEngine/suretax.pm
@@ -77,7 +77,7 @@ sub build_request {
   ($self->{bill_zip}, $self->{bill_plus4}) =
     split('-', $cust_main->bill_location->zip);
 
-  $self->{regcode} = $REGCODE{ $conf->config('suretax-regulatory_code') };
+  $self->{regcode} = $REGCODE{ $conf->config('suretax-regulatory_code', $agentnum) };
 
   %taxproduct_cache = ();
 
diff --git a/httemplate/browse/part_pkg_taxproduct/suretax.html b/httemplate/browse/part_pkg_taxproduct/compliance_solutions.html
old mode 100755
new mode 100644
similarity index 51%
copy from httemplate/browse/part_pkg_taxproduct/suretax.html
copy to httemplate/browse/part_pkg_taxproduct/compliance_solutions.html
index 178062c..cf07b31
--- a/httemplate/browse/part_pkg_taxproduct/suretax.html
+++ b/httemplate/browse/part_pkg_taxproduct/compliance_solutions.html
@@ -1,7 +1,7 @@
 <& /elements/header-popup.html, $title &>
 <& /browse/elements/browse.html,
   'name_singular'  => 'tax product',
-  'html_form'      => include('.form', $category_code),
+  #'html_form'      => include('.form', $category_code),
   'query'          => {
                         'table'     => 'part_pkg_taxproduct',
                         'hashref'   => $hashref,
@@ -16,44 +16,29 @@
   'nohtmlheader'   => 1,
   'disable_total'  => 1,
 &>
-<script src="<% $fsurl %>elements/jquery.js"></script>
 <script>
-var category_labels = <% encode_json(\%category_labels) %>;
 $().ready(function() {
   var new_taxproduct = $('#new_taxproduct');
-  var new_category_desc = $('#new_category_desc');
+  var new_taxproduct2 = $('#new_taxproduct2');
+//  var new_category_desc = $('#new_category_desc');
   var new_taxproduct_desc = $('#new_taxproduct_desc');
   var new_taxproduct_submit = $('#new_taxproduct_submit');
 
-  new_taxproduct.on('keyup', function() {
-    var curr_value = this.value || '';
-    var a = curr_value.match(/^\d{2}/);
-    var f = this.form;
-    if (a) { // there is a category code in the box
-      var category = a[0];
-      if (category_labels[category]) { // it matches an existing category
-        new_category_desc.val(category_labels[category]);
-        new_category_desc.prop('disabled', true);
-      } else {
-        new_category_desc.val('');
-        new_category_desc.prop('disabled', false);
-      }
-    } else {
-      new_category_desc.prop('disabled', true);
-    }
-    if (curr_value.match(/^\d{6}$/)) {
-      new_taxproduct_submit.prop('disabled', false);
-    } else {
-      new_taxproduct_submit.prop('disabled', true);
-    }
-  });
+//  new_taxproduct.on('keyup', function() {
+//    var curr_value = this.value || '';
+//    if (curr_value.match(/^\d{7}$/)) {
+//      new_taxproduct_submit.prop('disabled', false);
+//    } else {
+//      new_taxproduct_submit.prop('disabled', true);
+//    }
+//  });
 
   new_taxproduct_submit.on('click', function() {
     select_taxproduct( -1,
-                       new_taxproduct.val()
+                       new_taxproduct.val() + new_taxproduct2.val()
                           + ' '
-                          + new_category_desc.val()
-                          + ':'
+//                          + new_category_desc.val()
+//                          + ':'
                           + new_taxproduct_desc.val()
                      );
   });
@@ -67,6 +52,9 @@ function select_taxproduct(taxproductnum, description) {
   
 </script>  
 <BR>
+
+Please contact <a href="http://csilongwood.com/" target="_blank">Compliance Solutions</a> for a full list of your product and service codes.<BR><BR>
+
 <FORM NAME="myform">
   <FONT SIZE="+1"><B><% emt('Add tax product') %></B></FONT>
   <% ntable('#cccccc', 2) %>
@@ -74,59 +62,54 @@ function select_taxproduct(taxproductnum, description) {
       'label'     => emt('Product code'),
       'field'     => 'new_taxproduct',
       'id'        => 'new_taxproduct',
-      'size'      => 6,
-      'maxlength' => 6,
+      'size'      => 4,
+      'maxlength' => 4,
     &>
     <& /elements/tr-input-text.html,
-      'label'     => emt('Category'),
-      'field'     => 'new_category_desc',
-      'id'        => 'new_category_desc',
-      'disabled'  => 1
+      'label'     => emt('Service code'),
+      'field'     => 'new_taxproduct2',
+      'id'        => 'new_taxproduct2',
+      'size'      => 3,
+      'maxlength' => 3,
     &>
     <& /elements/tr-input-text.html,
-      'label'     => emt('Product'),
+      'label'     => emt('Product name'),
       'field'     => 'new_taxproduct_desc',
       'id'        => 'new_taxproduct_desc',
     &>
   </table>
-  <input type="button" id="new_taxproduct_submit" disabled=1 value="Add">
+%#  <input type="button" id="new_taxproduct_submit" disabled=1 value="Add">
+  <input type="button" id="new_taxproduct_submit" value="Add">
 </FORM>
+
+<& /elements/footer-popup.html &>
 <%shared>
 # populate dropdown
 
-# taxproduct is 6 digits: 2-digit category code + 4-digit detail code.
+#taxproduct is 7 digits: 4-digit (well, alpha) productcode + 3-digit servicecode
 # Description is also two parts, corresponding to those codes, separated with
 # a :.
 
-my (@category_codes, @taxproduct_codes, %category_labels, %taxproduct_labels);
+my (@productcodes, @servicecodes);
 foreach my $row ( qsearch({
   table   => 'part_pkg_taxproduct',
-  select  => 'DISTINCT substr(taxproduct, 1, 2) AS code, '.
-             "substring(description from '(.*):') AS label",
-  hashref => { data_vendor => 'suretax' },
+  select  => 'DISTINCT substr(taxproduct, 1, 4) AS productcode ',
+  hashref => { data_vendor => 'compliance_solutions' },
   }))
 {
-  $category_labels{$row->get('code')} = $row->get('label');
+  push @productcodes, $row->{productcode};
 }
 
- at category_codes = sort {$a <=> $b} keys %category_labels;
+foreach my $row ( qsearch({
+  table   => 'part_pkg_taxproduct',
+  select  => 'DISTINCT substr(taxproduct, 4, 3) AS servicecode ',
+  hashref => { data_vendor => 'compliance_solutions' },
+  }))
+{
+  push @servicecodes, $row->{servicecode};
+}
 
 </%shared>
-<%def .form>
-% my ($category_code) = @_;
-<FORM ACTION="<% $cgi->url %>" METHOD="GET">
-<& /elements/select.html,
-  field       => 'category_code',
-  options     => \@category_codes,
-  labels      => \%category_labels,
-  curr_value  => $category_code,
-  onchange    => 'this.form.submit()',
-&>
-<& /elements/hidden.html,
-  field       => 'id',
-  curr_value  => scalar($cgi->param('id')),
-&>
-</%def>
 <%init>
 
 die "access denied"
@@ -145,32 +128,33 @@ my $select_onclick = sub {
 my @menubar;
 my $title = 'Tax Products';
 
-my $hashref = { data_vendor => 'suretax' };
+my $hashref = { data_vendor => 'compliance_solutions' };
 
-my ($category_code, $taxproduct);
-if ( $cgi->param('category_code') =~ /^(\d+)$/ ) {
-  $category_code = $1;
-  $taxproduct = $category_code . '%';
-} else {
-  $taxproduct = '%';
-}
+#my ($category_code, $taxproduct);
+#if ( $cgi->param('category_code') =~ /^(\d+)$/ ) {
+#  $category_code = $1;
+#  $taxproduct = $category_code . '%';
+#} else {
+#  $taxproduct = '%';
+#}
+my $taxproduct = '%';
 
 $hashref->{taxproduct} = { op => 'LIKE', value => $taxproduct };
 
 my $count_query = "SELECT COUNT(*) FROM part_pkg_taxproduct ".
-                  "WHERE data_vendor = 'suretax' AND ".
+                  "WHERE data_vendor = 'compliance_solutions' AND ".
                   "taxproduct LIKE '$taxproduct'";
 
 my @fields = (
   'taxproduct',
   'description',
-  'note'
+#  'note'
 );
 
 my @header = (
   'Code',
   'Description',
-  '',
+#  '',
 );
 
 my $align = 'lll';
diff --git a/httemplate/browse/part_pkg_taxproduct/suretax.html b/httemplate/browse/part_pkg_taxproduct/suretax.html
index 178062c..4605c1f 100755
--- a/httemplate/browse/part_pkg_taxproduct/suretax.html
+++ b/httemplate/browse/part_pkg_taxproduct/suretax.html
@@ -91,6 +91,7 @@ function select_taxproduct(taxproductnum, description) {
   </table>
   <input type="button" id="new_taxproduct_submit" disabled=1 value="Add">
 </FORM>
+<& /elements/footer-popup.html &>
 <%shared>
 # populate dropdown
 

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Conf.pm                                      |   16 +-
 FS/FS/TaxEngine.pm                                 |    7 +-
 FS/FS/TaxEngine/compliance_solutions.pm            |  330 ++++++++++++++++++++
 FS/FS/TaxEngine/suretax.pm                         |    2 +-
 .../{suretax.html => compliance_solutions.html}    |  124 ++++----
 httemplate/browse/part_pkg_taxproduct/suretax.html |    1 +
 6 files changed, 407 insertions(+), 73 deletions(-)
 create mode 100644 FS/FS/TaxEngine/compliance_solutions.pm
 copy httemplate/browse/part_pkg_taxproduct/{suretax.html => compliance_solutions.html} (51%)
 mode change 100755 => 100644




More information about the freeside-commits mailing list