[freeside-commits] branch FREESIDE_4_BRANCH updated. 5f2511bd9a1d2919ae6b3c619bdfe929823936bf
Ivan
ivan at 420.am
Tue Jun 20 16:32:10 PDT 2017
The branch, FREESIDE_4_BRANCH has been updated
via 5f2511bd9a1d2919ae6b3c619bdfe929823936bf (commit)
from e7cecbe67f14245eb17aaf0ebe4c371d93afe1ff (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 5f2511bd9a1d2919ae6b3c619bdfe929823936bf
Author: Ivan Kohler <ivan at freeside.biz>
Date: Tue Jun 20 16:32:09 2017 -0700
Compliance Solutions <http://csilongwood.com/> integration, RT#75262
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index d26da0b..639a9ec 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2484,7 +2484,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' ],
},
{
@@ -2550,6 +2550,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