Initial commit: OCA Financial packages (186 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:04 +02:00
commit 3e0e8473fb
8757 changed files with 947473 additions and 0 deletions

View file

@ -0,0 +1,45 @@
# Avalara Avatax Certified Connector
Odoo addon: account_avatax_oca
## Installation
```bash
pip install odoo-bringout-oca-account-fiscal-rule-account_avatax_oca
```
## Dependencies
This addon depends on:
- sale_stock
- base_geolocalize
## Manifest Information
- **Name**: Avalara Avatax Certified Connector
- **Version**: 16.0.1.7.0
- **Category**: Accounting
- **License**: AGPL-3
- **Installable**: True
## Source
Based on [OCA/account-fiscal-rule](https://github.com/OCA/account-fiscal-rule) branch 16.0, addon `account_avatax_oca`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,519 @@
==================================
Avalara Avatax Certified Connector
==================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:7f9e0f6757252679c2b9dd5853a0ef54dd73e94fb1ff3289c902ef982cda6ea5
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
:alt: Production/Stable
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--fiscal--rule-lightgray.png?logo=github
:target: https://github.com/OCA/account-fiscal-rule/tree/16.0/account_avatax_oca
:alt: OCA/account-fiscal-rule
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-fiscal-rule-16-0/account-fiscal-rule-16-0-account_avatax_oca
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/account-fiscal-rule&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
.. |avataxbadge1| image:: https://raw.githubusercontent.com/OCA/account-fiscal-rule/16.0/account_avatax_oca/static/description/SalesTax.png
:target: https://developer.avalara.com/certification/avatax/sales-tax-badge/
:alt: Sales Tax Certification
:width: 250
.. |avataxbadge2| image:: https://raw.githubusercontent.com/OCA/account-fiscal-rule/16.0/account_avatax_oca/static/description/Refunds.png
:target: https://developer.avalara.com/certification/avatax/refunds-credit-memos-badge/
:alt: Refunds Certification
:width: 250
.. |avataxbadge3| image:: https://raw.githubusercontent.com/OCA/account-fiscal-rule/16.0/account_avatax_oca/static/description/AddressValidation.png
:target: https://developer.avalara.com/certification/avatax/address-validation-badge/
:alt: Address Validation Certification
:width: 250
|avataxbadge1| |avataxbadge2| |avataxbadge3|
Odoo provides integration with AvaTax, a tax solution software by Avalara
which includes sales tax calculation for all US states and territories
and all Canadian provinces and territories (including GST, PST, and HST).
This module is capable of automatically detecting origin (Output Warehouse)
and destination (Client Address), then calculating and reporting taxes
to the user's Avalara account as well as a recording the correct sales taxes
for the validated addresses within Odoo ERP.
This module is compatible both with the Odoo Enterprise and Odoo Community
editions.
An Avatax account is needed. Account information to access
the Avatax dashboard can be obtained through the Avalara website here:
https://www.avalara.com/products/calculations.html
Once configured, the module operates in the background and performs
calculations and reporting seamlessly to the AvaTax server.
This guide includes instructions for the following elements:
- Activating your organization's AvaTax account and downloading the product
- Entering the AvaTax credentials into your Odoo database and configuring it
to use AvaTax services and features within Odoo
Note: Test the module before deploying in live environment.
All changes to the AvaTax settings must be performed by a user with
administrative access rights.
**IMPORTANT - resolving name conflict with Odoo EE**
Avatax support was added to Odoo EE 14 and 15.
Unfortunately the module names used are the same as the OCA ones,
and because of this name collision the OCA modules were forced to change name.
The main module was renamed from ``account_avatax`` (now used by Odoo EE) to
``account_avatax_oca``.
To apply this change in your odoo database and continue using the OCA Avalara certified
connector:
1. Ensure you have the latest version from the OCA, and you see ``account_avatax_oca``
in your Apps list.
2. Install the new ``account_avatax_oca`` module
3. Unistall the ``account_avatax`` module
4. Confirm that your configurations were kept safe, in particular:
Avatax API, "Avatax" default Fiscal Position, and "Avatax" default Tax record.
**Table of contents**
.. contents::
:local:
Installation
============
Before installing the Avatax app, the Avalara Python client
must be installed in your system.
It is available at https://pypi.org/project/Avalara.
Typically it can be installed in your system usin ``pip``::
pip3 install Avalara
The base app, ``account_Avatax``, adds Avatax support to Customer Invoices.
Inthe official app store: https://apps.odoo.com/apps/modules/15.0/account_avatax/
The ``account_avatax_sale`` extension adds support to Quotations / Sales Orders.
Inthe official app store: https://apps.odoo.com/apps/modules/15.0/account_avatax_sale/
In most cases you will want to download and install both modules.
To install the Avatax app:
- Download the AvaTax modules
- Extract the downloaded files
- Upload the extracted directories into your Odoo module/addons directory
- Log into Odoo as an Administrator and enable the Developer Mode, in 'Settings'
- Navigate to 'Apps', select the 'Update Apps List' menu, to have the new apps listed.
- In the Apps list, search for 'AvaTax'
- Click on the Install button. If available, the ``account_avatax_sale`` module will
also be installed automatically.
Configuration
=============
To configure an Odoo company to use Avatax, follow these steps.
Note tha tsome of them might be configured out of the box
for the Odoo default company.
1. Configure AvaTax API Connection
2. Configure Company Taxes
3. Configure Customers
4. Configure Products
Configure Avatax API Connection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before you can configure the Odoo Avatax connector,
you will need some connection details ready:
- Login to https://home.avalara.com/
- Navigate to Settings >> All AvaTax Settings.
There you will see the company details.
- Take note of the Account ID and Company Code
- Navigate to Settings >> License and API Keys.
In the "Reset License Key" tab, click on the "Generate License Key" button,
and take note of it.
To configure AvaTax connector in Odoo:
- Navigate to: Accounting/Invoicing App >> Configuration >> AvaTax >> AvaTax API
- Click on the Create button
- Fill out the form with the elements collected from the AvaTax website:
* Account ID
* License Key
* Service URL: usually Production, or Sandox if you have that available.
* Company Code
- Click the Test Connection button
- Click the Save button
Other Avatax API advanced configurations:
- Tax Calculation tab:
- Disable Document Recording/Commiting: invoices will not be stored in Avalara
- Enable UPC Taxability: this will transmit Odoo's product ean13 number
instead of its Internal Reference. If there is no ean13
then the Internal Reference will be sent automatically.
- Hide Exemption & Tax Based on shipping address -- this will give user ability
to hide or show Tax Exemption and Tax Based on shipping address fields
at the invoice level.
- Address Validation tab:
- Automatic Address Validation: automatically attempts
to validate on creation and update of customer record,
last validation date will be visible and stored
- Require Validated Addresses: if validation for customer is required but not valid,
the validation will be forced
- Return validation results in upper case: validation results
will return in upper case form
- Advanced tab:
- Automatically generate missing customer code: generates a customer code
on creation and update of customer profile
- Log API requests: enables detailed AvaTax transaction logging within application
- Request Timeout: default is 300ms
- Countries: countries where AvaTax can be used.
Configure Company Taxes
~~~~~~~~~~~~~~~~~~~~~~~
Each company linked to AvaTax and their associated warehouses
should be configured to ensure the correct tax is calculated
and applied for all transactions.
Validate Company Address:
- On the AvTax API configuration form, click on the "Company Address" link
- On the company address form, click on the "validate" button
in the "AvaTax" tab
Validate Warehouse Address:
- Navigate to: Inventory >> Configuration >> Warehouse Management >> Warehouses
- For each warehouse, open the correspoding from view
- On the Warehouse form, click on the "Address" link
- On the warehouse address form, click on the "validate" button
in the "AvaTax" tab
Fiscal Positions is what tells the AvaTax connector if the AvaTax service
should be used for a particular Sales Order or Invoice.
Configure Fiscal Position:
- Navigate to: Accounting/Invoicing App >> Configuration >> Accounting
>> Fiscal Positions
- Ensure there is a Fiscal Position record for the Company,
with the "Use Avatax API" flag checked
When the appropriate Fiscal Position is being used, and a tax rate is retrieved form
AvaTax, then the corresponding Tax is automatically created in Odoo
using a template tax record, that should have the appropriate accounting configurations.
Configure Taxes:
- Navigate to: Accounting/Invoicing App >> Configuration >> Accounting >> Taxes
- Ensure there is a Tax record for the Company, with the "Is Avatax" flag checked
(visible in the "Advanced Options" tab). This Tax should have:
* Tax Type: Sales
* Tax Computation: Percentage of Price
* Amount: 0.0%
* Distribution for Invoices: ensure correct account configuration
* Distribution for Credit Notes: ensure correct account configuration
Configure Customers
~~~~~~~~~~~~~~~~~~~
Exemption codes are allowed for users where they may apply (ex. Government entities).
Navigate to: Accounting or Invoicing App >> Configuration >> AvaTax >> Exemption Code
The module is installed with 16 predefined exemption codes.
You can add, remove, and modify exemption codes.
Properly configuring each customer ensures the correct tax is calculated
and applied for all transactions.
Create New Customer
- Navigate to Contacts
- Click Create button
Configure and Validate Customer Address
- Enter Customer Address
- Under AvaTax >> Validation, click Validate button
- AvaTax Module will attempt to match the address you entered
with a valid address in its database.
Click the Accept button if the address is valid.
Tax Exemption Status
- If the customer is tax exempt, check the box under
AvaTax >> Tax Exemption >> Is Tax Exempt and
- Select the desired Tax Exempt Code from the dropdown menu.
Configure Products
~~~~~~~~~~~~~~~~~~
Create product tax codes to assign to products and/or product categories.
Navigate to: Accounting or Invoicing App >> Configuration >> AvaTax >> Product Tax Codes.
From here you can add, remove, and modify the product tax codes.
Products in Odoo are typically assigned to product categories.
AvaTax settings can also be assigned to the product category
when a product category is created.
- Create New Product Category
- Navigate to: Inventory >> Configuration >> Products >> Product Categories
- Click Create button
- Configure Product Category Tax Code
- Under AvaTax Properties >> Tax Code
- Select the desired Tax Code
Usage
=====
Customer Invoices
~~~~~~~~~~~~~~~~~
The AvaTax module is integrated into Sales Invoices
and is applied to each transaction.
The transaction log in the AvaTax dashboard shows the invoice details
and displays whether the transaction is in an uncommitted or committed status.
A validated invoice will have a Committed status
and a cancelled invoice will have a Voided status.
The module will check if there is a selected warehouse
and will automatically determine the address of the warehouse
and the origin location.
If no address is assigned to the warehouse, the company address is used.
Discounts are handled when they are enabled in Odoo's settings.
They are calculated as a net deduction on the line item cost
before the total is sent to AvaTax.
Create New Customer Invoice
^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Navigate to: Accounting or Invoicing >> Customers >> Invoices.
- Click Create button.
Validate Invoice
^^^^^^^^^^^^^^^^
- Ensure that Tax based on shipping address is checked.
- Line items should have AVATAX selected under Taxes for internal records.
- To complete the invoice, click the Validate button.
- The sale order will now appear in the AvaTax dashboard.
Register Payment
^^^^^^^^^^^^^^^^
- Click the Register Payment button to finalize the invoice.
Customer Refunds
^^^^^^^^^^^^^^^^
Odoo applies refunds as opposed to voids in its accounting module.
As with customer invoices, the AvaTax module is integrated
with customer refunds and is applied to each transaction.
Refunded invoice transactions will be indicated
with a negative total in the AvaTax interface.
Initiate Customer Refund
- Navigate to: Accounting or Invoicing >> Customers >> Invoices
- Select the invoice you wish to refund
- Click Add Credit Note button
Create Credit Note
- Under Credit Method, select Create a draft credit note.
- Enter a reason.
- Click Add Credit Note button.
Note: You will be taken to the Credit Notes list view
Validate Refund
- Select the Credit Note you wish to validate, review and then click Validate button.
Register Refund Payment
- Click Register Payment button to complete a refund
Sales Orders
~~~~~~~~~~~~
The AvaTax module is integrated into Sales Orders and allows computation of taxes.
Sales order transactions do not appear in the in the AvaTax interface.
The information placed in the sales order will automatically pass to the invoice
on the Avalara server and can be viewed in the AvaTax control panel.
Discounts are handled when they are enabled in Odoo's settings.
They will be reported as a net deduction on the line item cost.
Create New Sales Order
- Navigate to: Sales >> Orders >> Orders
- Click Create button
Compute Taxes with AvaTax
- The module will calculate tax when the sales order is confirmed,
or by navigating to Action >> Update taxes with Avatax.
At this step, the sales order will retrieve the tax amount from Avalara
but will not report the transaction to the AvaTax dashboard.
Only invoice, refund, and payment activity are reported to the dashboard.
- The module will check if there is a selected warehouse
and will automatically determine the address of the warehouse
and the origin location. If no address is assigned to the warehouse
the module will automatically use the address of the company as its origin.
Location code will automatically populate with the warehouse code
but can be modified if needed.
Known issues / Roadmap
======================
The development of this module was driven by US companies to compute Sales Tax.
However the Avatax service supports more use cases, that could be added:
- Add support to EU VAT
- Add support to US Use Tax on Purchases / vendor Bills
Other improvements that could be added:
- Detect and warn if customers State is not a nexus available for the current account
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-fiscal-rule/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/account-fiscal-rule/issues/new?body=module:%20account_avatax_oca%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Open Source Integrators
* Fabrice Henrion
* Sodexis
Contributors
~~~~~~~~~~~~
* Odoo SA
* Fabrice Henrion
* Open Source Integrators (https://opensourceintegrators.com)
* Daniel Reis <dreis@opensourceintegrators.com>
* Bhavesh Odedra <bodedra@opensourceintegrators.com>
* Sandip Mangukiya <smangukiya@opensourceintegrators.com>
* Nikul Chaudhary <nchaudhary@opensourceintegrators.com>
* Serpent CS
* Murtuza Saleh
* Sodexis
* Atchuthan Ubendran
- Kencove (<https://kencove.com>)
- Don Kendall \<<kendall@donkendall.com>\>
- Mohamed Alkobrosli \<<malkobrosly@kencove.com>\>
- Wai-Lun Lin \<<wlin@kencove.com>\>
Other credits
~~~~~~~~~~~~~
This module was originally developed by Fabrice Henrion at Odoo SA,
and maintained up to version 11.
For version 12, Fabrice invited partners to migrate this modules to
later version, and maintain it.
Open Source Integrators performed the migration to Odoo 12
, and later added support for the more up to date REST API
, alongside with the legacy SOAP API.
With the addition of the REST API, a deep refactor was introduced,
changing the tax calculation approach, from just setting the total
tax amount, to instead adding the tax rates to each document line
and then having Odoo do all the other computations.
For Odoo 13, the legacy SOAP support was supported, and
additional refactoring was done to contribute the module
to the Odoo Community Association.
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-dreispt| image:: https://github.com/dreispt.png?size=40px
:target: https://github.com/dreispt
:alt: dreispt
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-dreispt|
This module is part of the `OCA/account-fiscal-rule <https://github.com/OCA/account-fiscal-rule/tree/16.0/account_avatax_oca>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,4 @@
from . import models
from . import wizard
from .hooks import pre_init_hook
from .hooks import post_load_hook

View file

@ -0,0 +1,36 @@
{
"name": "Avalara Avatax Certified Connector",
"version": "16.0.1.7.0",
"author": "Open Source Integrators, Fabrice Henrion,"
"Sodexis, Odoo Community Association (OCA)",
"summary": "Compute Sales Tax using the Avalara Avatax Service",
"license": "AGPL-3",
"category": "Accounting",
"website": "https://github.com/OCA/account-fiscal-rule",
"depends": ["sale_stock", "base_geolocalize"],
"pre_init_hook": "pre_init_hook",
"post_load": "post_load_hook",
"data": [
"security/avalara_salestax_security.xml",
"security/ir.model.access.csv",
"data/avalara_salestax_data.xml",
"data/avalara_salestax_exemptions.xml",
"wizard/avalara_get_company_code_view.xml",
"wizard/avalara_salestax_address_validate_view.xml",
"wizard/avalara_salestax_ping_view.xml",
"views/avalara_salestax_view.xml",
"views/partner_view.xml",
"views/product_view.xml",
"views/account_move_action.xml",
"views/account_move_view.xml",
"views/account_tax_view.xml",
"views/account_fiscal_position_view.xml",
],
"demo": ["demo/avatax_demo.xml"],
"images": ["static/description/avatax_icon.png"],
"installable": True,
"application": True,
"external_dependencies": {"python": ["Avalara"]},
"development_status": "Production/Stable",
"maintainers": ["dreispt"],
}

View file

@ -0,0 +1,22 @@
<odoo noupdate="1">
<record id="avatax_fiscal_position_us" model="account.fiscal.position">
<field name="name">Avatax Tax Mapping (US)</field>
<field name="is_avatax" eval="True" />
<field name="auto_apply" eval="True" />
<field name="country_id" ref="base.us" />
</record>
<record id="avatax_tax_group" model="account.tax.group">
<field name="name">AvaTax</field>
</record>
<!-- Used as template for automatic Tax records created -->
<record id="avatax" model="account.tax">
<field name="name">AVATAX</field>
<field name="description">Sales Tax</field>
<field name="tax_group_id" ref="avatax_tax_group" />
<field name="amount_type">percent</field>
<field name="amount" eval="0.00" />
<field name="type_tax_use">sale</field>
<field name="is_avatax">True</field>
<field name="country_id" ref="base.us" />
</record>
</odoo>

View file

@ -0,0 +1,69 @@
<data noupdate="1">
<!--
Partner Exemption Code
-->
<record id="federal_government_type" model="exemption.code">
<field name="name">Federal Government</field>
<field name="code">A</field>
</record>
<record id="state_government_type" model="exemption.code">
<field name="name">State Government</field>
<field name="code">B</field>
</record>
<record id="tribe_indian_band_type" model="exemption.code">
<field name="name">Tribe / Status Indian / Indian Band</field>
<field name="code">C</field>
</record>
<record id="foreign_diplomat_type" model="exemption.code">
<field name="name">Foreign Diplomat</field>
<field name="code">D</field>
</record>
<record id="charitable_org_type" model="exemption.code">
<field name="name">Charitable or Benevolent Org</field>
<field name="code">E</field>
</record>
<record id="religious_eductional_org_type" model="exemption.code">
<field name="name">Religious or Educational Org</field>
<field name="code">F</field>
</record>
<record id="resale_type" model="exemption.code">
<field name="name">Resale</field>
<field name="code">G</field>
</record>
<record id="commercial_agriculture_production_type" model="exemption.code">
<field name="name">Commercial Agricultural Production</field>
<field name="code">H</field>
</record>
<record id="industrial_manufacturer_type" model="exemption.code">
<field name="name">Industrial Production / Manufacturer</field>
<field name="code">I</field>
</record>
<record id="direct_pay_permit_type" model="exemption.code">
<field name="name">Direct Pay Permit</field>
<field name="code">J</field>
</record>
<record id="direct_mail_type" model="exemption.code">
<field name="name">Direct Mail</field>
<field name="code">K</field>
</record>
<record id="other_type" model="exemption.code">
<field name="name">Other</field>
<field name="code">L</field>
</record>
<record id="local_government_type" model="exemption.code">
<field name="name">Local Government</field>
<field name="code">N</field>
</record>
<record id="commercial_aquaculture_type" model="exemption.code">
<field name="name">Commercial Aquaculture</field>
<field name="code">P</field>
</record>
<record id="commercial_fishery_type" model="exemption.code">
<field name="name">Commercial Fishery</field>
<field name="code">Q</field>
</record>
<record id="non_resident_type" model="exemption.code">
<field name="name">Non-Resident</field>
<field name="code">R</field>
</record>
</data>

View file

@ -0,0 +1,44 @@
<odoo>
<record id="avatax_customer" model="res.partner">
<field name="name">Washington Customer</field>
<field name="street">100 Ravine Ln Ne</field>
<field name="city">Bainbridge Island</field>
<field name="state_id" ref="base.state_us_48" />
<field name="country_id" ref="base.us" />
<field name="zip">98110-2687</field>
</record>
<record id="avatax_product_taxcodeP" model="product.tax.code">
<field name="name">P0000000</field>
<field name="type">product</field>
</record>
<record id="avatax_product_taxcodeNT" model="product.tax.code">
<field name="name">NT</field>
<field name="type">product</field>
</record>
<record id="avatax_product_taxcodeF" model="product.tax.code">
<field name="name">FR020100</field>
<field name="type">freight</field>
</record>
<record id="avatax_product_sku1" model="product.product">
<field name="name">Test Item P0000000</field>
<field name="detailed_type">product</field>
<field name="list_price">100.0</field>
<field name="tax_code_id" ref="avatax_product_taxcodeP" />
</record>
<record id="avatax_product_sku2" model="product.product">
<field name="name">Test Item NT</field>
<field name="detailed_type">product</field>
<field name="list_price">100.0</field>
<field name="tax_code_id" ref="avatax_product_taxcodeNT" />
</record>
<record id="avatax_product_freight" model="product.product">
<field name="name">Common Carrier FR020100</field>
<field name="detailed_type">service</field>
<field name="list_price">50.0</field>
<field name="tax_code_id" ref="avatax_product_taxcodeF" />
</record>
</odoo>

View file

@ -0,0 +1,102 @@
# Copyright (C) 2022 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api
from odoo.tools import frozendict
from odoo.addons.account.models.account_move_line import AccountMoveLine
def pre_init_hook(cr):
# Preserve key data when moving from account_avatax to account_avatax_oca
# The process is to first install account_avatax_oca
# and then uninstall account_avatax
cr.execute(
"""
UPDATE ir_model_data
SET module = 'account_avatax_oca'
WHERE name in ('avatax_fiscal_position_us', 'account_avatax')
"""
)
def post_load_hook(): # noqa: C901
@api.depends(
"tax_ids",
"currency_id",
"partner_id",
"analytic_distribution",
"balance",
"partner_id",
"move_id.partner_id",
"price_unit",
"quantity",
)
def _compute_all_tax_new(self):
for line in self:
sign = line.move_id.direction_sign
if line.display_type == "tax":
line.compute_all_tax = {}
line.compute_all_tax_dirty = False
continue
if line.display_type == "product" and line.move_id.is_invoice(True):
amount_currency = sign * line.price_unit * (1 - line.discount / 100)
handle_price_include = True
quantity = line.quantity
else:
amount_currency = line.amount_currency
handle_price_include = False
quantity = 1
compute_all_currency = line.tax_ids.with_context(
current_aml=line.id
).compute_all(
amount_currency,
currency=line.currency_id,
quantity=quantity,
product=line.product_id,
partner=line.move_id.partner_id or line.partner_id,
is_refund=line.is_refund,
handle_price_include=handle_price_include,
include_caba_tags=line.move_id.always_tax_exigible,
fixed_multiplicator=sign,
)
rate = line.amount_currency / line.balance if line.balance else 1
line.compute_all_tax_dirty = True
line.compute_all_tax = {
frozendict(
{
"tax_repartition_line_id": tax["tax_repartition_line_id"],
"group_tax_id": tax["group"] and tax["group"].id or False,
"account_id": tax["account_id"] or line.account_id.id,
"currency_id": line.currency_id.id,
"analytic_distribution": (
(tax["analytic"] or not tax["use_in_tax_closing"])
and line.move_id.state == "draft"
)
and line.analytic_distribution,
"tax_ids": [(6, 0, tax["tax_ids"])],
"tax_tag_ids": [(6, 0, tax["tag_ids"])],
"partner_id": line.move_id.partner_id.id or line.partner_id.id,
"move_id": line.move_id.id,
"display_type": line.display_type,
}
): {
"name": tax["name"]
+ (" " + _("(Discount)") if line.display_type == "epd" else ""),
"balance": tax["amount"] / rate,
"amount_currency": tax["amount"],
"tax_base_amount": tax["base"]
/ rate
* (-1 if line.tax_tag_invert else 1),
}
for tax in compute_all_currency["taxes"]
if tax["amount"]
}
if not line.tax_repartition_line_id:
line.compute_all_tax[frozendict({"id": line.id})] = {
"tax_tag_ids": [(6, 0, compute_all_currency["base_tags"])],
}
if not hasattr(AccountMoveLine, "_compute_all_tax_origin"):
AccountMoveLine._compute_all_tax_origin = AccountMoveLine._compute_all_tax
AccountMoveLine._compute_all_tax = _compute_all_tax_new

View file

@ -0,0 +1,8 @@
from . import avalara_salestax
from . import product
from . import partner
from . import account_move
from . import account_fiscal_position
from . import account_tax
from . import res_company
from . import avatax_rest_api

View file

@ -0,0 +1,7 @@
from odoo import fields, models
class FiscalPosition(models.Model):
_inherit = "account.fiscal.position"
is_avatax = fields.Boolean(string="Use Avatax API")

View file

@ -0,0 +1,532 @@
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import float_compare
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
"""Inherit to implement the tax calculation using avatax API"""
_inherit = "account.move"
@api.depends("partner_shipping_id", "partner_id", "company_id")
def _compute_onchange_exemption(self):
"""
Set the exemption to use for the Invoice.
An exemption can be applied if
there an exemption for the delivery address Country + State
- Get the delivery address Country & State
- Search the invoiced commercial partner addresses
for an exemption in this Country & State
- In case there is a "country wide" exemption, use it.
Example:
- ACME company partner, USA CA, has exemption status
- ACME Invoicing address, USA CA, no exemption status
- ACME Delivery address, USA CA, no exemption status
Invoice to ACME Invoicing, Shipped to ACME Delivery with be exempt.
For this to work properly, the "exemption_lock" is no longer supported.
"""
for invoice in self.filtered(lambda x: x.state == "draft"):
invoice_partner = invoice.partner_id.commercial_partner_id
ship_to_address = (
hasattr(invoice, "partner_shipping_id")
and invoice.partner_shipping_id
or invoice_partner
)
# Find an exemption address matching the Country + State
# of the Delivery address
exemption_addresses = (
invoice_partner | invoice_partner.child_ids
).filtered("property_tax_exempt")
exemption_address_naive = exemption_addresses.filtered(
lambda a: a.country_id == ship_to_address.country_id
and (
a.state_id == ship_to_address.state_id
or invoice_partner.property_exemption_country_wide
)
)[:1]
# Force Company to get the correct values from the Property fields
exemption_address = exemption_address_naive.with_company(
invoice.company_id.id
)
invoice.exemption_code = exemption_address.property_exemption_number
invoice.exemption_code_id = exemption_address.property_exemption_code_id
@api.onchange("warehouse_id")
def onchange_warehouse_id(self):
if self.warehouse_id:
if self.warehouse_id.company_id:
self.company_id = self.warehouse_id.company_id
if self.warehouse_id.code:
self.location_code = self.warehouse_id.code
is_avatax = fields.Boolean(related="fiscal_position_id.is_avatax")
invoice_doc_no = fields.Char(
"Source/Ref Invoice No",
readonly=True,
states={"draft": [("readonly", False)]},
help="Reference of the invoice",
)
exemption_code = fields.Char(
"Exemption Number",
compute=_compute_onchange_exemption,
readonly=False, # New computed writeable fields
store=True,
help="It show the customer exemption number",
)
exemption_code_id = fields.Many2one(
"exemption.code",
"Exemption Code",
compute=_compute_onchange_exemption,
readonly=False, # New computed writeable fields
store=True,
help="It show the customer exemption code",
)
exemption_locked = fields.Boolean(
help="Exemption code won't be automatically changed, "
"for instance, when changing the Customer."
)
tax_on_shipping_address = fields.Boolean(
"Tax based on shipping address", default=True
)
tax_address_id = fields.Many2one(
"res.partner", "Tax Shipping Address", compute="_compute_tax_address_id"
)
location_code = fields.Char(readonly=True, states={"draft": [("readonly", False)]})
warehouse_id = fields.Many2one("stock.warehouse", "Warehouse")
avatax_amount = fields.Float(string="AvaTax", copy=False)
calculate_tax_on_save = fields.Boolean()
so_partner_id = fields.Many2one(comodel_name="res.partner", string="SO Partner")
avatax_request_log = fields.Text(
"Avatax API Request Log", readonly=True, copy=False
)
avatax_response_log = fields.Text(
"Avatax API Response Log", readonly=True, copy=False
)
@api.model
@api.depends("company_id")
def _compute_hide_exemption(self):
avatax_config = self.env.company.get_avatax_config_company()
for inv in self:
inv.hide_exemption = avatax_config.hide_exemption
hide_exemption = fields.Boolean(
"Hide Exemption & Tax Based on shipping address",
compute=_compute_hide_exemption, # For past transactions visibility
default=lambda self: self.env.company.get_avatax_config_company,
help="Uncheck the this field to show exemption fields on SO/Invoice form view. "
"Also, it will show Tax based on shipping address button",
)
@api.depends("tax_on_shipping_address", "partner_id", "partner_shipping_id")
def _compute_tax_address_id(self):
for invoice in self:
invoice.tax_address_id = (
invoice.partner_shipping_id
if invoice.tax_on_shipping_address
else invoice.partner_id
)
@api.onchange("tax_address_id", "fiscal_position_id")
def onchange_reset_avatax_amount(self):
"""
When changing quantities or prices, reset the Avatax computed amount.
The Odoo computed tax amount will then be shown, as a reference.
The Avatax amount will be recomputed upon document validation.
"""
for inv in self:
inv.avatax_amount = 0
for line in inv.invoice_line_ids:
line.avatax_amt_line = 0
# Same as v12
def get_origin_tax_date(self):
if self.invoice_doc_no:
orig_invoice = self.search(
[
("name", "=", self.invoice_doc_no),
("partner_id", "=", self.partner_id.id),
]
)
return orig_invoice.invoice_date
return False
# Same as v12
def _get_avatax_doc_type(self, commit=True):
self.ensure_one()
avatax_config = self.company_id.get_avatax_config_company()
if avatax_config.disable_tax_reporting:
commit = False
if "refund" in self.move_type:
doc_type = "ReturnInvoice" if commit else "ReturnOrder"
else:
doc_type = "SalesInvoice" if commit else "SalesOrder"
return doc_type
# Same as v12
def _avatax_prepare_lines(self, doc_type=None):
"""
Prepare the lines to use for Avatax computation.
Returns a list of dicts
"""
sign = 1 if self.move_type.startswith("out") else -1
lines = [
line._avatax_prepare_line(sign, doc_type)
for line in self.invoice_line_ids
if line.price_subtotal or line.quantity
]
return [x for x in lines if x]
def update_tax_details(self, tax, line, tax_result_line):
"""Method to update details in tax"""
return tax, line
# Same as v12
def _avatax_compute_tax(self, commit=False):
"""Contact REST API and recompute taxes for a Sale Order"""
# Override to handle lines with split taxes (e.g. TN)
self and self.ensure_one()
avatax_config = self.company_id.get_avatax_config_company()
if not avatax_config:
# Skip Avatax computation if no configuration is found
return
doc_type = self._get_avatax_doc_type(commit=commit)
tax_date = self.get_origin_tax_date() or self.invoice_date
taxable_lines = self._avatax_prepare_lines(doc_type)
tax_result = avatax_config.create_transaction(
self.invoice_date or fields.Date.today(),
self.name,
doc_type,
(
self.so_partner_id
if self.so_partner_id and avatax_config.use_so_partner_id
else self.partner_id
),
self.warehouse_id.partner_id or self.company_id.partner_id,
self.tax_address_id or self.partner_id,
taxable_lines,
self.user_id,
self.exemption_code or None,
self.exemption_code_id.code or None,
commit,
tax_date,
# TODO: can we report self.invoice_doc_no?
self.name if self.move_type == "out_refund" else "",
self.location_code or "",
is_override=self.move_type == "out_refund",
currency_id=self.currency_id,
ignore_error=300 if commit else None,
log_to_record=self,
)
# If commiting, and document exists, try unvoiding it
# Error number 300 = GetTaxError, Expected Saved|Posted
if commit and tax_result.get("number") == 300:
_logger.info(
"Document %s (%s) already exists in Avatax. "
"Should be a voided transaction. "
"Unvoiding and re-commiting.",
self.name,
doc_type,
)
avatax_config.unvoid_transaction(self.name, doc_type)
avatax_config.commit_transaction(self.name, doc_type)
return tax_result
if self.state == "draft":
Tax = self.env["account.tax"]
tax_result_lines = {int(x["lineNumber"]): x for x in tax_result["lines"]}
taxes_to_set = {}
for line in self.invoice_line_ids:
tax_result_line = tax_result_lines.get(line.id)
if tax_result_line:
# rate = tax_result_line.get("rate", 0.0)
tax_calculation = 0.0
if tax_result_line["taxableAmount"]:
tax_calculation = (
tax_result_line["taxCalculated"]
/ tax_result_line["taxableAmount"]
)
rate = round(tax_calculation * 100, 4)
tax = Tax.get_avalara_tax(rate, doc_type)
tax, line = self.update_tax_details(tax, line, tax_result_line)
if tax and tax not in line.tax_ids:
line_taxes = line.tax_ids.filtered(lambda x: not x.is_avatax)
taxes_to_set[line.id] = line_taxes | tax
line.avatax_amt_line = tax_result_line["tax"]
self.with_context(check_move_validity=False).avatax_amount = tax_result[
"totalTax"
]
container = {"records": self}
# Set Taxes on lines in a way that properly triggers onchanges
# This same approach is also used by the official account_taxcloud connector
with self.with_context(
avatax_invoice=self, check_move_validity=False
)._sync_dynamic_lines(container), self.line_ids.mapped(
"move_id"
)._check_balanced(
container
):
for line_id in taxes_to_set.keys():
line = self.invoice_line_ids.filtered(lambda x: x.id == line_id)
line.write({"tax_ids": [(6, 0, [])]})
line.with_context(
avatax_invoice=self, check_move_validity=False
).write({"tax_ids": taxes_to_set.get(line_id).ids})
# After taxes are changed is needed to force compute taxes again, in 16 version
# change of tax doesn't trigger compute of taxes on header for unknown reason
self._compute_amount()
if float_compare(
self.amount_untaxed + max(self.amount_tax, abs(self.avatax_amount)),
self.amount_residual,
precision_rounding=self.currency_id.rounding or 0.001,
):
taxes_data = {
iline.id: iline.tax_ids for iline in self.invoice_line_ids
}
self.invoice_line_ids.write({"tax_ids": [(6, 0, [])]})
for line in self.invoice_line_ids:
line.write({"tax_ids": taxes_data[line.id].ids})
return tax_result
# Same as v13
def avatax_compute_taxes(self, commit=False):
"""
Called from Invoice's Action menu.
Forces computation of the Invoice taxes
"""
for invoice in self:
if (
invoice.move_type in ["out_invoice", "out_refund"]
and invoice.fiscal_position_id.is_avatax
and (invoice.state == "draft" or commit)
):
invoice._avatax_compute_tax(commit=commit)
return True
def avatax_commit_taxes(self):
for invoice in self:
avatax_config = invoice.company_id.get_avatax_config_company()
if not avatax_config.disable_tax_reporting:
doc_type = invoice._get_avatax_doc_type()
avatax_config.commit_transaction(invoice.name, doc_type)
return True
def is_avatax_calculated(self):
"""
Only apply Avatax for these types of documents.
Can be extended to support other types.
"""
return self.is_sale_document()
def _post(self, soft=True):
for invoice in self:
if invoice.is_avatax_calculated():
avatax_config = self.company_id.get_avatax_config_company()
if avatax_config and avatax_config.force_address_validation:
for addr in [self.partner_id, self.partner_shipping_id]:
if not addr.date_validation:
# The Validate action will be interrupted
# if the address is not validated
raise UserError(_("Avatax address is not validated!"))
# We should compute taxes before validating the invoice
# to ensure correct account moves
# However, we can't save the invoice because it wasn't assigned a
# number yet
invoice.avatax_compute_taxes(commit=False)
res = super()._post(soft=soft)
for invoice in res:
if invoice.is_avatax_calculated():
# We can only commit to Avatax after validating the invoice
# because we need the generated Invoice number
invoice.avatax_compute_taxes(commit=True)
return res
# prepare_return in v12
def _reverse_move_vals(self, default_values, cancel=True):
# OVERRIDE
# Don't keep anglo-saxon lines if not cancelling an existing invoice.
move_vals = super(AccountMove, self)._reverse_move_vals(
default_values, cancel=cancel
)
move_vals.update(
{
"invoice_doc_no": self.name,
"invoice_date": default_values
and default_values.get("invoice_date")
or self.invoice_date,
"tax_on_shipping_address": self.tax_on_shipping_address,
"warehouse_id": self.warehouse_id.id,
"location_code": self.location_code,
"exemption_code": self.exemption_code or "",
"exemption_code_id": self.exemption_code_id.id or None,
"tax_address_id": self.tax_address_id.id,
}
)
return move_vals
# action_cancel in v12
def button_draft(self):
"""
Sets invoice to Draft, either from the Posted or Cancelled states
"""
posted_invoices = self.filtered(
lambda invoice: invoice.move_type in ["out_invoice", "out_refund"]
and invoice.fiscal_position_id.is_avatax
and invoice.state == "posted"
)
res = super(AccountMove, self).button_draft()
for invoice in posted_invoices:
avatax_config = invoice.company_id.get_avatax_config_company()
if avatax_config:
doc_type = invoice._get_avatax_doc_type()
avatax_config.void_transaction(invoice.name, doc_type)
return res
@api.onchange(
"invoice_line_ids",
"warehouse_id",
"tax_address_id",
"tax_on_shipping_address",
"partner_id",
)
def onchange_avatax_calculation(self):
avatax_config = self.env.company.get_avatax_config_company()
self.calculate_tax_on_save = False
if avatax_config.invoice_calculate_tax:
if (
self._origin.warehouse_id != self.warehouse_id
or self._origin.tax_address_id.street != self.tax_address_id.street
or self._origin.partner_id != self.partner_id
or self._origin.tax_on_shipping_address != self.tax_on_shipping_address
):
self.calculate_tax_on_save = True
return
for line in self.invoice_line_ids:
if (
line._origin.price_unit != line.price_unit
or line._origin.discount != line.discount
or line._origin.quantity != line.quantity
) and line.display_type == "product":
self.calculate_tax_on_save = True
break
def write(self, vals):
result = super(AccountMove, self).write(vals)
avatax_config = self.env.company.get_avatax_config_company()
for record in self:
if (
avatax_config.invoice_calculate_tax
and record.calculate_tax_on_save
and record.state == "draft"
and not self._context.get("skip_second_write", False)
):
record.with_context(skip_second_write=True).write(
{"calculate_tax_on_save": False}
)
record.avatax_compute_taxes()
return result
@api.model_create_multi
def create(self, vals_list):
moves = super().create(vals_list)
avatax_config = self.env.company.get_avatax_config_company()
for move in moves:
if (
avatax_config.invoice_calculate_tax
and move.calculate_tax_on_save
and not self._context.get("skip_second_write", False)
):
move.with_context(skip_second_write=True).write(
{"calculate_tax_on_save": False}
)
move.avatax_compute_taxes()
return moves
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
avatax_amt_line = fields.Float(string="AvaTax Line", copy=False)
def _get_avatax_amount(self, qty=None):
"""
Return the company currency line amount, after discounts,
to use for Tax calculation.
Can be used to compute unit price only, using qty=1.
Code extracted from account/models/account_move.py,
from the compute_base_line_taxes() nested function,
adjusted to compute line amount instead of unit price.
"""
self.ensure_one()
base_line = self
move = base_line.move_id
sign = -1 if move.is_inbound() else 1
quantity = qty or base_line.quantity
base_amount = base_line.price_unit * quantity
if base_line.currency_id:
price_unit_foreign_curr = (
sign * base_amount * (1 - (base_line.discount / 100.0))
)
price_unit_comp_curr = base_line.currency_id._convert(
price_unit_foreign_curr,
move.company_id.currency_id,
move.company_id,
move.date,
)
else:
price_unit_comp_curr = (
sign * base_amount * (1 - (base_line.discount / 100.0))
)
return -price_unit_comp_curr
# Same in v12
def _avatax_prepare_line(self, sign=1, doc_type=None):
"""
Prepare a line to use for Avatax computation.
Returns a dict
"""
line = self
res = {}
# Add UPC to product item code
avatax_config = line.company_id.get_avatax_config_company()
product = line.product_id
if product.barcode and avatax_config.upc_enable:
item_code = "UPC:%d" % product.barcode
else:
item_code = product.default_code or ("ID:%d" % product.id)
tax_code = line.product_id.applicable_tax_code_id.name
amount = sign * line._get_avatax_amount()
if line.quantity < 0:
amount = -amount
res = {
"qty": line.quantity,
"itemcode": item_code,
"description": line.name,
"amount": amount,
"tax_code": tax_code,
"id": line,
"account_id": line.account_id.id,
"tax_id": line.tax_ids,
}
return res
@api.onchange("price_unit", "discount", "quantity")
def onchange_reset_tax_amt(self):
"""
When changing quantities or prices, reset the Avatax computed amount.
The Odoo computed tax amount will then be shown, as a reference.
The Avatax amount will be recomputed upon document validation.
"""
for line in self:
line.avatax_amt_line = 0.0
line.move_id.avatax_amount = 0.0

View file

@ -0,0 +1,131 @@
from math import copysign
from odoo import _, api, exceptions, fields, models
from odoo.tools.float_utils import float_compare
class AccountTax(models.Model):
"""Inherit to implement the tax using avatax API"""
_inherit = "account.tax"
is_avatax = fields.Boolean()
@api.model
def _get_avalara_tax_domain(self, tax_rate, doc_type):
return [
("amount", "=", tax_rate),
("is_avatax", "=", True),
(
"company_id",
"=",
self.env.company.id,
),
]
@api.model
def _get_avalara_tax_name(self, tax_rate, doc_type=None):
return _("{}%*").format(str(tax_rate))
@api.model
def get_avalara_tax(self, tax_rate, doc_type):
domain = self._get_avalara_tax_domain(tax_rate, doc_type)
tax = self.with_context(active_test=False).search(domain, limit=1)
if tax and not tax.active:
tax.active = True
if not tax:
domain = self._get_avalara_tax_domain(0, doc_type)
tax_template = self.search(domain, limit=1)
if not tax_template:
raise exceptions.UserError(
_("Please configure Avatax Tax for Company %s:")
% self.env.company.name
)
# If you get a unique constraint error here,
# check the data for your existing Avatax taxes.
vals = {
"amount": tax_rate,
"name": self._get_avalara_tax_name(tax_rate, doc_type),
}
tax = tax_template.sudo().copy(default=vals)
# Odoo core does not use the name set in default dict
tax.name = vals.get("name")
return tax
def compute_all(
self,
price_unit,
currency=None,
quantity=1.0,
product=None,
partner=None,
is_refund=False,
handle_price_include=True,
include_caba_tags=False,
fixed_multiplicator=1,
):
"""
Adopted as the central point to inject custom tax computations.
Avatax logic is triggered if the "avatax_invoice" is set in the context.
To find the Avatax amount, we search an Invoice line with the same
quantity, price and product.
"""
res = super().compute_all(
price_unit,
currency,
quantity,
product,
partner,
is_refund,
handle_price_include,
include_caba_tags,
fixed_multiplicator,
)
avatax_invoice = self.env.context.get("avatax_invoice")
current_aml = False
if "current_aml" in self.env.context:
current_aml = self.env["account.move.line"].browse(
self.env.context.get("current_aml")
)
if not (
current_aml.display_type == "product"
and current_aml.account_type != "asset_receivable"
):
avatax_invoice = False
if avatax_invoice:
# Find the Avatax amount in the invoice Lines
# Looks up the line for the current product, price_unit, and quantity
# Note that the price_unit used must consider discount
base = res["total_excluded"]
digits = 6
avatax_amount = None
if current_aml:
avatax_amount = copysign(current_aml.avatax_amt_line, base)
else:
for line in avatax_invoice.invoice_line_ids:
price_unit = line.currency_id._convert(
price_unit,
avatax_invoice.company_id.currency_id,
avatax_invoice.company_id,
avatax_invoice.date,
)
if (
line.product_id == product
and float_compare(line.quantity, quantity, digits) == 0
):
avatax_amount = copysign(line.avatax_amt_line, base)
break
if avatax_amount is None:
avatax_amount = 0.0
raise exceptions.UserError(
_(
"Incorrect retrieval of Avatax amount for Invoice %(avatax_invoice)s:"
" product %(product.display_name)s, price_unit %(-price_unit)f"
" , quantity %(quantity)f"
)
)
for tax_item in res["taxes"]:
if tax_item["amount"] != 0:
tax_item["amount"] = avatax_amount
res["total_included"] = base + avatax_amount
return res

View file

@ -0,0 +1,328 @@
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from .avatax_rest_api import AvaTaxRESTService
_logger = logging.getLogger(__name__)
class ExemptionCode(models.Model):
_name = "exemption.code"
_description = "Exemption Code"
name = fields.Char(required=True)
code = fields.Char()
@api.depends("name", "code")
def name_get(self):
def name(r):
return r.code and "({}) {}".format(r.code, r.name) or r.name
return [(r.id, name(r)) for r in self]
class AvalaraSalestax(models.Model):
_name = "avalara.salestax"
_description = "AvaTax Configuration"
_rec_name = "account_number"
@api.model
def _get_avatax_supported_countries(self):
"""Returns the countries supported by AvaTax Address Validation Service."""
return self.env["res.country"].search([("code", "in", ["US", "CA"])])
account_number = fields.Char(
name="Account ID", required=True, help="Account Number provided by AvaTax"
)
license_key = fields.Char(required=True, help="License Key provided by AvaTax")
service_url = fields.Selection(
[
("https://rest.avatax.com/api/v2", "Production (REST API)"),
("https://sandbox-rest.avatax.com/api/v2", "Sandbox (REST API)"),
],
string="Service URL",
default="https://rest.avatax.com/api/v2",
help="The url to connect with",
)
request_timeout = fields.Integer(
default=300,
help="Defines AvaTax request time out length"
", AvaTax best practices prescribes default setting of 300 seconds",
)
company_code = fields.Char(
default="DEFAULT",
required=True,
help="The company code as defined in the Admin Console of AvaTax",
)
logging = fields.Boolean(
"Log API Requests",
help="Enables detailed AvaTax transaction logging within application",
)
result_in_uppercase = fields.Boolean(
"Return validation results in upper case",
help="The address validation results are returned in in upper case",
)
disable_address_validation = fields.Boolean(
help="Disables the ability to perform address validation"
)
validation_on_save = fields.Boolean(
"Automatic Address Validation",
help="Automatically validates addresses when they are created or modified",
)
force_address_validation = fields.Boolean(
"Require Validated Addresses",
help="Only compute taxes if addresses were validated by the Avatax service",
)
auto_generate_customer_code = fields.Boolean(
"Automatically generate missing customer code",
default=True,
help="This will generate customer code for the customer used in the "
"transaction, if it doesn't have one already. "
"Each code is unique per customer. "
"When this is disabled, you will have to manually go to each customer "
"and manually generate their customer code. "
"This is required for Avatax and is only generated one time.",
)
disable_tax_calculation = fields.Boolean(
"Disable AvaTax Calculation",
help="No tax calculation requests will be sent to the AvaTax web service.",
)
# TODO: Control - Disable Document Recording
# In order for this connector to be used in conjunction
# with other integrations to AvaTax, the user must be able to control which connector
# is used for recording documents to AvaTax.
# From a technical standpoint, simply use DocType: 'SalesOrder' on all calls
# and suppress any non-getTax calls (i.e. cancelTax, postTax).
disable_tax_reporting = fields.Boolean(
"Disable Document Recording/Commiting",
help="No transactions will be recorded (commited) to the Avatax service.",
)
country_ids = fields.Many2many(
"res.country",
"avalara_salestax_country_rel",
"avalara_salestax_id",
"country_id",
"Countries",
default=_get_avatax_supported_countries,
help="Countries where address validation will be used",
)
active = fields.Boolean(
default=True,
help="Uncheck the active field to hide the record",
)
company_id = fields.Many2one(
"res.company",
"Company",
required=True,
default=lambda self: self.env.company,
help="Company which has subscribed to the AvaTax service",
)
company_partner_id = fields.Many2one(
string="Company Address",
related="company_id.partner_id",
)
upc_enable = fields.Boolean(
"Enable UPC Taxability",
help="Allows ean13 to be reported in place of Item Reference"
" as upc identifier.",
)
invoice_calculate_tax = fields.Boolean(
"Auto Calculate Tax on Invoice Save",
help="Automatically triggers API to calculate tax If changes made on"
"Invoice's warehouse_id, tax_on_shipping_address, "
"Invoice line's price_unit, discount, quantity",
)
use_so_partner_id = fields.Boolean(
string="Use Sale Customer Code on Invoice",
help="Use Sales Order's Customer field to determine Taxable "
"Status on the Customer Invoice. If no Sales Order exists, "
"Customer field on the invoice form view will be used instead",
)
hide_exemption = fields.Boolean(
"Hide Exemption & Tax Based on shipping address",
default=False,
help="Uncheck the this field to show exemption fields on SO/Invoice form view. "
"Also, it will show Tax based on shipping address button",
)
# TODO: add option to Display Prices with Tax Included
# Enabled the tax inclusive flag in the GetTax Request.
# constraints on uniq records creation with account_number and company_id
_sql_constraints = [
(
"code_company_uniq",
"unique (company_code)",
"Avalara setting is already available for this company code",
),
(
"account_number_company_uniq",
"unique (account_number, company_id)",
"The account number must be unique per company!",
),
]
def get_avatax_rest_service(self):
self.ensure_one()
if self.disable_tax_calculation:
_logger.info(
"Avatax tax calculation is disabled, skipping Avatax API contact."
)
return False
return AvaTaxRESTService(
self.account_number,
self.license_key,
self.service_url,
self.request_timeout,
self.logging,
config=self,
)
def create_transaction(
self,
doc_date,
doc_code,
doc_type,
partner,
ship_from_address,
shipping_address,
lines,
user=None,
exemption_number=None,
exemption_code_name=None,
commit=False,
invoice_date=None,
reference_code=None,
location_code=None,
is_override=None,
currency_id=None,
ignore_error=None,
log_to_record=False,
):
self.ensure_one()
avatax_config = self
currency_code = self.env.company.currency_id.name
if currency_id:
currency_code = currency_id.name
if not partner.customer_code:
if not avatax_config.auto_generate_customer_code:
raise UserError(
_(
"Customer Code for customer %(partner.name)s not defined.\n\n "
"You can edit the Customer Code in customer profile. "
'You can fix by clicking "Generate Customer Code" '
"button in the customer contact information"
)
)
else:
partner.generate_cust_code()
if not shipping_address:
raise UserError(
_("There is no source shipping address defined for partner %s.")
% partner.name
)
if not ship_from_address:
raise UserError(_("There is no Company address defined."))
if avatax_config.validation_on_save:
for address in [partner, shipping_address, ship_from_address]:
if not address.date_validation:
address.multi_address_validation(validation_on_save=True)
# this condition is required, in case user select force address validation
# on AvaTax API Configuration
if (
avatax_config.force_address_validation
and not avatax_config.disable_address_validation
):
if not shipping_address.date_validation:
raise UserError(
_(
"Please validate the shipping address for the partner %(partner.name)s."
)
)
# if not avatax_config.address_validation:
if not ship_from_address.date_validation:
raise UserError(_("Please validate the origin warehouse address."))
if avatax_config.disable_tax_calculation:
_logger.info(
"Avatax tax calculation is disabled. Skipping %s %s.",
doc_code,
doc_type,
)
return False
if commit and avatax_config.disable_tax_reporting:
_logger.warning(
_("Avatax commiting document %s, but it tax reporting is disabled."),
doc_code,
)
avatax = self.get_avatax_rest_service()
result = avatax.get_tax(
avatax_config.company_code,
doc_date,
doc_type,
partner.customer_code,
doc_code,
ship_from_address,
shipping_address,
lines,
exemption_number,
exemption_code_name,
user and user.name or None,
commit and not avatax_config.disable_tax_reporting,
invoice_date,
reference_code,
location_code,
currency_code,
partner.vat or None,
is_override,
ignore_error=ignore_error,
log_to_record=log_to_record,
)
return result
def commit_transaction(self, doc_code, doc_type):
self.ensure_one()
result = False
if not self.disable_tax_reporting:
avatax = self.get_avatax_rest_service()
result = avatax.call(
"commit_transaction", self.company_code, doc_code, {"commit": True}
)
return result
def void_transaction(self, doc_code, doc_type):
if self:
self.ensure_one()
result = False
if not self.disable_tax_reporting:
avatax = self.get_avatax_rest_service()
result = avatax.call(
"void_transaction",
self.company_code,
doc_code,
{"code": "DocVoided"},
)
return result
def unvoid_transaction(self, doc_code, doc_type):
self.ensure_one()
result = False
if not self.disable_tax_reporting:
avatax = self.get_avatax_rest_service()
result = avatax.call("unvoid_transaction", self.company_code, doc_code)
return result
def ping(self):
client = AvaTaxRESTService(config=self)
client.ping()
return True

View file

@ -0,0 +1,349 @@
# Copyright (C) 2020 Open Source Integrators
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import logging
import pprint
import socket
from odoo import _, fields, tools
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
from avalara import AvataxClient
except Exception:
_logger.info("AvataxClient missing")
class AvaTaxRESTService:
def __init__(
self,
username=None,
password=None,
url=None,
timeout=300,
enable_log=False,
config=None,
):
self.config = config
self.timeout = not config and timeout or config.request_timeout
self.is_log_enabled = enable_log or config and config.logging
# Set elements adapter defaults
self.appname = "Odoo 15 - Open Source Integrators/OCA"
self.version = "a0o5a000007SPdsAAG"
self.hostname = socket.gethostname()
url = url or (config and config.service_url) or ""
self.environment = (
"sandbox" if "sandbox" in url or "development" in url else "production"
)
username = username or (config and config.account_number) or False
password = password or (config and config.license_key) or False
if username and password:
try:
self.client = AvataxClient(
self.appname, self.version, self.hostname, self.environment
)
except NameError as exc:
raise UserError(
_(
"AvataxClient is not available in your system. "
"Please contact your system administrator "
"to 'pip3 install Avalara'"
)
) from exc
self.client.add_credentials(username, password)
def _sanitize_text(self, text):
res = (
text.replace("/", "_-ava2f-_")
.replace("+", "_-ava2b-_")
.replace("?", "_-ava3f-_")
.replace(" ", "%20")
)
return res
def get_result(self, response, ignore_error=None):
# To call from validate address and from compute tax
result = response.json()
if self.is_log_enabled:
_logger.info("Response\n" + pprint.pformat(result, indent=1))
if result.get("messages") or result.get("error"):
messages = result.get("messages") or result.get("error", {}).get("details")
if ignore_error and messages and messages[0].get("number") == ignore_error:
return messages[0]
for w_message in messages:
if w_message.get("severity") in ("Error", "Exception"):
if w_message.get("refersTo", "").startswith("Address"):
raise UserError(
_(
"AvaTax: Warning AvaTax could not validate the"
" address:\n%s\n\n"
"You can save the address and AvaTax will make an"
" attempt to "
"compute taxes based on the zip code if"
' "Force Address Validation" is disabled '
"in the Avatax connector configuration. \n\n "
"Also please ensure that the company address is"
" set and Validated. "
"You can get there by going to Sales->Customers "
'and removing "Customers" filter from the search'
" at the top. "
"Then go to your company contact info and validate"
" your address in the Avatax Tab"
)
% str(", ".join(result.get("address", {}).values()))
)
elif w_message.get("refersTo") == "Country":
raise UserError(
_(
"AvaTax: Notice\n\n Address Validation for this"
" country not supported. "
"But, Avalara will still calculate global tax"
" rules."
)
)
else:
message = "AvaTax: Error: "
if w_message.get("refersTo"):
message += str(w_message.get("refersTo")) + "\n\n"
elif w_message.get("code"):
message += str(w_message.get("code")) + "\n\n"
if w_message.get("summary"):
message += "Summary: " + str(w_message.get("summary"))
elif w_message.get("message"):
message += "Message: " + str(w_message.get("message"))
if w_message.get("details"):
message += "\n Details: " + str(
w_message.get("details", "")
)
elif w_message.get("description"):
message += "\n Description: " + str(
w_message.get("description", "")
)
message += "\n Severity: " + str(w_message.get("severity"))
raise UserError(_(message))
return result
def ping(self):
response = self.client.ping()
res = response.json()
if self.is_log_enabled:
_logger.info(pprint.pformat(res, indent=1))
if not res.get("authenticated"):
raise UserError(_("The user or account could not be authenticated"))
return res
def validate_rest_address(
self, street, street2, city, zip_code, state_code, country_code
):
if self.config.disable_address_validation:
raise UserError(
_(
"The AvaTax Address Validation Service"
" is disabled by the administrator."
" Please make sure it's enabled for the address validation"
)
)
supported_countries = [x.code for x in self.config.country_ids]
if country_code and country_code not in supported_countries:
raise UserError(
_(
"The AvaTax Address Validation Service does not support"
" this country in the configuration,"
" please continue with your normal process."
)
)
textcase = "Upper" if self.config.result_in_uppercase else "Mixed"
partner_data = {
"line1": street or "",
"line2": street2 or "",
"city": city or "",
"postalCode": zip_code or "",
"region": state_code or "",
"country": country_code or "",
"textcase": textcase,
}
response_partner = self.client.resolve_address(partner_data)
partner_dict = self.get_result(response_partner)
valid_address = partner_dict.get("validatedAddresses")[0]
Partner = self.config.env["res.partner"]
country = Partner.get_country_from_code(valid_address.get("country"))
state = Partner.get_state_from_code(
valid_address.get("region"), valid_address.get("country")
)
address_vals = {
"street": valid_address.get("line1", ""),
"street2": valid_address.get("line2", ""),
"city": valid_address.get("city", ""),
"zip": valid_address.get("postalCode", ""),
"country_id": country.id,
"state_id": state.id,
"date_validation": fields.Date.today(),
"validation_method": "avatax",
"partner_latitude": valid_address.get("latitude"),
"partner_longitude": valid_address.get("longitude"),
}
return address_vals
def _enrich_result_lines_with_tax_rate(self, avatax_result):
"""
Enrich Avatax result with Odoo tax computation.
Tax details can have a tax rate with zero tax amount.
In this case the tax rate should be ignored.
result is a dict with a 'createTransactionModel' returned by Avatax
"""
for line in avatax_result.get("lines", []):
line["rate"] = (
round(
sum(x["rate"] for x in line["details"] if x and x.get("tax")) * 100,
4,
)
or 0.0
)
return avatax_result
def _get_tax_post_process(self, data, result, doc_type):
"""Inherit if needed"""
return True
def get_tax(
self,
company_code,
doc_date,
doc_type,
partner_code,
doc_code,
origin,
destination,
received_lines,
exemption_no=None,
customer_usage_type=None,
salesman_code=None,
commit=False,
invoice_date=None,
reference_code=None,
location_code=None,
currency_code="USD",
vat=None,
is_override=False,
ignore_error=None,
log_to_record=False,
):
"""Create tax request and get tax amount by customer address
@currency_code : 'USD' is the default currency code for avalara,
if user not specify in the own company
return information about how the tax was calculated. Intended
for use only while the SDK is in a development environment.
"""
if not origin.street:
raise UserError(
_(
"Please set the Company Address "
"in the partner information and validate. "
"We are checking against the first line of the address "
"and it's empty. \n\n "
"Typically located in Sales->Customers, "
'you have to clear "Customers" '
"from search filter and type in your own company name. "
"Ensure the address is filled out "
"and go to Avatax tab in the partner information "
"and validate the address. Save partner update when done."
)
)
lineslist = [
{
"number": line["id"].id,
"description": tools.ustr(line.get("description", ""))[:255],
"itemCode": line.get("itemcode"),
"quantity": line.get("qty", 1),
"amount": line.get("amount", 0.0),
"taxCode": line.get("tax_code"),
}
for line in received_lines
]
if doc_date and type(doc_date) != str:
doc_date = fields.Date.to_string(doc_date)
create_transaction = {
"addresses": {
"shipFrom": {
"city": origin.city,
"country": origin.country_id.code or None,
"line1": origin.street or None,
"postalCode": origin.zip,
"region": origin.state_id.code or None,
},
"shipTo": {
"city": destination.city,
"country": destination.country_id.code or None,
"line1": destination.street or None,
"postalCode": destination.zip,
"region": destination.state_id.code or None,
},
},
"lines": lineslist,
# 'purchaseOrderNo": "2020-02-05-001"
"companyCode": company_code,
"currencyCode": currency_code,
"customerCode": partner_code,
"businessIdentificationNo": vat,
"referenceCode": reference_code,
"salespersonCode": salesman_code and salesman_code[:25] or None,
"reportingLocationCode": location_code,
"entityUseCode": customer_usage_type,
"exemptionNo": exemption_no,
"description": doc_code or "Draft",
"date": doc_date,
"code": doc_code,
"type": doc_type,
"commit": commit,
}
if is_override and invoice_date:
create_transaction.update(
{
"taxOverride": {
"type": "TaxDate",
"taxAmount": 0,
"taxDate": fields.Date.to_string(invoice_date),
"reason": "Return Items",
}
}
)
data = {"createTransactionModel": create_transaction}
if self.is_log_enabled:
_logger.info(
"Request CreateOrAdjustTransaction %s %s (commit %s)\n%s",
doc_type,
doc_code,
commit,
pprint.pformat(data, indent=1),
)
response = self.client.create_or_adjust_transaction(data)
result = self.get_result(response, ignore_error=ignore_error)
if log_to_record:
log_to_record.avatax_request_log = pprint.pformat(data, indent=1)
log_to_record.avatax_response_log = pprint.pformat(result, indent=1)
self._get_tax_post_process(data, result, doc_type)
return self._enrich_result_lines_with_tax_rate(result)
def call(self, endpoint, company_code, doc_code, model=None, params=None):
if self.is_log_enabled:
_logger.info(
"Request Call %s(%s, %s, %s, %s)",
endpoint,
company_code,
doc_code,
model,
params,
)
company_code = self._sanitize_text(company_code)
doc_code = self._sanitize_text(doc_code)
endpoint_method = getattr(self.client, endpoint)
if params:
response = endpoint_method(company_code, doc_code, model, params)
else:
response = endpoint_method(company_code, doc_code, model)
result = self.get_result(response)
return result

View file

@ -0,0 +1,214 @@
import logging
import time
from random import random
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from .avatax_rest_api import AvaTaxRESTService
_LOGGER = logging.getLogger(__name__)
class ResPartner(models.Model):
"""
Update partner information by adding new fields
according to Avalara partner configuration
"""
_inherit = "res.partner"
date_validation = fields.Date(
"Last Validation Date",
readonly=True,
copy=False,
help="The date the address was last validated by AvaTax and accepted",
)
validation_method = fields.Selection(
[("avatax", "AVALARA"), ("usps", "USPS"), ("other", "Other")],
"Address Validation Method",
readonly=True,
copy=False,
help="It gets populated when the address is validated by the method",
)
validated_on_save = fields.Boolean(
help="Indicates if the address is already validated on save"
" before calling the wizard",
)
customer_code = fields.Char(copy=False)
tax_exempt = fields.Boolean(
"Is Tax Exempt (Deprecated))",
)
exemption_number = fields.Char(
"Exemption Number (Deprecated)",
)
exemption_code_id = fields.Many2one(
"exemption.code",
"Exemption Code (Deprecated)",
)
property_tax_exempt = fields.Boolean(
"Is Tax Exempt",
company_dependent=True,
help="This company or address can claim for tax exemption",
)
property_exemption_number = fields.Char(
"Exemption Number",
company_dependent=True,
help="The State identification number relevant fot the exemption",
)
property_exemption_code_id = fields.Many2one(
"exemption.code",
"Exemption Code",
company_dependent=True,
help="The type of exemption granted",
)
_sql_constraints = [
("name_uniq", "unique(customer_code)", "Customer Code must be unique!"),
]
@api.depends(
"property_tax_exempt", "property_exemption_code_id", "property_exemption_number"
)
def check_exemption_number(self):
"""
When tax exempt check then atleast exemption number
or exemption code should be filled
"""
for partner in self:
if partner.property_tax_exempt and not (
partner.property_exemption_code_id or partner.property_exemption_number
):
raise UserError(
_(
"Please enter either Exemption Number or Exemption Code"
" for marking customer as Exempt."
)
)
def _get_avatax_customer_code(self):
self.ensure_one()
return "%d-%d-Cust-%d" % (
int(time.time()),
int(random() * 10),
self.id,
)
def generate_cust_code(self):
"Auto populate customer code"
for partner in self:
partner.customer_code = partner._get_avatax_customer_code()
return True
@api.onchange("tax_exempt")
def onchange_tax_exemption(self):
if not self.property_tax_exempt:
self.property_exemption_number = ""
self.property_exemption_code_id = None
def get_state_from_code(self, state_code, country_code):
"""Returns the state from the code."""
state = self.env["res.country.state"].search(
[("code", "=", state_code), ("country_id.code", "=", country_code)],
)
return state
def get_country_from_code(self, code):
"""Returns the country from the code."""
country = self.env["res.country"].search([("code", "=", code)])
return country
def get_valid_address_vals(self, validation_on_save=False):
self.ensure_one()
partner = self
# For automatic validation on save, skip
# if no relevant address details are given
if validation_on_save and not (
partner.city or partner.zip or partner.country_id
):
_LOGGER.info(
"Skipping address validation for %d %s, not enough details.",
partner.id,
partner.display_name,
)
return False
avatax_config = self.env.company.get_avatax_config_company()
# Skip automatic validation for countries not supported by Avatax
supported_countries = [x.code for x in avatax_config.country_ids]
country_code = partner.country_id.code
if validation_on_save and country_code not in supported_countries:
_LOGGER.info(
"Skipping automatic address validation for %d %s"
", country %s not supported.",
partner.id,
partner.display_name,
country_code,
)
return False
avatax_restpoint = AvaTaxRESTService(config=avatax_config)
valid_address = avatax_restpoint.validate_rest_address(
partner.street,
partner.street2,
partner.city,
partner.zip,
partner.state_id.code,
partner.country_id.code,
)
return valid_address
def multi_address_validation(self, validation_on_save=False):
for partner in self:
if not (partner.parent_id and partner.type == "contact"):
valid_address = partner.get_valid_address_vals(
validation_on_save=validation_on_save
)
if valid_address:
partner.write(valid_address)
return True
def button_avatax_validate_address(self):
"""Method is used to verify of state and country"""
view_ref = self.env.ref(
"account_avatax_oca.view_avalara_salestax_address_validate"
)
ctx = self.env.context.copy()
ctx.update({"active_ids": self.ids, "active_id": self.id})
return {
"type": "ir.actions.act_window",
"name": "Address Validation",
"binding_view_types": "form",
"view_mode": "form",
"view_id": view_ref.id,
"res_model": "avalara.salestax.address.validate",
"nodestroy": True,
"res_id": False,
"target": "new",
"context": ctx,
}
@api.model_create_multi
def create(self, vals_list):
partners = super().create(vals_list)
avatax_config = self.env.company.get_avatax_config_company()
for partner in partners:
# Auto populate customer code, if not provided
if not partner.customer_code:
partner.generate_cust_code()
# Auto validate address, if enabled
if avatax_config.validation_on_save:
partner.multi_address_validation(validation_on_save=True)
partner.validated_on_save = True
return partners
def write(self, vals):
res = super(ResPartner, self).write(vals)
address_fields = ["street", "street2", "city", "zip", "state_id", "country_id"]
if not self.env.context.get("avatax_writing") and any(
x in vals for x in address_fields
):
partner = self.with_context(avatax_writing=True)
avatax_config = self.env.company.get_avatax_config_company()
if avatax_config.validation_on_save:
partner.multi_address_validation(validation_on_save=True)
partner.validated_on_save = True
return res

View file

@ -0,0 +1,64 @@
from odoo import fields, models
class ProductTaxCode(models.Model):
"""Define type of tax code:
@param type: product is use as product code,
@param type: freight is use for shipping code
@param type: service is use for service type product
"""
_name = "product.tax.code"
_description = "AvaTax Code"
name = fields.Char("Code", required=True)
description = fields.Char()
type = fields.Selection(
[
("product", "Product"),
("freight", "Freight"),
("service", "Service"),
("digital", "Digital"),
("other", "Other"),
],
required=True,
help="Type of tax code as defined in AvaTax",
)
class ProductTemplate(models.Model):
_inherit = "product.template"
tax_code_id = fields.Many2one(
"product.tax.code", "Product AvaTax Code", help="AvaTax Product Tax Code"
)
def _compute_applicable_tax_code(self):
for product in self:
product.applicable_tax_code_id = (
product.tax_code_id or product.categ_id.applicable_tax_code_id
)
applicable_tax_code_id = fields.Many2one(
"product.tax.code",
"Applicable AvaTax Code",
compute=_compute_applicable_tax_code,
)
class ProductCategory(models.Model):
_inherit = "product.category"
tax_code_id = fields.Many2one("product.tax.code", "AvaTax Code")
def _compute_applicable_tax_code(self):
for categ in self:
categ.applicable_tax_code_id = categ.tax_code_id
if not categ.applicable_tax_code_id and categ.parent_id:
categ.applicable_tax_code_id = categ.parent_id.applicable_tax_code_id
applicable_tax_code_id = fields.Many2one(
"product.tax.code",
"Applicable AvaTax Code",
compute=_compute_applicable_tax_code,
)

View file

@ -0,0 +1,28 @@
import logging
from odoo import _, models
_LOGGER = logging.getLogger(__name__)
class Company(models.Model):
_inherit = "res.company"
def get_avatax_config_company(self):
"""Returns the AvaTax configuration for the Company"""
if self:
self.ensure_one()
AvataxConfig = self.env["avalara.salestax"]
res = AvataxConfig.search(
[("company_id", "=", self.id), ("disable_tax_calculation", "=", False)]
)
if len(res) > 1:
_LOGGER.warning(
_("Company %s has too many Avatax configurations!"),
self.display_name,
)
if len(res) < 1:
_LOGGER.warning(
_("Company %s has no Avatax configuration."), self.display_name
)
return res and res[0]

View file

@ -0,0 +1,172 @@
To configure an Odoo company to use Avatax, follow these steps.
Note tha tsome of them might be configured out of the box
for the Odoo default company.
1. Configure AvaTax API Connection
2. Configure Company Taxes
3. Configure Customers
4. Configure Products
Configure Avatax API Connection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before you can configure the Odoo Avatax connector,
you will need some connection details ready:
- Login to https://home.avalara.com/
- Navigate to Settings >> All AvaTax Settings.
There you will see the company details.
- Take note of the Account ID and Company Code
- Navigate to Settings >> License and API Keys.
In the "Reset License Key" tab, click on the "Generate License Key" button,
and take note of it.
To configure AvaTax connector in Odoo:
- Navigate to: Accounting/Invoicing App >> Configuration >> AvaTax >> AvaTax API
- Click on the Create button
- Fill out the form with the elements collected from the AvaTax website:
* Account ID
* License Key
* Service URL: usually Production, or Sandox if you have that available.
* Company Code
- Click the Test Connection button
- Click the Save button
Other Avatax API advanced configurations:
- Tax Calculation tab:
- Disable Document Recording/Commiting: invoices will not be stored in Avalara
- Enable UPC Taxability: this will transmit Odoo's product ean13 number
instead of its Internal Reference. If there is no ean13
then the Internal Reference will be sent automatically.
- Hide Exemption & Tax Based on shipping address -- this will give user ability
to hide or show Tax Exemption and Tax Based on shipping address fields
at the invoice level.
- Address Validation tab:
- Automatic Address Validation: automatically attempts
to validate on creation and update of customer record,
last validation date will be visible and stored
- Require Validated Addresses: if validation for customer is required but not valid,
the validation will be forced
- Return validation results in upper case: validation results
will return in upper case form
- Advanced tab:
- Automatically generate missing customer code: generates a customer code
on creation and update of customer profile
- Log API requests: enables detailed AvaTax transaction logging within application
- Request Timeout: default is 300ms
- Countries: countries where AvaTax can be used.
Configure Company Taxes
~~~~~~~~~~~~~~~~~~~~~~~
Each company linked to AvaTax and their associated warehouses
should be configured to ensure the correct tax is calculated
and applied for all transactions.
Validate Company Address:
- On the AvTax API configuration form, click on the "Company Address" link
- On the company address form, click on the "validate" button
in the "AvaTax" tab
Validate Warehouse Address:
- Navigate to: Inventory >> Configuration >> Warehouse Management >> Warehouses
- For each warehouse, open the correspoding from view
- On the Warehouse form, click on the "Address" link
- On the warehouse address form, click on the "validate" button
in the "AvaTax" tab
Fiscal Positions is what tells the AvaTax connector if the AvaTax service
should be used for a particular Sales Order or Invoice.
Configure Fiscal Position:
- Navigate to: Accounting/Invoicing App >> Configuration >> Accounting
>> Fiscal Positions
- Ensure there is a Fiscal Position record for the Company,
with the "Use Avatax API" flag checked
When the appropriate Fiscal Position is being used, and a tax rate is retrieved form
AvaTax, then the corresponding Tax is automatically created in Odoo
using a template tax record, that should have the appropriate accounting configurations.
Configure Taxes:
- Navigate to: Accounting/Invoicing App >> Configuration >> Accounting >> Taxes
- Ensure there is a Tax record for the Company, with the "Is Avatax" flag checked
(visible in the "Advanced Options" tab). This Tax should have:
* Tax Type: Sales
* Tax Computation: Percentage of Price
* Amount: 0.0%
* Distribution for Invoices: ensure correct account configuration
* Distribution for Credit Notes: ensure correct account configuration
Configure Customers
~~~~~~~~~~~~~~~~~~~
Exemption codes are allowed for users where they may apply (ex. Government entities).
Navigate to: Accounting or Invoicing App >> Configuration >> AvaTax >> Exemption Code
The module is installed with 16 predefined exemption codes.
You can add, remove, and modify exemption codes.
Properly configuring each customer ensures the correct tax is calculated
and applied for all transactions.
Create New Customer
- Navigate to Contacts
- Click Create button
Configure and Validate Customer Address
- Enter Customer Address
- Under AvaTax >> Validation, click Validate button
- AvaTax Module will attempt to match the address you entered
with a valid address in its database.
Click the Accept button if the address is valid.
Tax Exemption Status
- If the customer is tax exempt, check the box under
AvaTax >> Tax Exemption >> Is Tax Exempt and
- Select the desired Tax Exempt Code from the dropdown menu.
Configure Products
~~~~~~~~~~~~~~~~~~
Create product tax codes to assign to products and/or product categories.
Navigate to: Accounting or Invoicing App >> Configuration >> AvaTax >> Product Tax Codes.
From here you can add, remove, and modify the product tax codes.
Products in Odoo are typically assigned to product categories.
AvaTax settings can also be assigned to the product category
when a product category is created.
- Create New Product Category
- Navigate to: Inventory >> Configuration >> Products >> Product Categories
- Click Create button
- Configure Product Category Tax Code
- Under AvaTax Properties >> Tax Code
- Select the desired Tax Code

View file

@ -0,0 +1,23 @@
* Odoo SA
* Fabrice Henrion
* Open Source Integrators (https://opensourceintegrators.com)
* Daniel Reis <dreis@opensourceintegrators.com>
* Bhavesh Odedra <bodedra@opensourceintegrators.com>
* Sandip Mangukiya <smangukiya@opensourceintegrators.com>
* Nikul Chaudhary <nchaudhary@opensourceintegrators.com>
* Serpent CS
* Murtuza Saleh
* Sodexis
* Atchuthan Ubendran
- Kencove (<https://kencove.com>)
- Don Kendall \<<kendall@donkendall.com>\>
- Mohamed Alkobrosli \<<malkobrosly@kencove.com>\>
- Wai-Lun Lin \<<wlin@kencove.com>\>

View file

@ -0,0 +1,18 @@
This module was originally developed by Fabrice Henrion at Odoo SA,
and maintained up to version 11.
For version 12, Fabrice invited partners to migrate this modules to
later version, and maintain it.
Open Source Integrators performed the migration to Odoo 12
, and later added support for the more up to date REST API
, alongside with the legacy SOAP API.
With the addition of the REST API, a deep refactor was introduced,
changing the tax calculation approach, from just setting the total
tax amount, to instead adding the tax rates to each document line
and then having Odoo do all the other computations.
For Odoo 13, the legacy SOAP support was supported, and
additional refactoring was done to contribute the module
to the Odoo Community Association.

View file

@ -0,0 +1,63 @@
.. |avataxbadge1| image:: static/description/SalesTax.png
:target: https://developer.avalara.com/certification/avatax/sales-tax-badge/
:alt: Sales Tax Certification
:width: 250
.. |avataxbadge2| image:: static/description/Refunds.png
:target: https://developer.avalara.com/certification/avatax/refunds-credit-memos-badge/
:alt: Refunds Certification
:width: 250
.. |avataxbadge3| image:: static/description/AddressValidation.png
:target: https://developer.avalara.com/certification/avatax/address-validation-badge/
:alt: Address Validation Certification
:width: 250
|avataxbadge1| |avataxbadge2| |avataxbadge3|
Odoo provides integration with AvaTax, a tax solution software by Avalara
which includes sales tax calculation for all US states and territories
and all Canadian provinces and territories (including GST, PST, and HST).
This module is capable of automatically detecting origin (Output Warehouse)
and destination (Client Address), then calculating and reporting taxes
to the user's Avalara account as well as a recording the correct sales taxes
for the validated addresses within Odoo ERP.
This module is compatible both with the Odoo Enterprise and Odoo Community
editions.
An Avatax account is needed. Account information to access
the Avatax dashboard can be obtained through the Avalara website here:
https://www.avalara.com/products/calculations.html
Once configured, the module operates in the background and performs
calculations and reporting seamlessly to the AvaTax server.
This guide includes instructions for the following elements:
- Activating your organization's AvaTax account and downloading the product
- Entering the AvaTax credentials into your Odoo database and configuring it
to use AvaTax services and features within Odoo
Note: Test the module before deploying in live environment.
All changes to the AvaTax settings must be performed by a user with
administrative access rights.
**IMPORTANT - resolving name conflict with Odoo EE**
Avatax support was added to Odoo EE 14 and 15.
Unfortunately the module names used are the same as the OCA ones,
and because of this name collision the OCA modules were forced to change name.
The main module was renamed from ``account_avatax`` (now used by Odoo EE) to
``account_avatax_oca``.
To apply this change in your odoo database and continue using the OCA Avalara certified
connector:
1. Ensure you have the latest version from the OCA, and you see ``account_avatax_oca``
in your Apps list.
2. Install the new ``account_avatax_oca`` module
3. Unistall the ``account_avatax`` module
4. Confirm that your configurations were kept safe, in particular:
Avatax API, "Avatax" default Fiscal Position, and "Avatax" default Tax record.

View file

@ -0,0 +1,26 @@
Before installing the Avatax app, the Avalara Python client
must be installed in your system.
It is available at https://pypi.org/project/Avalara.
Typically it can be installed in your system usin ``pip``::
pip3 install Avalara
The base app, ``account_Avatax``, adds Avatax support to Customer Invoices.
Inthe official app store: https://apps.odoo.com/apps/modules/15.0/account_avatax/
The ``account_avatax_sale`` extension adds support to Quotations / Sales Orders.
Inthe official app store: https://apps.odoo.com/apps/modules/15.0/account_avatax_sale/
In most cases you will want to download and install both modules.
To install the Avatax app:
- Download the AvaTax modules
- Extract the downloaded files
- Upload the extracted directories into your Odoo module/addons directory
- Log into Odoo as an Administrator and enable the Developer Mode, in 'Settings'
- Navigate to 'Apps', select the 'Update Apps List' menu, to have the new apps listed.
- In the Apps list, search for 'AvaTax'
- Click on the Install button. If available, the ``account_avatax_sale`` module will
also be installed automatically.

View file

@ -0,0 +1,10 @@
The development of this module was driven by US companies to compute Sales Tax.
However the Avatax service supports more use cases, that could be added:
- Add support to EU VAT
- Add support to US Use Tax on Purchases / vendor Bills
Other improvements that could be added:
- Detect and warn if customers State is not a nexus available for the current account

View file

@ -0,0 +1,103 @@
Customer Invoices
~~~~~~~~~~~~~~~~~
The AvaTax module is integrated into Sales Invoices
and is applied to each transaction.
The transaction log in the AvaTax dashboard shows the invoice details
and displays whether the transaction is in an uncommitted or committed status.
A validated invoice will have a Committed status
and a cancelled invoice will have a Voided status.
The module will check if there is a selected warehouse
and will automatically determine the address of the warehouse
and the origin location.
If no address is assigned to the warehouse, the company address is used.
Discounts are handled when they are enabled in Odoo's settings.
They are calculated as a net deduction on the line item cost
before the total is sent to AvaTax.
Create New Customer Invoice
^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Navigate to: Accounting or Invoicing >> Customers >> Invoices.
- Click Create button.
Validate Invoice
^^^^^^^^^^^^^^^^
- Ensure that Tax based on shipping address is checked.
- Line items should have AVATAX selected under Taxes for internal records.
- To complete the invoice, click the Validate button.
- The sale order will now appear in the AvaTax dashboard.
Register Payment
^^^^^^^^^^^^^^^^
- Click the Register Payment button to finalize the invoice.
Customer Refunds
^^^^^^^^^^^^^^^^
Odoo applies refunds as opposed to voids in its accounting module.
As with customer invoices, the AvaTax module is integrated
with customer refunds and is applied to each transaction.
Refunded invoice transactions will be indicated
with a negative total in the AvaTax interface.
Initiate Customer Refund
- Navigate to: Accounting or Invoicing >> Customers >> Invoices
- Select the invoice you wish to refund
- Click Add Credit Note button
Create Credit Note
- Under Credit Method, select Create a draft credit note.
- Enter a reason.
- Click Add Credit Note button.
Note: You will be taken to the Credit Notes list view
Validate Refund
- Select the Credit Note you wish to validate, review and then click Validate button.
Register Refund Payment
- Click Register Payment button to complete a refund
Sales Orders
~~~~~~~~~~~~
The AvaTax module is integrated into Sales Orders and allows computation of taxes.
Sales order transactions do not appear in the in the AvaTax interface.
The information placed in the sales order will automatically pass to the invoice
on the Avalara server and can be viewed in the AvaTax control panel.
Discounts are handled when they are enabled in Odoo's settings.
They will be reported as a net deduction on the line item cost.
Create New Sales Order
- Navigate to: Sales >> Orders >> Orders
- Click Create button
Compute Taxes with AvaTax
- The module will calculate tax when the sales order is confirmed,
or by navigating to Action >> Update taxes with Avatax.
At this step, the sales order will retrieve the tax amount from Avalara
but will not report the transaction to the AvaTax dashboard.
Only invoice, refund, and payment activity are reported to the dashboard.
- The module will check if there is a selected warehouse
and will automatically determine the address of the warehouse
and the origin location. If no address is assigned to the warehouse
the module will automatically use the address of the company as its origin.
Location code will automatically populate with the warehouse code
but can be modified if needed.

View file

@ -0,0 +1,30 @@
<odoo noupdate="1">
<!-- Ensure old deprecated buggy rule is removed -->
<delete
model="ir.rule"
search="[
('model_id', '=', ref('model_avalara_salestax')),
'|',
('domain_force', 'like', 'user.company_id.id'),
('domain_force', '=', '[\'|\',(\'company_id\',\'=\',False),(\'company_id\',\'in\',company_ids])]')
]"
/>
<!-- Rule for multi-company -->
<record id="account_salestax_avatax_comp_rule" model="ir.rule">
<field name="name">AvaTax multi-company</field>
<field name="model_id" ref="model_avalara_salestax" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
</record>
<!--
company_id field was removed from Product Tax Codes,
and the corresponding record rule also.
This ensures the old record rule is removed from the database.
-->
<delete
model="ir.rule"
search="[('model_id', '=', ref('model_product_tax_code'))]"
/>
</odoo>

View file

@ -0,0 +1,10 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_avalara_salestax_manager,avalara.salestax.manager,model_avalara_salestax,account.group_account_manager,1,1,1,1
access_avalara_salestax_employee,avalara.salestax.employee,model_avalara_salestax,base.group_user,1,0,0,0
access_product_tax_code_employee,product.tax.code.employee,model_product_tax_code,base.group_user,1,0,0,0
access_product_tax_code_manager,product.tax.code.manager,model_product_tax_code,account.group_account_manager,1,1,1,1
access_exemption_code_manager,exemption.code.manager,model_exemption_code,account.group_account_manager,1,1,1,1
access_exemption_code_employee,exemption.code.employee,model_exemption_code,base.group_user,1,0,0,0
access_avalara_salestax_ping,avalara_salestax_ping,model_avalara_salestax_ping,account.group_account_manager,1,1,1,1
access_avalara_salestax_address_validate,avalara_salestax_address_validate,model_avalara_salestax_address_validate,base.group_user,1,1,1,1
access_avalara_salestax_getcompany,avalara_salestax_getcompany,model_avalara_salestax_getcompany,account.group_account_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_avalara_salestax_manager avalara.salestax.manager model_avalara_salestax account.group_account_manager 1 1 1 1
3 access_avalara_salestax_employee avalara.salestax.employee model_avalara_salestax base.group_user 1 0 0 0
4 access_product_tax_code_employee product.tax.code.employee model_product_tax_code base.group_user 1 0 0 0
5 access_product_tax_code_manager product.tax.code.manager model_product_tax_code account.group_account_manager 1 1 1 1
6 access_exemption_code_manager exemption.code.manager model_exemption_code account.group_account_manager 1 1 1 1
7 access_exemption_code_employee exemption.code.employee model_exemption_code base.group_user 1 0 0 0
8 access_avalara_salestax_ping avalara_salestax_ping model_avalara_salestax_ping account.group_account_manager 1 1 1 1
9 access_avalara_salestax_address_validate avalara_salestax_address_validate model_avalara_salestax_address_validate base.group_user 1 1 1 1
10 access_avalara_salestax_getcompany avalara_salestax_getcompany model_avalara_salestax_getcompany account.group_account_manager 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,844 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Avalara Avatax Certified Connector</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="avalara-avatax-certified-connector">
<h1 class="title">Avalara Avatax Certified Connector</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:7f9e0f6757252679c2b9dd5853a0ef54dd73e94fb1ff3289c902ef982cda6ea5
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-fiscal-rule/tree/16.0/account_avatax_oca"><img alt="OCA/account-fiscal-rule" src="https://img.shields.io/badge/github-OCA%2Faccount--fiscal--rule-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-fiscal-rule-16-0/account-fiscal-rule-16-0-account_avatax_oca"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-fiscal-rule&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://developer.avalara.com/certification/avatax/sales-tax-badge/"><img alt="Sales Tax Certification" src="https://raw.githubusercontent.com/OCA/account-fiscal-rule/16.0/account_avatax_oca/static/description/SalesTax.png" style="width: 250px;" /></a> <a class="reference external image-reference" href="https://developer.avalara.com/certification/avatax/refunds-credit-memos-badge/"><img alt="Refunds Certification" src="https://raw.githubusercontent.com/OCA/account-fiscal-rule/16.0/account_avatax_oca/static/description/Refunds.png" style="width: 250px;" /></a> <a class="reference external image-reference" href="https://developer.avalara.com/certification/avatax/address-validation-badge/"><img alt="Address Validation Certification" src="https://raw.githubusercontent.com/OCA/account-fiscal-rule/16.0/account_avatax_oca/static/description/AddressValidation.png" style="width: 250px;" /></a></p>
<p>Odoo provides integration with AvaTax, a tax solution software by Avalara
which includes sales tax calculation for all US states and territories
and all Canadian provinces and territories (including GST, PST, and HST).</p>
<p>This module is capable of automatically detecting origin (Output Warehouse)
and destination (Client Address), then calculating and reporting taxes
to the users Avalara account as well as a recording the correct sales taxes
for the validated addresses within Odoo ERP.</p>
<p>This module is compatible both with the Odoo Enterprise and Odoo Community
editions.</p>
<p>An Avatax account is needed. Account information to access
the Avatax dashboard can be obtained through the Avalara website here:
<a class="reference external" href="https://www.avalara.com/products/calculations.html">https://www.avalara.com/products/calculations.html</a></p>
<p>Once configured, the module operates in the background and performs
calculations and reporting seamlessly to the AvaTax server.</p>
<p>This guide includes instructions for the following elements:</p>
<ul class="simple">
<li>Activating your organizations AvaTax account and downloading the product</li>
<li>Entering the AvaTax credentials into your Odoo database and configuring it
to use AvaTax services and features within Odoo</li>
</ul>
<p>Note: Test the module before deploying in live environment.
All changes to the AvaTax settings must be performed by a user with
administrative access rights.</p>
<p><strong>IMPORTANT - resolving name conflict with Odoo EE</strong></p>
<p>Avatax support was added to Odoo EE 14 and 15.
Unfortunately the module names used are the same as the OCA ones,
and because of this name collision the OCA modules were forced to change name.</p>
<p>The main module was renamed from <tt class="docutils literal">account_avatax</tt> (now used by Odoo EE) to
<tt class="docutils literal">account_avatax_oca</tt>.</p>
<p>To apply this change in your odoo database and continue using the OCA Avalara certified
connector:</p>
<blockquote>
<ol class="arabic simple">
<li><dl class="first docutils">
<dt>Ensure you have the latest version from the OCA, and you see <tt class="docutils literal">account_avatax_oca</tt></dt>
<dd>in your Apps list.</dd>
</dl>
</li>
<li>Install the new <tt class="docutils literal">account_avatax_oca</tt> module</li>
<li>Unistall the <tt class="docutils literal">account_avatax</tt> module</li>
<li><dl class="first docutils">
<dt>Confirm that your configurations were kept safe, in particular:</dt>
<dd>Avatax API, “Avatax” default Fiscal Position, and “Avatax” default Tax record.</dd>
</dl>
</li>
</ol>
</blockquote>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="toc-entry-1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a><ul>
<li><a class="reference internal" href="#configure-avatax-api-connection" id="toc-entry-3">Configure Avatax API Connection</a></li>
<li><a class="reference internal" href="#configure-company-taxes" id="toc-entry-4">Configure Company Taxes</a></li>
<li><a class="reference internal" href="#configure-customers" id="toc-entry-5">Configure Customers</a></li>
<li><a class="reference internal" href="#configure-products" id="toc-entry-6">Configure Products</a></li>
</ul>
</li>
<li><a class="reference internal" href="#usage" id="toc-entry-7">Usage</a><ul>
<li><a class="reference internal" href="#customer-invoices" id="toc-entry-8">Customer Invoices</a><ul>
<li><a class="reference internal" href="#create-new-customer-invoice" id="toc-entry-9">Create New Customer Invoice</a></li>
<li><a class="reference internal" href="#validate-invoice" id="toc-entry-10">Validate Invoice</a></li>
<li><a class="reference internal" href="#register-payment" id="toc-entry-11">Register Payment</a></li>
<li><a class="reference internal" href="#customer-refunds" id="toc-entry-12">Customer Refunds</a></li>
</ul>
</li>
<li><a class="reference internal" href="#sales-orders" id="toc-entry-13">Sales Orders</a></li>
</ul>
</li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-14">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-15">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-16">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-17">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-18">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-19">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-20">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h1><a class="toc-backref" href="#toc-entry-1">Installation</a></h1>
<p>Before installing the Avatax app, the Avalara Python client
must be installed in your system.
It is available at <a class="reference external" href="https://pypi.org/project/Avalara">https://pypi.org/project/Avalara</a>.</p>
<p>Typically it can be installed in your system usin <tt class="docutils literal">pip</tt>:</p>
<pre class="literal-block">
pip3 install Avalara
</pre>
<p>The base app, <tt class="docutils literal">account_Avatax</tt>, adds Avatax support to Customer Invoices.
Inthe official app store: <a class="reference external" href="https://apps.odoo.com/apps/modules/15.0/account_avatax/">https://apps.odoo.com/apps/modules/15.0/account_avatax/</a></p>
<p>The <tt class="docutils literal">account_avatax_sale</tt> extension adds support to Quotations / Sales Orders.
Inthe official app store: <a class="reference external" href="https://apps.odoo.com/apps/modules/15.0/account_avatax_sale/">https://apps.odoo.com/apps/modules/15.0/account_avatax_sale/</a></p>
<p>In most cases you will want to download and install both modules.</p>
<p>To install the Avatax app:</p>
<ul class="simple">
<li>Download the AvaTax modules</li>
<li>Extract the downloaded files</li>
<li>Upload the extracted directories into your Odoo module/addons directory</li>
<li>Log into Odoo as an Administrator and enable the Developer Mode, in Settings</li>
<li>Navigate to Apps, select the Update Apps List menu, to have the new apps listed.</li>
<li>In the Apps list, search for AvaTax</li>
<li>Click on the Install button. If available, the <tt class="docutils literal">account_avatax_sale</tt> module will
also be installed automatically.</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
<p>To configure an Odoo company to use Avatax, follow these steps.
Note tha tsome of them might be configured out of the box
for the Odoo default company.</p>
<ol class="arabic simple">
<li>Configure AvaTax API Connection</li>
<li>Configure Company Taxes</li>
<li>Configure Customers</li>
<li>Configure Products</li>
</ol>
<div class="section" id="configure-avatax-api-connection">
<h2><a class="toc-backref" href="#toc-entry-3">Configure Avatax API Connection</a></h2>
<p>Before you can configure the Odoo Avatax connector,
you will need some connection details ready:</p>
<ul class="simple">
<li>Login to <a class="reference external" href="https://home.avalara.com/">https://home.avalara.com/</a></li>
<li>Navigate to Settings &gt;&gt; All AvaTax Settings.
There you will see the company details.</li>
<li>Take note of the Account ID and Company Code</li>
<li>Navigate to Settings &gt;&gt; License and API Keys.
In the “Reset License Key” tab, click on the “Generate License Key” button,
and take note of it.</li>
</ul>
<p>To configure AvaTax connector in Odoo:</p>
<ul class="simple">
<li>Navigate to: Accounting/Invoicing App &gt;&gt; Configuration &gt;&gt; AvaTax &gt;&gt; AvaTax API</li>
<li>Click on the Create button</li>
<li>Fill out the form with the elements collected from the AvaTax website:<ul>
<li>Account ID</li>
<li>License Key</li>
<li>Service URL: usually Production, or Sandox if you have that available.</li>
<li>Company Code</li>
</ul>
</li>
<li>Click the Test Connection button</li>
<li>Click the Save button</li>
</ul>
<p>Other Avatax API advanced configurations:</p>
<ul class="simple">
<li>Tax Calculation tab:<ul>
<li>Disable Document Recording/Commiting: invoices will not be stored in Avalara</li>
<li>Enable UPC Taxability: this will transmit Odoos product ean13 number
instead of its Internal Reference. If there is no ean13
then the Internal Reference will be sent automatically.</li>
<li>Hide Exemption &amp; Tax Based on shipping address this will give user ability
to hide or show Tax Exemption and Tax Based on shipping address fields
at the invoice level.</li>
</ul>
</li>
<li>Address Validation tab:<ul>
<li>Automatic Address Validation: automatically attempts
to validate on creation and update of customer record,
last validation date will be visible and stored</li>
<li>Require Validated Addresses: if validation for customer is required but not valid,
the validation will be forced</li>
<li>Return validation results in upper case: validation results
will return in upper case form</li>
</ul>
</li>
<li>Advanced tab:<ul>
<li>Automatically generate missing customer code: generates a customer code
on creation and update of customer profile</li>
<li>Log API requests: enables detailed AvaTax transaction logging within application</li>
<li>Request Timeout: default is 300ms</li>
<li>Countries: countries where AvaTax can be used.</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configure-company-taxes">
<h2><a class="toc-backref" href="#toc-entry-4">Configure Company Taxes</a></h2>
<p>Each company linked to AvaTax and their associated warehouses
should be configured to ensure the correct tax is calculated
and applied for all transactions.</p>
<p>Validate Company Address:</p>
<ul class="simple">
<li>On the AvTax API configuration form, click on the “Company Address” link</li>
<li>On the company address form, click on the “validate” button
in the “AvaTax” tab</li>
</ul>
<p>Validate Warehouse Address:</p>
<ul class="simple">
<li>Navigate to: Inventory &gt;&gt; Configuration &gt;&gt; Warehouse Management &gt;&gt; Warehouses</li>
<li>For each warehouse, open the correspoding from view</li>
<li>On the Warehouse form, click on the “Address” link</li>
<li>On the warehouse address form, click on the “validate” button
in the “AvaTax” tab</li>
</ul>
<p>Fiscal Positions is what tells the AvaTax connector if the AvaTax service
should be used for a particular Sales Order or Invoice.</p>
<p>Configure Fiscal Position:</p>
<ul class="simple">
<li>Navigate to: Accounting/Invoicing App &gt;&gt; Configuration &gt;&gt; Accounting
&gt;&gt; Fiscal Positions</li>
<li>Ensure there is a Fiscal Position record for the Company,
with the “Use Avatax API” flag checked</li>
</ul>
<p>When the appropriate Fiscal Position is being used, and a tax rate is retrieved form
AvaTax, then the corresponding Tax is automatically created in Odoo
using a template tax record, that should have the appropriate accounting configurations.</p>
<p>Configure Taxes:</p>
<ul class="simple">
<li>Navigate to: Accounting/Invoicing App &gt;&gt; Configuration &gt;&gt; Accounting &gt;&gt; Taxes</li>
<li>Ensure there is a Tax record for the Company, with the “Is Avatax” flag checked
(visible in the “Advanced Options” tab). This Tax should have:<ul>
<li>Tax Type: Sales</li>
<li>Tax Computation: Percentage of Price</li>
<li>Amount: 0.0%</li>
<li>Distribution for Invoices: ensure correct account configuration</li>
<li>Distribution for Credit Notes: ensure correct account configuration</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configure-customers">
<h2><a class="toc-backref" href="#toc-entry-5">Configure Customers</a></h2>
<dl class="docutils">
<dt>Exemption codes are allowed for users where they may apply (ex. Government entities).</dt>
<dd>Navigate to: Accounting or Invoicing App &gt;&gt; Configuration &gt;&gt; AvaTax &gt;&gt; Exemption Code</dd>
<dt>The module is installed with 16 predefined exemption codes.</dt>
<dd>You can add, remove, and modify exemption codes.</dd>
</dl>
<p>Properly configuring each customer ensures the correct tax is calculated
and applied for all transactions.</p>
<p>Create New Customer</p>
<ul class="simple">
<li>Navigate to Contacts</li>
<li>Click Create button</li>
</ul>
<p>Configure and Validate Customer Address</p>
<ul class="simple">
<li>Enter Customer Address</li>
<li>Under AvaTax &gt;&gt; Validation, click Validate button</li>
<li>AvaTax Module will attempt to match the address you entered
with a valid address in its database.
Click the Accept button if the address is valid.</li>
</ul>
<p>Tax Exemption Status</p>
<ul class="simple">
<li>If the customer is tax exempt, check the box under
AvaTax &gt;&gt; Tax Exemption &gt;&gt; Is Tax Exempt and</li>
<li>Select the desired Tax Exempt Code from the dropdown menu.</li>
</ul>
</div>
<div class="section" id="configure-products">
<h2><a class="toc-backref" href="#toc-entry-6">Configure Products</a></h2>
<p>Create product tax codes to assign to products and/or product categories.
Navigate to: Accounting or Invoicing App &gt;&gt; Configuration &gt;&gt; AvaTax &gt;&gt; Product Tax Codes.</p>
<p>From here you can add, remove, and modify the product tax codes.</p>
<p>Products in Odoo are typically assigned to product categories.
AvaTax settings can also be assigned to the product category
when a product category is created.</p>
<ul class="simple">
<li>Create New Product Category<ul>
<li>Navigate to: Inventory &gt;&gt; Configuration &gt;&gt; Products &gt;&gt; Product Categories</li>
<li>Click Create button</li>
</ul>
</li>
<li>Configure Product Category Tax Code<ul>
<li>Under AvaTax Properties &gt;&gt; Tax Code</li>
<li>Select the desired Tax Code</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-7">Usage</a></h1>
<div class="section" id="customer-invoices">
<h2><a class="toc-backref" href="#toc-entry-8">Customer Invoices</a></h2>
<p>The AvaTax module is integrated into Sales Invoices
and is applied to each transaction.
The transaction log in the AvaTax dashboard shows the invoice details
and displays whether the transaction is in an uncommitted or committed status.</p>
<p>A validated invoice will have a Committed status
and a cancelled invoice will have a Voided status.</p>
<p>The module will check if there is a selected warehouse
and will automatically determine the address of the warehouse
and the origin location.
If no address is assigned to the warehouse, the company address is used.</p>
<p>Discounts are handled when they are enabled in Odoos settings.
They are calculated as a net deduction on the line item cost
before the total is sent to AvaTax.</p>
<div class="section" id="create-new-customer-invoice">
<h3><a class="toc-backref" href="#toc-entry-9">Create New Customer Invoice</a></h3>
<ul class="simple">
<li>Navigate to: Accounting or Invoicing &gt;&gt; Customers &gt;&gt; Invoices.</li>
<li>Click Create button.</li>
</ul>
</div>
<div class="section" id="validate-invoice">
<h3><a class="toc-backref" href="#toc-entry-10">Validate Invoice</a></h3>
<ul class="simple">
<li>Ensure that Tax based on shipping address is checked.</li>
<li>Line items should have AVATAX selected under Taxes for internal records.</li>
<li>To complete the invoice, click the Validate button.</li>
<li>The sale order will now appear in the AvaTax dashboard.</li>
</ul>
</div>
<div class="section" id="register-payment">
<h3><a class="toc-backref" href="#toc-entry-11">Register Payment</a></h3>
<ul class="simple">
<li>Click the Register Payment button to finalize the invoice.</li>
</ul>
</div>
<div class="section" id="customer-refunds">
<h3><a class="toc-backref" href="#toc-entry-12">Customer Refunds</a></h3>
<p>Odoo applies refunds as opposed to voids in its accounting module.
As with customer invoices, the AvaTax module is integrated
with customer refunds and is applied to each transaction.</p>
<dl class="docutils">
<dt>Refunded invoice transactions will be indicated</dt>
<dd>with a negative total in the AvaTax interface.</dd>
</dl>
<p>Initiate Customer Refund</p>
<ul class="simple">
<li>Navigate to: Accounting or Invoicing &gt;&gt; Customers &gt;&gt; Invoices</li>
<li>Select the invoice you wish to refund</li>
<li>Click Add Credit Note button</li>
</ul>
<p>Create Credit Note</p>
<ul class="simple">
<li>Under Credit Method, select Create a draft credit note.</li>
<li>Enter a reason.</li>
<li>Click Add Credit Note button.</li>
</ul>
<p>Note: You will be taken to the Credit Notes list view</p>
<p>Validate Refund</p>
<ul class="simple">
<li>Select the Credit Note you wish to validate, review and then click Validate button.</li>
</ul>
<p>Register Refund Payment</p>
<ul class="simple">
<li>Click Register Payment button to complete a refund</li>
</ul>
</div>
</div>
<div class="section" id="sales-orders">
<h2><a class="toc-backref" href="#toc-entry-13">Sales Orders</a></h2>
<p>The AvaTax module is integrated into Sales Orders and allows computation of taxes.
Sales order transactions do not appear in the in the AvaTax interface.</p>
<dl class="docutils">
<dt>The information placed in the sales order will automatically pass to the invoice</dt>
<dd>on the Avalara server and can be viewed in the AvaTax control panel.</dd>
</dl>
<p>Discounts are handled when they are enabled in Odoos settings.
They will be reported as a net deduction on the line item cost.</p>
<p>Create New Sales Order</p>
<ul class="simple">
<li>Navigate to: Sales &gt;&gt; Orders &gt;&gt; Orders</li>
<li>Click Create button</li>
</ul>
<p>Compute Taxes with AvaTax</p>
<ul class="simple">
<li>The module will calculate tax when the sales order is confirmed,
or by navigating to Action &gt;&gt; Update taxes with Avatax.
At this step, the sales order will retrieve the tax amount from Avalara
but will not report the transaction to the AvaTax dashboard.
Only invoice, refund, and payment activity are reported to the dashboard.</li>
<li>The module will check if there is a selected warehouse
and will automatically determine the address of the warehouse
and the origin location. If no address is assigned to the warehouse
the module will automatically use the address of the company as its origin.
Location code will automatically populate with the warehouse code
but can be modified if needed.</li>
</ul>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-14">Known issues / Roadmap</a></h1>
<p>The development of this module was driven by US companies to compute Sales Tax.</p>
<p>However the Avatax service supports more use cases, that could be added:</p>
<ul class="simple">
<li>Add support to EU VAT</li>
<li>Add support to US Use Tax on Purchases / vendor Bills</li>
</ul>
<p>Other improvements that could be added:</p>
<ul class="simple">
<li>Detect and warn if customers State is not a nexus available for the current account</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-15">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-fiscal-rule/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/account-fiscal-rule/issues/new?body=module:%20account_avatax_oca%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-16">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-17">Authors</a></h2>
<ul class="simple">
<li>Open Source Integrators</li>
<li>Fabrice Henrion</li>
<li>Sodexis</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-18">Contributors</a></h2>
<ul class="simple">
<li>Odoo SA<ul>
<li>Fabrice Henrion</li>
</ul>
</li>
<li>Open Source Integrators (<a class="reference external" href="https://opensourceintegrators.com">https://opensourceintegrators.com</a>)<ul>
<li>Daniel Reis &lt;<a class="reference external" href="mailto:dreis&#64;opensourceintegrators.com">dreis&#64;opensourceintegrators.com</a>&gt;</li>
<li>Bhavesh Odedra &lt;<a class="reference external" href="mailto:bodedra&#64;opensourceintegrators.com">bodedra&#64;opensourceintegrators.com</a>&gt;</li>
<li>Sandip Mangukiya &lt;<a class="reference external" href="mailto:smangukiya&#64;opensourceintegrators.com">smangukiya&#64;opensourceintegrators.com</a>&gt;</li>
<li>Nikul Chaudhary &lt;<a class="reference external" href="mailto:nchaudhary&#64;opensourceintegrators.com">nchaudhary&#64;opensourceintegrators.com</a>&gt;</li>
</ul>
</li>
<li>Serpent CS<ul>
<li>Murtuza Saleh</li>
</ul>
</li>
<li>Sodexis<ul>
<li>Atchuthan Ubendran</li>
</ul>
</li>
</ul>
<ul class="simple">
<li>Kencove (&lt;<a class="reference external" href="https://kencove.com">https://kencove.com</a>&gt;)
- Don Kendall &lt;&lt;<a class="reference external" href="mailto:kendall&#64;donkendall.com">kendall&#64;donkendall.com</a>&gt;&gt;
- Mohamed Alkobrosli &lt;&lt;<a class="reference external" href="mailto:malkobrosly&#64;kencove.com">malkobrosly&#64;kencove.com</a>&gt;&gt;
- Wai-Lun Lin &lt;&lt;<a class="reference external" href="mailto:wlin&#64;kencove.com">wlin&#64;kencove.com</a>&gt;&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#toc-entry-19">Other credits</a></h2>
<p>This module was originally developed by Fabrice Henrion at Odoo SA,
and maintained up to version 11.</p>
<p>For version 12, Fabrice invited partners to migrate this modules to
later version, and maintain it.</p>
<p>Open Source Integrators performed the migration to Odoo 12
, and later added support for the more up to date REST API
, alongside with the legacy SOAP API.</p>
<p>With the addition of the REST API, a deep refactor was introduced,
changing the tax calculation approach, from just setting the total
tax amount, to instead adding the tax rates to each document line
and then having Odoo do all the other computations.</p>
<p>For Odoo 13, the legacy SOAP support was supported, and
additional refactoring was done to contribute the module
to the Odoo Community Association.</p>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-20">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/dreispt"><img alt="dreispt" src="https://github.com/dreispt.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-fiscal-rule/tree/16.0/account_avatax_oca">OCA/account-fiscal-rule</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,4 @@
from . import test_account_tax
from . import test_avatax
from . import test_parter
from . import test_rest_api

View file

@ -0,0 +1,367 @@
def _mock_line(product_data):
subtotal = product_data.get("price_unit", 0.0) * product_data.get(
"quantity"
) - product_data.get("discount_amount")
tax_amount = subtotal * product_data.get("rate_expected", 0.0)
res = {
"boundaryOverrideId": 0,
"businessIdentificationNo": "",
"costInsuranceFreight": 0.0,
"customerUsageType": "",
"description": product_data.get("product")
and product_data.get("product").display_name
or "No Name",
"destinationAddressId": 85600959974166,
"details": [
{
"addressId": 85600959974167,
"chargedTo": "Buyer",
"country": "US",
"countyFIPS": "",
"exemptAmount": product_data.get("exemption_amount", 0.0),
"exemptReasonId": 3,
"exemptRuleId": 7455340,
"exemptUnits": product_data.get("exemption_amount", 0.0),
"id": 85600959974176,
"inState": True,
"isFee": False,
"isNonPassThru": False,
"jurisCode": "29",
"jurisName": "MISSOURI",
"jurisType": "STA",
"jurisdictionId": 2000001420,
"jurisdictionType": "State",
"liabilityType": "Seller",
"nonTaxableAmount": 0.0,
"nonTaxableRuleId": 0,
"nonTaxableType": "RateRule",
"nonTaxableUnits": 0.0,
"rate": product_data.get("rate_expected", 0.0),
"rateRuleId": 1065438,
"rateSourceId": 3,
"rateType": "General",
"rateTypeCode": "G",
"region": "MO",
"reportingExemptUnits": product_data.get("exemption_amount", 0.0),
"reportingNonTaxableUnits": 0.0,
"reportingTax": 0.0,
"reportingTaxCalculated": 0.0,
"reportingTaxableUnits": 0.0,
"serCode": "",
"signatureCode": "AXYM",
"sourcing": "Origin",
"stateAssignedNo": "",
"stateFIPS": "29",
"tax": tax_amount,
"taxAuthorityTypeId": 45,
"taxCalculated": tax_amount,
"taxName": "MO STATE TAX",
"taxOverride": 0.0,
"taxRegionId": 2078034,
"taxSubTypeId": "S",
"taxType": "Sales",
"taxTypeGroupId": "SalesAndUse",
"taxableAmount": subtotal,
"taxableUnits": subtotal,
"transactionId": 85600959974165,
"transactionLineId": 85600959974171,
"unitOfBasis": "PerCurrencyUnit",
},
{
"addressId": 85600959974167,
"chargedTo": "Buyer",
"country": "US",
"countyFIPS": "",
"exemptAmount": product_data.get("exemption_amount", 0.0),
"exemptReasonId": 3,
"exemptRuleId": 7455340,
"exemptUnits": product_data.get("exemption_amount", 0.0),
"id": 85600959974177,
"inState": True,
"isFee": False,
"isNonPassThru": False,
"jurisCode": "037",
"jurisName": "CASS",
"jurisType": "CTY",
"jurisdictionId": 1527,
"jurisdictionType": "County",
"liabilityType": "Seller",
"nonTaxableAmount": 0.0,
"nonTaxableRuleId": 0,
"nonTaxableType": "RateRule",
"nonTaxableUnits": 0.0,
"rate": product_data.get("rate_expected", 0.0),
"rateRuleId": 1654198,
"rateSourceId": 3,
"rateType": "General",
"rateTypeCode": "G",
"region": "MO",
"reportingExemptUnits": product_data.get("exemption_amount", 0.0),
"reportingNonTaxableUnits": 0.0,
"reportingTax": 0.0,
"reportingTaxCalculated": 0.0,
"reportingTaxableUnits": 0.0,
"serCode": "",
"signatureCode": "AYFX",
"sourcing": "Origin",
"stateAssignedNo": "56756-037-000",
"stateFIPS": "29",
"tax": tax_amount,
"taxAuthorityTypeId": 45,
"taxCalculated": tax_amount,
"taxName": "MO COUNTY TAX",
"taxOverride": 0.0,
"taxRegionId": 2078034,
"taxSubTypeId": "S",
"taxType": "Sales",
"taxTypeGroupId": "SalesAndUse",
"taxableAmount": subtotal,
"taxableUnits": subtotal,
"transactionId": 85600959974165,
"transactionLineId": 85600959974171,
"unitOfBasis": "PerCurrencyUnit",
},
{
"addressId": 85600959974167,
"chargedTo": "Buyer",
"country": "US",
"countyFIPS": "",
"exemptAmount": product_data.get("exemption_amount", 0.0),
"exemptReasonId": 3,
"exemptRuleId": 7455340,
"exemptUnits": product_data.get("exemption_amount", 0.0),
"id": 85600959974178,
"inState": True,
"isFee": False,
"isNonPassThru": False,
"jurisCode": "56756",
"jurisName": "PECULIAR",
"jurisType": "CIT",
"jurisdictionId": 85774,
"jurisdictionType": "City",
"liabilityType": "Seller",
"nonTaxableAmount": 0.0,
"nonTaxableRuleId": 0,
"nonTaxableType": "RateRule",
"nonTaxableUnits": 0.0,
"rate": product_data.get("rate_expected"),
"rateRuleId": 1391040,
"rateSourceId": 3,
"rateType": "General",
"rateTypeCode": "G",
"region": "MO",
"reportingExemptUnits": product_data.get("exemption_amount", 0.0),
"reportingNonTaxableUnits": 0.0,
"reportingTax": 0.0,
"reportingTaxCalculated": 0.0,
"reportingTaxableUnits": 0.0,
"serCode": "",
"signatureCode": "AYGM",
"sourcing": "Origin",
"stateAssignedNo": "56756-037-000",
"stateFIPS": "29",
"tax": tax_amount,
"taxAuthorityTypeId": 45,
"taxCalculated": tax_amount,
"taxName": "MO CITY TAX",
"taxOverride": 0.0,
"taxRegionId": 2078034,
"taxSubTypeId": "S",
"taxType": "Sales",
"taxTypeGroupId": "SalesAndUse",
"taxableAmount": subtotal,
"taxableUnits": subtotal,
"transactionId": 85600959974165,
"transactionLineId": 85600959974171,
"unitOfBasis": "PerCurrencyUnit",
},
],
"discountAmount": product_data.get("discount_amount"),
"discountTypeId": 0,
"entityUseCode": "",
"exemptAmount": product_data.get("exemption_amount", 0.0),
"exemptCertId": 90867213,
"exemptNo": "",
"hsCode": "",
"id": 85600959974171,
"isItemTaxable": False,
"isSSTP": False,
"itemCode": "MPC",
"lineAmount": subtotal,
"lineLocationTypes": [
{
"documentAddressId": 85600959974167,
"documentLineId": 85600959974171,
"documentLineLocationTypeId": 85600959974174,
"locationTypeCode": "ShipFrom",
},
{
"documentAddressId": 85600959974166,
"documentLineId": 85600959974171,
"documentLineLocationTypeId": 85600959974175,
"locationTypeCode": "ShipTo",
},
],
"lineNumber": f"{product_data.get('line_id')}",
"nonPassthroughDetails": [],
"originAddressId": 85600959974167,
"quantity": product_data.get("quantity"),
"ref1": "",
"ref2": "",
"reportingDate": "2024-09-17",
"revAccount": "",
"sourcing": "Origin",
"tax": tax_amount,
"taxCalculated": tax_amount,
"taxCode": "PA020122",
"taxCodeId": 71096,
"taxDate": "2024-09-17",
"taxEngine": "",
"taxIncluded": False,
"taxOverrideAmount": 0.0,
"taxOverrideReason": "",
"taxOverrideType": "None",
"taxableAmount": subtotal,
"transactionId": 85600959974165,
"vatCode": "",
"vatNumberTypeId": 0,
}
return subtotal, tax_amount, res
def mock_response(product_data_list):
"""
Mock to simulate avalara answer, it's only a standard compute
Keyword arguments:
product_data_list -- List of dict with:
- product (browse record)
- quantity
- price_unit
- discount_amount
- exemption_amount
- rate_expected
- line_id (invoice line id)
Return:
Dict with mocked response
"""
lines_data = [_mock_line(product_data) for product_data in product_data_list]
subtotal = sum(line[0] for line in lines_data)
tax_amount = sum(line[1] for line in lines_data)
lines_data = [line[2] for line in lines_data]
res = {
"addresses": [
{
"boundaryLevel": "Zip5",
"city": "Hale",
"country": "US",
"id": 85600941773548,
"line1": "0000 E State Rd",
"line2": "",
"line3": "",
"postalCode": "00000-0000",
"region": "MI",
"taxRegionId": 1056912,
"transactionId": 85600941773547,
},
{
"boundaryLevel": "Address",
"city": "Blairsville",
"country": "US",
"id": 85600941773549,
"latitude": "0.000000",
"line1": "000 Kendall Rd",
"line2": "",
"line3": "",
"longitude": "0.000000",
"postalCode": "00000-0000",
"region": "PA",
"taxRegionId": 4012044,
"transactionId": 85600941773547,
},
],
"adjustmentDescription": "",
"adjustmentReason": "NotAdjusted",
"apStatus": None,
"apStatusCode": None,
"batchCode": "",
"businessIdentificationNo": "",
"code": "INV/2024/09/1482",
"companyId": 951445,
"country": "US",
"currencyCode": "USD",
"customerCode": "CC-000000:0",
"customerUsageType": "",
"customerVendorCode": "CC-000000:0",
"date": "2024-09-17",
"description": "INV/2024/09/1482",
"destinationAddressId": 85600941773548,
"email": "",
"entityUseCode": "",
"exchangeRate": 1.0,
"exchangeRateCurrencyCode": "USD",
"exchangeRateEffectiveDate": "2024-09-17",
"exemptNo": "",
"id": 85600941773547,
"lines": lines_data,
"locationCode": "",
"locationTypes": [
{
"documentAddressId": 85600941773549,
"documentId": 85600941773547,
"documentLocationTypeId": 85600941773551,
"locationTypeCode": "ShipFrom",
},
{
"documentAddressId": 85600941773548,
"documentId": 85600941773547,
"documentLocationTypeId": 85600941773552,
"locationTypeCode": "ShipTo",
},
],
"locked": False,
"modifiedDate": "2024-09-17T20:56:39.7321524Z",
"modifiedUserId": 1094294,
"originAddressId": 85600941773549,
"purchaseOrderNo": "",
"reconciled": False,
"referenceCode": "",
"region": "MI",
"reportingLocationCode": "",
"salespersonCode": "Jimmy Dunmire",
"softwareVersion": "24.8.0.0",
"status": "Committed",
"summary": [
{
"country": "US",
"exemption": 0.0,
"jurisCode": "26",
"jurisName": "MICHIGAN",
"jurisType": "State",
"nonTaxable": 0.0,
"rate": 0.06,
"rateType": "General",
"region": "MI",
"stateAssignedNo": "",
"tax": tax_amount,
"taxAuthorityType": 45,
"taxCalculated": tax_amount,
"taxName": "MI STATE TAX",
"taxSubType": "S",
"taxType": "Sales",
"taxable": subtotal,
}
],
"taxDate": "2024-09-17",
"taxOverrideAmount": 0.0,
"taxOverrideReason": "",
"taxOverrideType": "None",
"totalAmount": subtotal,
"totalDiscount": 0.0,
"totalExempt": 0.0,
"totalTax": tax_amount,
"totalTaxCalculated": tax_amount,
"totalTaxable": subtotal,
"type": "SalesInvoice",
"version": 1,
}
return res

View file

@ -0,0 +1,35 @@
# Copyright 2021 Open Source Integrators
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from odoo import exceptions
from odoo.tests import common
class TestAvatax(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.Tax = cls.env["account.tax"]
cls.company1 = cls.env.ref("base.main_company")
cls.company2 = cls.env["res.company"].create({"name": "Company Avatax 2"})
cls.journal = cls.env["account.journal"].create(
{
"name": "Test Sales Journal",
"type": "sale",
"code": "TSJ",
"company_id": cls.company2.id,
}
)
def test_get_avatax_tax_rate(self):
tax75 = self.Tax.get_avalara_tax(7.5, "out_invoice")
self.assertEqual(tax75.amount, 7.5)
def test_get_avatax_template(self):
tax = self.Tax.get_avalara_tax(0, "out_invoice")
self.assertEqual(tax.name, "AVATAX")
def test_get_avatax_template_missing(self):
with self.assertRaises(exceptions.UserError):
self.Tax.with_company(self.company2).get_avalara_tax(0, "out_invoice")

View file

@ -0,0 +1,179 @@
# Copyright 2021 Open Source Integrators
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from unittest.mock import patch
from odoo.tests import Form, common
from .mock_avatax import mock_response
class TestAvatax(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.fiscal_position = cls.env["account.fiscal.position"].create(
{
"name": "Avatax Demo",
"is_avatax": True,
}
)
cls.customer = cls.env["res.partner"].create(
{
"name": "Customer",
"property_account_position_id": cls.fiscal_position.id,
"property_tax_exempt": True,
"property_exemption_number": "12321",
"property_exemption_code_id": cls.env.ref(
"account_avatax_oca.resale_type"
),
}
)
cls.invoice = cls.env["account.move"].create(
{
"move_type": "out_invoice",
"partner_id": cls.customer.id,
"invoice_line_ids": [
(0, 0, {"name": "Invoice Line", "price_unit": 10, "quantity": 10})
],
}
)
def test_100_onchange_customer_exempt(self):
self.invoice.partner_id = self.customer
self.assertEqual(
self.invoice.exemption_code, self.customer.property_exemption_number
)
@patch(
"odoo.addons.account_avatax_oca.models.res_company.Company.get_avatax_config_company"
)
@patch(
"odoo.addons.account_avatax_oca.models.avalara_salestax.AvalaraSalestax.create_transaction" # noqa: B950
)
def test_avatax_compute_tax(
self, mock_create_transaction, mock_get_avatax_config_company
):
avatax_config = self.env["avalara.salestax"].create(
{
"account_number": "123456",
"license_key": "123456",
"company_code": "DEFAULT",
"disable_tax_calculation": False,
"invoice_calculate_tax": False,
}
)
mock_get_avatax_config_company.return_value = avatax_config
# Force empty taxes to check only avatax taxes
self.invoice.invoice_line_ids.write(
{
"tax_ids": [(6, 0, [])],
}
)
invoice_line_data = [
{
"product_id": self.env["product.product"].create({"name": "Product 1"}),
"quantity": 5,
"price_unit": 102.5,
"rate": 0.06448,
},
{
"product_id": self.env["product.product"].create({"name": "Product 2"}),
"quantity": 4,
"price_unit": 25.5,
"rate": 0.03448,
},
]
self.invoice.invoice_line_ids.unlink()
invoice_form = Form(self.invoice)
for line_data in invoice_line_data:
with invoice_form.invoice_line_ids.new() as line:
line.product_id = line_data.get("product_id")
line.quantity = line_data.get("quantity")
line.price_unit = line_data.get("price_unit")
line.tax_ids.clear()
self.assertFalse(invoice_form.calculate_tax_on_save)
self.invoice = invoice_form.save()
self.assertFalse(self.invoice.calculate_tax_on_save)
mock_create_transaction.return_value = mock_response(
[
{
"product": line.product_id,
"quantity": line.quantity,
"price_unit": line.price_unit,
"discount_amount": line.price_subtotal
- ((line.price_unit * line.quantity) * (1 - line.discount * 100.0)),
"rate_expected": line_data.get("rate"),
"line_id": line.id,
}
for line, line_data in zip(
self.invoice.invoice_line_ids, invoice_line_data
)
]
)
self.invoice.invalidate_model(["invoice_line_ids"])
for line in self.invoice.invoice_line_ids:
self.assertFalse(bool(line.tax_ids))
self.invoice.action_post()
for line in self.invoice.invoice_line_ids:
self.assertTrue(bool(line.tax_ids))
self.assertEqual(
self.invoice.amount_tax + self.invoice.amount_untaxed,
self.invoice.amount_residual,
)
mock_get_avatax_config_company.assert_called()
mock_create_transaction.assert_called()
self.invoice.button_draft()
avatax_config.write(
{
"invoice_calculate_tax": True,
}
)
self.invoice.invoice_line_ids.unlink()
invoice_form = Form(self.invoice)
for line_data in invoice_line_data:
with invoice_form.invoice_line_ids.new() as line:
line.product_id = line_data.get("product_id")
line.quantity = line_data.get("quantity")
line.price_unit = line_data.get("price_unit")
line.tax_ids.clear()
self.assertTrue(invoice_form.calculate_tax_on_save)
self.invoice = invoice_form.save()
mock_create_transaction.return_value = mock_response(
[
{
"product": line.product_id,
"quantity": line.quantity,
"price_unit": line.price_unit,
"discount_amount": line.price_subtotal
- ((line.price_unit * line.quantity) * (1 - line.discount * 100.0)),
"rate_expected": line_data.get("rate"),
"line_id": line.id,
}
for line, line_data in zip(
self.invoice.invoice_line_ids, invoice_line_data
)
]
)
self.assertFalse(self.invoice.calculate_tax_on_save)
self.invoice.action_post()
for line in self.invoice.invoice_line_ids:
self.assertTrue(bool(line.tax_ids))
self.assertEqual(
self.invoice.amount_tax + self.invoice.amount_untaxed,
self.invoice.amount_residual,
)

View file

@ -0,0 +1,12 @@
# Copyright 2021 Open Source Integrators
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from odoo.tests import common
class TestAvatax(common.TransactionCase):
def test_customer_existing_code(self):
"Create Customer with an already existing code (data import)"
val_customer = {"name": "New Customer", "customer_code": "ABC"}
new_customer = self.env["res.partner"].create(val_customer)
self.assertEqual(new_customer.customer_code, "ABC")

View file

@ -0,0 +1,52 @@
# Copyright 2022 Open Source Integrators
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from odoo.tests import common
from ..models.avatax_rest_api import AvaTaxRESTService
class TestAvatax(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.API = AvaTaxRESTService()
def test_enrich_result_lines_with_tax_rate(self):
avatax_result = {
"lines": [
{
"details": [
{
"rate": 0.056,
"tax": 0.0,
"taxCalculated": 0.0,
"taxName": "AZ STATE TAX",
"taxableAmount": 0.0,
},
{
"rate": 0.007,
"tax": 0.0,
"taxCalculated": 0.0,
"taxName": "AZ COUNTY TAX",
"taxableAmount": 0.0,
},
{
"rate": 0.018,
"tax": 0.16,
"taxCalculated": 0.16,
"taxName": "AZ CITY TAX",
"taxableAmount": 9.0,
},
],
"isItemTaxable": True,
"lineAmount": 9.0,
"tax": 0.16,
"taxCalculated": 0.16,
"taxableAmount": 9.0,
}
]
}
result = self.API._enrich_result_lines_with_tax_rate(avatax_result)
rate = result["lines"][0]["rate"]
self.assertEqual(rate, 1.8)

View file

@ -0,0 +1,12 @@
<odoo>
<record id="view_fiscal_position_form_avatax" model="ir.ui.view">
<field name="name">account.fiscal.position form add Avatax</field>
<field name="model">account.fiscal.position</field>
<field name="inherit_id" ref="account.view_account_position_form" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="is_avatax" />
</field>
</field>
</record>
</odoo>

View file

@ -0,0 +1,9 @@
<odoo>
<record model="ir.actions.server" id="action_account_invoice_compute_taxes">
<field name="name">Update taxes with AvaTax</field>
<field name="model_id" ref="account.model_account_move" />
<field name="binding_model_id" ref="account.model_account_move" />
<field name="state">code</field>
<field name="code">records.avatax_compute_taxes()</field>
</record>
</odoo>

Some files were not shown because too many files have changed in this diff Show more