The tax computation in the US is different from various other countries and is further different in each state which is ever-changing based on the reforms. Therefore, while doing business in the States you will require advanced as well as real-time tax computational systems. TaxCloud helps to calculate sales tax in real-time for every state, city, and special jurisdiction in the United States.
Let's discuss the TaxCloud integration in the Odoo community version.
TaxCloud Configuration
Initially, you should create an account on the TaxCloud website which is absolutely free of charge.
Upon creating an account in TaxCloud you will obtain an API Key and an API ID. In Settings, under Store, we can see the API Key and API ID
In Settings of your TaxCloud dashboard, under the Locations section enter the location of your facility & Warehouse(s).
You can describe the Tax of the respective state form which you are operating under the Manage Tax States section in the Settings menu.
Tax Cloud Integration
Now let's look at how to integrate TaxCloud into our Odoo community edition.
First let's create fields to activate TaxCloud, and fields to enter API Key and API ID in the settings. Then create a view in the settings to add TaxCloud credentials. The code is given below.
class ResSettings(models.TransientModel):
_inherit = 'res.config.settings'
activate_tax_cloud = fields.Boolean()
tax_cloud_id = fields.Char('Tax Cloud Id', config_parameter='tax_cloud_id')
tax_cloud_key = fields.Char('Tax Cloud Key', config_parameter='tax_cloud_key')
Now let's create the view.
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="res_config_settings_tax_cloud_view" model="ir.ui.view">
<field name="name">res.config.settings.tax.cloud.view</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@data-key='account']/div[hasclass('o_settings_container')][2]/div[hasclass('o_setting_box')][2]"
position="after">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="activate_tax_cloud"/>
</div>
<div class="o_setting_right_pane">
<span class="o_form_label">TaxCloud</span>
<div class="text-muted">
Compute tax rates based on U.S. ZIP codes
</div>
<div class="content-group" attrs="{'invisible': [('activate_tax_cloud', '=', False)]}">
<div class="row mt16">
<label string="API ID" for="tax_cloud_id" class="col-md-4 o_light_label"/>
<field name="tax_cloud_id"/>
</div>
<div class="row">
<label string="API KEY" for="tax_cloud_key" class="col-md-4 o_light_label"/>
<field name="tax_cloud_key"/>
<button type="object" name="test_api" class="btn btn-link" string="Test Connection"
icon="fa-television">
</button>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
Now let's take a look on TaxCloud API
1.1. Ping
If Ping fails, API credentials may be wrong.
You can use the PING API to check whether API credentials are valid or not. Additionally, ist will help you to understand whether the networking between your systems and TaxCloud's are configured to the correct terms to establish real-time communications.
Request
POST https://api.taxcloud.net/1.0/TaxCloud/Ping
In the above XML code we added a button:
<button type="object" name="test_api" class="btn btn-link" string="Test Connection" icon="fa-television"></button>
This button is used to call the PING API. We can look at the PING API added in the res.config.settings model.
def test_api(self):
vals = {
"apiKey": str(self.tax_cloud_key),
"apiLoginID": str(self.tax_cloud_id)
}
tax_ping = requests.post("https://api.taxcloud.net/1.0/TaxCloud/Ping", json=vals)
status = tax_ping.json()
if status['ResponseType'] == 3:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Connection Test Succeeded!',
'message': 'Everything seems properly set up!',
'sticky': False,
}
}
elif status['ResponseType'] == 0:
raise UserError(_('Connection Test Failed \n' + status['Messages'][0]['Message']))
We can see that we are passing the api credentials for the PING API using:
vals = {
"apiKey": str(self.tax_cloud_key),
"apiLoginID": str(self.tax_cloud_id)
}
And this value is passed in the API:
tax_ping = requests.post("https://api.taxcloud.net/1.0/TaxCloud/Ping", json=vals)
We can see that if the ResponseType is 3 the API credentials are valid and we can proceed, and if the ResponseType is 0 the credentials are invalid so we have to check the credentials.
If the API credentials are verified we can proceed with TaxCloud integrations in our sales. Here we are calculating the tax for the customer address when we are confirming the sale order, let’s take a look at it.
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
config_parm = self.env['ir.config_parameter'].sudo()
verify_address = {
"apiLoginID": config_parm.get_param('tax_cloud_id'),
"apiKey": config_parm.get_param('tax_cloud_key'),
"Address1": self.partner_id.street if self.partner_id.street else '',
"Address2": self.partner_id.street2 if self.partner_id.street2 else '',
"City": self.partner_id.city if self.partner_id.city else '',
"State": self.partner_id.state_id.code if self.partner_id.state_id.code else '',
"Zip5": self.partner_id.zip if self.partner_id.zip else ''
}
resp_address_verify = requests.post("https://api.taxcloud.net/1.0/TaxCloud/VerifyAddress",
json=verify_address)
address_info = resp_address_verify.json()
cart_items = []
for line in self.order_line:
cart_vals = {
"Index": str(line.id),
"Qty": line.product_uom_qty,
"Price": line.price_unit,
"TIC": '',
"ItemID": str(line.product_id.id)
}
cart_items.append(cart_vals)
if address_info['ErrNumber'] == '0':
lookup_data = {
"apiLoginID": config_parm.get_param('tax_cloud_id'),
"apiKey": config_parm.get_param('tax_cloud_key'),
"customerID": self.partner_id.name,
"deliveredBySeller": True,
"cartID": str(self.id),
"destination": {
"Address1": address_info['Address1'],
"Address2": address_info['Address2'],
"City": address_info['City'],
"State": address_info['State'],
"Zip5": address_info['Zip5'],
"Zip4": address_info['Zip4']
},
"origin": {
"Address1": address_info['Address1'],
"Address2": address_info['Address2'],
"City": address_info['City'],
"State": address_info['State'],
"Zip5": address_info['Zip5'],
"Zip4": address_info['Zip4']
},
"cartItems": cart_items
}
resp_lookup = requests.post("https://api.taxcloud.com/1.0/TaxCloud/Lookup", json=lookup_data)
lookup_info = resp_lookup.json()
if lookup_info['ResponseType'] != 0:
tax_resp = lookup_info.get('CartItemsResponse')
for tax in tax_resp:
order_line = self.order_line.filtered(lambda s: s.id == int(tax['CartItemIndex']))
tax_percentage = round((tax['TaxAmount'] * 100) / order_line.price_unit, 2)
tax_update = {
'name': 'Tax ' + str(tax_percentage) + '%',
'type_tax_use': 'sale',
'amount_type': 'percent',
'company_id': self.company_id.id,
'amount': tax_percentage,
'description': str(tax_percentage) + '%'
}
exist_tax = self.env['account.tax'].sudo().with_context(active_test=False).search(
[('company_id', '=', self.company_id.id),
('amount', '=', tax_percentage)],
limit=1)
if exist_tax:
exist_tax.active = True
order_line.tax_id = exist_tax.ids
else:
tax = self.env['account.tax'].sudo().create(tax_update)
order_line.tax_id = tax.ids
return res
Here we use super for the sale order confirm function.
config_parm = self.env['ir.config_parameter'].sudo()
"apiLoginID": config_parm.get_param('tax_cloud_id'),
"apiKey": config_parm.get_param('tax_cloud_key'),
Here, using the TaxCloud credentials that we have described the settings we initially verifying the customer address using the TaxCloud Verify Address API.
The customer delivery address verification is vital for understanding their state jurisdiction to provide them with the respective tax allocation.
Request
POST https://api.taxcloud.net/1.0/TaxCloud/VerifyAddress
verify_address = {
"apiLoginID": config_parm.get_param('tax_cloud_id'),
"apiKey": config_parm.get_param('tax_cloud_key'),
"Address1": self.partner_id.street if self.partner_id.street else '',
"Address2": self.partner_id.street2 if self.partner_id.street2 else '',
"City": self.partner_id.city if self.partner_id.city else '',
"State": self.partner_id.state_id.code if self.partner_id.state_id.code else '',
"Zip5": self.partner_id.zip if self.partner_id.zip else ''
}
resp_address_verify = requests.post("https://api.taxcloud.net/1.0/TaxCloud/VerifyAddress",
json=verify_address)
address_info = resp_address_verify.json()
The above-given code will verify the address of the customer.
If address_info['ErrNumber'] == '0' the address is verified else we have to proceed to Lookup using the customer-provided information.
After the address verification, we can proceed to Lookup API. Lookup API will provide the tax rates for the given address.
lookup_data = {
"apiLoginID": config_parm.get_param('tax_cloud_id'),
"apiKey": config_parm.get_param('tax_cloud_key'),
"customerID": self.partner_id.name,
"deliveredBySeller": True,
"cartID": str(self.id),
"destination": {
"Address1": address_info['Address1'],
"Address2": address_info['Address2'],
"City": address_info['City'],
"State": address_info['State'],
"Zip5": address_info['Zip5'],
"Zip4": address_info['Zip4']
},
"origin": {
"Address1": address_info['Address1'],
"Address2": address_info['Address2'],
"City": address_info['City'],
"State": address_info['State'],
"Zip5": address_info['Zip5'],
"Zip4": address_info['Zip4']
},
"cartItems": cart_items
}
resp_lookup = requests.post("https://api.taxcloud.com/1.0/TaxCloud/Lookup", json=lookup_data)
lookup_info = resp_lookup.json()
In lookup data we are providing the respective API credentials, addresses to calculate the tax. And we are describing cart items for the sale order lines as given below.
cart_items = []
for line in self.order_line:
cart_vals = {
"Index": str(line.id),
"Qty": line.product_uom_qty,
"Price": line.price_unit,
"TIC": '',
"ItemID": str(line.product_id.id)
}
cart_items.append(cart_vals)
Further, by calling the API to look up data we will get the tax rate for each line.
lookup_info = resp_lookup.json()
The response from the TaxCloud is stored in lookup_info, If lookup_info['ResponseType'] != 0, depicted that there is zero error therefore, we can proceed.
tax_resp = lookup_info.get('CartItemsResponse')
tax_resp will be having the list of tax rates for the sale order lines. When we get the CartItemsResponse we can create a record in account.tax for the calculated tax rates and that record will be passed to the tax_id of the lines, the following codes describe those:
tax_resp = lookup_info.get('CartItemsResponse')
for tax in tax_resp:
order_line = self.order_line.filtered(lambda s: s.id == int(tax['CartItemIndex']))
tax_percentage = round((tax['TaxAmount'] * 100) / order_line.price_unit, 2)
tax_update = {
'name': 'Tax ' + str(tax_percentage) + '%',
'type_tax_use': 'sale',
'amount_type': 'percent',
'company_id': self.company_id.id,
'amount': tax_percentage,
'description': str(tax_percentage) + '%'
}
exist_tax = self.env['account.tax'].sudo().with_context(active_test=False).search(
[('company_id', '=', self.company_id.id),
('amount', '=', tax_percentage)],
limit=1)
if exist_tax:
exist_tax.active = True
order_line.tax_id = exist_tax.ids
else:
tax = self.env['account.tax'].sudo().create(tax_update)
order_line.tax_id = tax.ids
Thus when we confirm the sale order, the tax_id of the sale order line will be added based on the tax rate calculated using TaxCloud API.
The above example just shows the simple method of calculating the tax based on TaxCloud. A similar terminology can be applied in calculating the taxes on invoices.