Initial commit: OCA Financial packages (186 packages)
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
from .hooks import pre_init_hook
|
||||
from .hooks import post_load_hook
|
||||
|
|
@ -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"],
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
|
|
@ -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>\>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -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&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 user’s 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 organization’s 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 >> 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 >> 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 >> Configuration >> AvaTax >> 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 Odoo’s 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 & 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 >> Configuration >> Warehouse Management >> 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 >> Configuration >> Accounting
|
||||
>> 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 >> Configuration >> Accounting >> 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 >> Configuration >> AvaTax >> 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 >> 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 >> Tax Exemption >> 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 >> Configuration >> AvaTax >> 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 >> Configuration >> Products >> Product Categories</li>
|
||||
<li>Click Create button</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Configure Product Category Tax Code<ul>
|
||||
<li>Under AvaTax Properties >> 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 Odoo’s 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 >> Customers >> 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 >> Customers >> 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 Odoo’s 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 >> Orders >> 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 >> 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 <<a class="reference external" href="mailto:dreis@opensourceintegrators.com">dreis@opensourceintegrators.com</a>></li>
|
||||
<li>Bhavesh Odedra <<a class="reference external" href="mailto:bodedra@opensourceintegrators.com">bodedra@opensourceintegrators.com</a>></li>
|
||||
<li>Sandip Mangukiya <<a class="reference external" href="mailto:smangukiya@opensourceintegrators.com">smangukiya@opensourceintegrators.com</a>></li>
|
||||
<li>Nikul Chaudhary <<a class="reference external" href="mailto:nchaudhary@opensourceintegrators.com">nchaudhary@opensourceintegrators.com</a>></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 (<<a class="reference external" href="https://kencove.com">https://kencove.com</a>>)
|
||||
- Don Kendall <<<a class="reference external" href="mailto:kendall@donkendall.com">kendall@donkendall.com</a>>>
|
||||
- Mohamed Alkobrosli <<<a class="reference external" href="mailto:malkobrosly@kencove.com">malkobrosly@kencove.com</a>>>
|
||||
- Wai-Lun Lin <<<a class="reference external" href="mailto:wlin@kencove.com">wlin@kencove.com</a>>></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>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
from . import test_account_tax
|
||||
from . import test_avatax
|
||||
from . import test_parter
|
||||
from . import test_rest_api
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||