diff --git a/account_avatax_exemption/README.rst b/account_avatax_exemption/README.rst new file mode 100644 index 000000000..683b069fa --- /dev/null +++ b/account_avatax_exemption/README.rst @@ -0,0 +1,85 @@ +================= +Avatax Exemptions +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c8b257f5868840c5425cd77f06787db45dcd3e6b4f8778dbdf499188212aa3bf + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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_exemption + :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_exemption + :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| + +This module is a component of the Avatax Exemption Integration with odoo app. + + * Export Exemption customer in Avatax + * Export Exemptions for customer based on nexus region + * Export Custom rules based on avatax nexus regions + * Export Product Taxcodes to Avatax + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Sodexis + +Contributors +~~~~~~~~~~~~ + +* Sodexis + + * Atchuthan Ubendran + * Stephan Keller + * SodexisTeam + +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. + +This module is part of the `OCA/account-fiscal-rule `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_avatax_exemption/__init__.py b/account_avatax_exemption/__init__.py new file mode 100644 index 000000000..5607426d8 --- /dev/null +++ b/account_avatax_exemption/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controller diff --git a/account_avatax_exemption/__manifest__.py b/account_avatax_exemption/__manifest__.py new file mode 100644 index 000000000..d88694ec8 --- /dev/null +++ b/account_avatax_exemption/__manifest__.py @@ -0,0 +1,39 @@ +{ + "name": "Avatax Exemptions", + "version": "16.0.1.0.0", + "category": "Sales", + "summary": """ + This application allows you to add exemptions to Avatax + """, + "website": "https://github.com/OCA/account-fiscal-rule", + "author": "Sodexis, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": [ + "website", + "product", + "queue_job", + "account_avatax_sale_oca", + "account_avatax_exemption_base", + ], + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "data/cron.xml", + "data/queue.xml", + "data/ir_sequence_data.xml", + "views/avalara_salestax_view.xml", + "views/avalara_exemption_view.xml", + "views/product_view.xml", + "views/exemption_template_views.xml", + "views/res_country_state_view.xml", + "views/website_layout.xml", + "views/website_exemption_tree.xml", + "views/website_exemption_form.xml", + "views/partner_view.xml", + "views/account_move_view.xml", + "views/sale_view.xml", + ], + "external_dependencies": {"python": ["Avalara"]}, + "installable": True, + "application": True, +} diff --git a/account_avatax_exemption/controller/__init__.py b/account_avatax_exemption/controller/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/account_avatax_exemption/controller/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/account_avatax_exemption/controller/main.py b/account_avatax_exemption/controller/main.py new file mode 100644 index 000000000..f74750734 --- /dev/null +++ b/account_avatax_exemption/controller/main.py @@ -0,0 +1,166 @@ +import logging + +from odoo import _, http +from odoo.http import request, route +from odoo.tools import exception_to_unicode + +from odoo.addons.portal.controllers.portal import CustomerPortal + +_logger = logging.getLogger(__name__) + + +class Exemption(http.Controller): + @http.route("/exemption/", website=True, auth="public") + def get_exemption(self, **kw): + exemption_id = kw.get("exemption_id") + try: + message = ( + request.env["res.partner.exemption"] + .sudo() + .search_exemption_line(exemption_id) + ) + except Exception as e: + message = False, exception_to_unicode(e) + return request.render( + "account_avatax_exemption.exemption_page", {"message": message} + ) + + +class WebsiteExemption(CustomerPortal): + def _exemptions_domain(self, search=""): + """Get user's exemptions domain.""" + + avalara_salestax = ( + request.env["avalara.salestax"] + .sudo() + .search([("exemption_export", "=", True)], limit=1) + ) + domain = [("partner_id", "child_of", request.env.user.partner_id.id)] + if avalara_salestax.use_commercial_entity: + domain = [ + ( + "partner_id", + "child_of", + request.env.user.partner_id.commercial_partner_id.id, + ) + ] + + return domain + + def _prepare_portal_layout_values(self, exemption=None): + values = super(WebsiteExemption, self)._prepare_portal_layout_values() + partner_counts = request.env["res.partner.exemption"].search_count( + self._exemptions_domain() + ) + values["exemption_count"] = partner_counts + return values + + def _prepare_exemptions_values( + self, page=1, date_begin=None, date_end=None, search="", sortby=None + ): + """Prepare the rendering context for the exemptions list.""" + values = self._prepare_portal_layout_values() + Exemption = request.env["res.partner.exemption"] + base_url = "/my/exemptions" + + searchbar_sortings = { + "date": {"label": _("Newest"), "order": "create_date desc"}, + "expiry_date": {"label": _("Expiry Date"), "order": "expiry_date desc"}, + } + if not sortby: + sortby = "date" + order = searchbar_sortings[sortby]["order"] + + # Get the required domains + domain = self._exemptions_domain(search) + + if date_begin and date_end: + domain += [ + ("create_date", ">=", date_begin), + ("create_date", "<", date_end), + ] + + # Make pager + pager = request.website.pager( + url=base_url, + url_args={"date_begin": date_begin, "date_end": date_end, "sortby": sortby}, + total=Exemption.search_count(domain), + page=page, + step=self._items_per_page, + ) + + # Current records to display + exemptions = Exemption.search( + domain, + order=order, + limit=self._items_per_page, + offset=pager["offset"], + ) + request.session["my_exemptions_history"] = exemptions.ids[:100] + + values.update( + { + "date": date_begin, + "date_end": date_end, + "exemptions": exemptions, + "page_name": "exemption", + "pager": pager, + "default_url": base_url, + "search": search, + "searchbar_sortings": searchbar_sortings, + "sortby": sortby, + } + ) + + return values + + def _exemptions_fields(self): + """Fields to display in the form.""" + return [ + "partner_id", + "exemption_type", + "exemption_code_id", + "state", + "exemption_number", + "exemption_number_type", + "effective_date", + "expiry_date", + ] + + @route( + ["/my/exemptions", "/my/exemptions/page/"], + type="http", + auth="user", + website=True, + ) + def portal_my_exemptions( + self, page=1, date_begin=None, date_end=None, sortby=None, search="", **kw + ): + """List all of your exemptions.""" + values = self._prepare_exemptions_values( + page, date_begin, date_end, search, sortby + ) + return request.render("account_avatax_exemption.portal_my_exemptions", values) + + def _exemption_get_page_view_values(self, exemption, access_token, **kwargs): + values = { + "exemption": exemption, + "fields": self._exemptions_fields(), + "page_name": "exemption", + "user": request.env.user, + } + + return self._get_page_view_values( + exemption, access_token, values, "my_exemption_history", False, **kwargs + ) + + @route( + ["/my/exemptions/"], + type="http", + auth="user", + website=True, + ) + def portal_my_exemptions_read(self, exemption, access_token=None, **kw): + """Read a exemption form.""" + values = self._exemption_get_page_view_values(exemption, access_token, **kw) + return request.render("account_avatax_exemption.exemptions_followup", values) diff --git a/account_avatax_exemption/data/cron.xml b/account_avatax_exemption/data/cron.xml new file mode 100644 index 000000000..9b6c4df4f --- /dev/null +++ b/account_avatax_exemption/data/cron.xml @@ -0,0 +1,32 @@ + + + Avatax TaxItem: Export New Items + + code + model.export_new_tax_items() + 1 + days + -1 + + + + Avatax Rules: Export New Rules + + code + model.export_new_exemption_rules() + 1 + days + -1 + + + + Avatax Exemption Download + + code + model.download_exemptions() + 1 + days + -1 + + + diff --git a/account_avatax_exemption/data/ir_sequence_data.xml b/account_avatax_exemption/data/ir_sequence_data.xml new file mode 100644 index 000000000..857045330 --- /dev/null +++ b/account_avatax_exemption/data/ir_sequence_data.xml @@ -0,0 +1,9 @@ + + + Exemption Custom Rule Sequence + exemption.code.rule.sequence + RULE + 5 + + + diff --git a/account_avatax_exemption/data/queue.xml b/account_avatax_exemption/data/queue.xml new file mode 100644 index 000000000..50dc356cf --- /dev/null +++ b/account_avatax_exemption/data/queue.xml @@ -0,0 +1,102 @@ + + + avatax + + + + + + _export_base_rule_based_on_type + + + + + + _cancel_custom_rule + + + + + + + _export_tax_item + + + + + + _delete_tax_item + + + + + + _update_tax_item + + + + + + + _export_avatax_customer + + + + + + _export_avatax_exemption_line + + + + + + link_certificates_to_customer + + + + + + _update_avatax_exemption_line_status + + + + + + _search_create_exemption_line + + + + diff --git a/account_avatax_exemption/i18n/account_avatax_exemption.pot b/account_avatax_exemption/i18n/account_avatax_exemption.pot new file mode 100644 index 000000000..25fc7a25d --- /dev/null +++ b/account_avatax_exemption/i18n/account_avatax_exemption.pot @@ -0,0 +1,536 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_avatax_exemption +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.form_breadcrumb +msgid "/ My Exemptions" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Are you sure you want to Cancel the Custom Rule?" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Are you sure you want to Enable the Custom Rule?" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_form_view +msgid "Are you sure you want to Enable the Exemptions?" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Are you sure you want to cancel the Custom Rules?" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_form_view +msgid "Are you sure you want to cancel the Exemptions?" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_product_tax_code +msgid "AvaTax Code" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_avalara_salestax +msgid "AvaTax Configuration" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.actions.act_window,name:account_avatax_exemption.exemption_rule_act_window +#: model:ir.model,name:account_avatax_exemption.model_exemption_code_rule +#: model:ir.ui.menu,name:account_avatax_exemption.menu_exemption_rule +msgid "Avatax Custom Rules" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_res_partner_exemption +msgid "Avatax Exemption" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.actions.server,name:account_avatax_exemption.ir_cron_download_exemption_ir_actions_server +#: model:ir.cron,cron_name:account_avatax_exemption.ir_cron_download_exemption +#: model:ir.cron,name:account_avatax_exemption.ir_cron_download_exemption +msgid "Avatax Exemption Download" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Avatax Exemption Rule export is disabled in Avatax configuration" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_res_partner_exemption_type +msgid "Avatax Exemption Type" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/avalara_salestax.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Avatax Exemption export is disabled in Avatax configuration" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.view_partner_details_form_inherit1 +msgid "Avatax Exemption related settings are managed on" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__avatax_rate +msgid "Avatax Rate" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__avatax_id +msgid "Avatax Rule ID" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_tax_code__rule_ids +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_country_state__rule_ids +msgid "Avatax Rules" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.actions.server,name:account_avatax_exemption.ir_cron_export_exemption_rule_ir_actions_server +#: model:ir.cron,cron_name:account_avatax_exemption.ir_cron_export_exemption_rule +#: model:ir.cron,name:account_avatax_exemption.ir_cron_export_exemption_rule +msgid "Avatax Rules: Export New Rules" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__avatax_tax_code +msgid "Avatax Tax Code" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_product__avatax_item_id +msgid "Avatax TaxItem" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.actions.server,name:account_avatax_exemption.ir_cron_export_new_tax_item_ir_actions_server +#: model:ir.cron,cron_name:account_avatax_exemption.ir_cron_export_new_tax_item +#: model:ir.cron,name:account_avatax_exemption.ir_cron_export_new_tax_item +msgid "Avatax TaxItem: Export New Items" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Avatax rate range is from 0 to 100" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_form_view +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Cancel" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Cancel Failed Job" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields.selection,name:account_avatax_exemption.selection__exemption_code_rule__state__cancel +msgid "Cancelled" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax__avatax_company_id +msgid "Company ID" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_res_country_state +msgid "Country state" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_code_form_view +msgid "Create Custom Rules" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__create_uid +msgid "Created by" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__create_date +msgid "Created on" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_tree_view +msgid "Custom Rules" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_category__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_product__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_tax_code__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_template__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_queue_job__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_country_state__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_business_type__display_name +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_type__display_name +msgid "Display Name" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields.selection,name:account_avatax_exemption.selection__exemption_code_rule__state__done +msgid "Done" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields.selection,name:account_avatax_exemption.selection__exemption_code_rule__state__draft +msgid "Draft" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_table +msgid "Effective Date" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Enable Custom Rule" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_form_view +msgid "Enable Exemptions" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__exemption_code_id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption__exemption_code_id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_business_type__exemption_code_id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_type__exemption_code_id +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_code_form_view +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_code_tree_view +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_table +msgid "Entity Use Code" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_res_partner_exemption_business_type +msgid "Exemption Activity Type" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_exemption_code +msgid "Exemption Code" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax__exemption_export +msgid "Exemption Export" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax__exemption_rule_export +msgid "Exemption Rule Export" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_code_form_view +msgid "Exemption Rules" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Exemption status needs to be in Cancel status to enable" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Exemption status needs to be in Done status to cancel" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.portal_layout +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.portal_my_home_exemptions +msgid "Exemptions" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/controller/main.py:0 +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_table +#, python-format +msgid "Expiry Date" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_form_view +msgid "Export Exemptions" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.view_avalara_salestax_form +msgid "Export New TaxItems" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Export Rules" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_followup +msgid "History" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_category__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_product__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_tax_code__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_template__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_queue_job__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_country_state__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_business_type__id +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_type__id +msgid "ID" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.view_avalara_salestax_form +msgid "Import Exemption Activity Type" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.view_avalara_salestax_form +msgid "Import Exemptions" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.view_avalara_salestax_form +msgid "Import Nexus Fed State Info" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.view_avalara_salestax_form +msgid "Import TaxItems" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields.selection,name:account_avatax_exemption.selection__exemption_code_rule__state__progress +msgid "In Progress" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__is_all_juris +msgid "Is All Juris" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_category____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_product____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_tax_code____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_product_template____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_queue_job____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_country_state____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_business_type____last_update +#: model:ir.model.fields,field_description:account_avatax_exemption.field_res_partner_exemption_type____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__write_date +msgid "Last Updated on" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__name +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_followup +msgid "Name" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "New" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/controller/main.py:0 +#, python-format +msgid "Newest" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "No Customer code added in Partner" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "No Exemption Lines added" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.portal_my_exemptions +msgid "No exemptions found." +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_product_product +msgid "Product" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_product_category +msgid "Product Category" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_product_template +msgid "Product Template" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model,name:account_avatax_exemption.model_queue_job +msgid "Queue Job" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__state_id +msgid "Region" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemption_rule_form_view +msgid "Reset to Draft" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code__rule_ids +msgid "Rule" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Rule is not in Cancelled state to Re-Export Custom Rule" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Rule is not in Done state to Cancel Custom Rule" +msgstr "" + +#. module: account_avatax_exemption +#: code:addons/account_avatax_exemption/models/exemption.py:0 +#, python-format +msgid "Rule is not in Draft state to Export Custom Rule" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__state +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_followup +msgid "State" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_followup +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_table +msgid "Status" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax__tax_item_export +msgid "Tax Item Export" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code_rule__taxable +msgid "Taxable" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_exemption_code__flag +msgid "Taxed by default" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,help:account_avatax_exemption.field_avalara_salestax__avatax_company_id +msgid "The company ID as defined in the Admin Console of AvaTax" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,field_description:account_avatax_exemption.field_avalara_salestax__use_commercial_entity +msgid "Use Commercial Entity" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.exemptions_followup +msgid "View Exemption:" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.portal_my_exemptions +msgid "Your Exemptions" +msgstr "" + +#. module: account_avatax_exemption +#: model:ir.model.fields,help:account_avatax_exemption.field_exemption_code__flag +msgid "helps to add custom rules for the nexus Avatax states" +msgstr "" + +#. module: account_avatax_exemption +#: model_terms:ir.ui.view,arch_db:account_avatax_exemption.view_partner_details_form_inherit1 +msgid "the parent company" +msgstr "" diff --git a/account_avatax_exemption/models/__init__.py b/account_avatax_exemption/models/__init__.py new file mode 100644 index 000000000..556b56fc9 --- /dev/null +++ b/account_avatax_exemption/models/__init__.py @@ -0,0 +1,5 @@ +from . import exemption +from . import avalara_salestax +from . import product +from . import queue_job +from . import res_country_state diff --git a/account_avatax_exemption/models/avalara_salestax.py b/account_avatax_exemption/models/avalara_salestax.py new file mode 100644 index 000000000..baba9fdfe --- /dev/null +++ b/account_avatax_exemption/models/avalara_salestax.py @@ -0,0 +1,938 @@ +import requests + +from odoo import _, fields, models +from odoo.exceptions import UserError + +from odoo.addons.account_avatax_oca.models.avatax_rest_api import AvaTaxRESTService +from odoo.addons.queue_job.exception import FailedJobError + + +class AvalaraSalestax(models.Model): + _inherit = "avalara.salestax" + + avatax_company_id = fields.Char( + "Company ID", + help="The company ID as defined in the Admin Console of AvaTax", + ) + tax_item_export = fields.Boolean() + exemption_export = fields.Boolean() + exemption_rule_export = fields.Boolean() + use_commercial_entity = fields.Boolean(default=True) + + 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, + ): + if self.use_commercial_entity and partner.commercial_partner_id: + partner = partner.commercial_partner_id + return super().create_transaction( + doc_date, + doc_code, + doc_type, + partner, + ship_from_address, + shipping_address, + lines, + user=user, + exemption_number=exemption_number, + exemption_code_name=exemption_code_name, + commit=commit, + invoice_date=invoice_date, + reference_code=reference_code, + location_code=location_code, + is_override=is_override, + currency_id=currency_id, + ignore_error=ignore_error, + log_to_record=log_to_record, + ) + + def set_tax_item_info_to_product(self, record, product): + vals = {} + product_tax_codes = self.env["product.tax.code"].search([]) + if product: + tax_code = product_tax_codes.filtered(lambda x: x.name == record["taxCode"]) + if not tax_code: + tax_code = product_tax_codes.create( + { + "type": "product", + "name": record["taxCode"], + } + ) + vals["tax_code_id"] = tax_code.id + vals["avatax_item_id"] = record["id"] + product.with_context(skip_job_creation=True).write(vals) + + def import_exemption_activity_type(self): + self.ensure_one() + business_type_obj = self.env["res.partner.exemption.business.type"] + avatax_restpoint = AvaTaxRESTService(config=self) + r = avatax_restpoint.client.list_certificate_exempt_reasons() + result = r.json() + if "error" in result: + error = result["error"] + error_message = "Code: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + raise FailedJobError(error_message) + for record in result["value"]: + business_type = business_type_obj.search( + ["|", ("name", "=", record["name"]), ("avatax_id", "=", record["id"])], + limit=1, + ) + if not business_type: + business_type_obj.create( + { + "name": record["name"], + "avatax_id": record["id"], + } + ) + + def import_exemption_country_state_code(self): + self.ensure_one() + state_obj = self.env["res.country.state"] + avatax_restpoint = AvaTaxRESTService(config=self) + r = avatax_restpoint.client.list_jurisdictions() + result = r.json() + if "error" in result: + error = result["error"] + error_message = "Code: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + raise FailedJobError(error_message) + for record in result["value"]: + if record["type"] != "State": + continue + state = state_obj.search( + [ + ("code", "=", record["region"]), + ("country_id.code", "=", record["country"]), + ("avatax_code", "=", False), + ], + limit=1, + ) + if state: + state.write( + { + "avatax_code": record["code"], + "avatax_name": record["name"], + } + ) + + r2 = avatax_restpoint.client.list_nexus_by_company(self.avatax_company_id) + result2 = r2.json() + if "error" in result2: + error = result2["error"] + error_message = "Code: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + raise FailedJobError(error_message) + for record in result2["value"]: + if record["jurisdictionTypeId"] != "State": + continue + state = state_obj.search( + [ + ("code", "=", record["region"]), + ("country_id.code", "=", record["country"]), + ], + limit=1, + ) + if state: + state.write( + { + "avatax_nexus": True, + } + ) + + exemption_rule_obj = self.env["exemption.code.rule"] + states = state_obj.search([("avatax_nexus", "=", True)]) + entity_use_codes = self.env["exemption.code"].search([]) + for state in states: + for use_code in entity_use_codes.filtered(lambda x: x.flag): + exemption_rule = exemption_rule_obj.search( + [ + ("exemption_code_id", "=", use_code.id), + ("state_id", "=", state.id), + ("taxable", "=", True), + ], + limit=1, + ) + if exemption_rule: + continue + else: + exemption_rule_obj.create( + { + "exemption_code_id": use_code.id, + "state_id": state.id, + "taxable": True, + "state": "draft", + } + ) + + def import_tax_items(self): + self.ensure_one() + products = self.env["product.product"].search( + [("default_code", "!=", False), ("avatax_item_id", "=", False)] + ) + + avatax_restpoint = AvaTaxRESTService(config=self) + client = avatax_restpoint.client + + result_vals = [] + main_url = url = "{}/api/v2/companies/{}/items".format( + client.base_url, self.avatax_company_id + ) + count = 0 + while True: + r = requests.get( + url, + auth=client.auth, + headers=client.client_header, + timeout=client.timeout_limit if client.timeout_limit else 1200, + ) + result = r.json() + count += 1000 + if "error" in result: + error = result["error"] + error_message = "Code: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + raise FailedJobError(error_message) + result_vals += result["value"] + if result["@recordsetCount"] <= count: + break + else: + url = main_url + "?%24skip=" + str(count) + + for product in products: + for record in result_vals: + if product.default_code == record["itemCode"]: + self.set_tax_item_info_to_product(record, product) + break + + def export_new_tax_items(self): + if not self.ids: + self = self.search([("tax_item_export", "=", True)], limit=1) + if not self.tax_item_export: + return + products = self.env["product.product"].search( + [ + ("default_code", "!=", False), + ("avatax_item_id", "=", False), + "|", + ("tax_code_id", "!=", False), + ("categ_id.tax_code_id", "!=", False), + ], + ) + + for product in products: + self.with_delay( + description="Export Tax Item %s" % (product.display_name) + )._export_tax_item(product) + + def export_new_exemption_rules(self, rules=None): + if not self.ids: + self = self.search([("exemption_rule_export", "=", True)], limit=1) + if not self.exemption_rule_export: + return + if not rules: + rules = self.env["exemption.code.rule"].search( + [("avatax_id", "=", False), ("state", "=", "progress")], + ) + + queue_job_sudo = self.env["queue.job"].sudo() + for rule in rules: + job = queue_job_sudo.search( + [ + ("method_name", "=", "_export_base_rule_based_on_type"), + ("state", "!=", "done"), + ("args", "ilike", "%[" + str(rule.id) + "]%"), + ], + limit=1, + ) + if not job: + self.with_delay( + priority=5, + max_retries=2, + description="Export Rule %s" % (rule.name), + )._export_base_rule_based_on_type(rule) + + def map_rule_vals_to_fields(self, json_data): + avatax_rule_data = { + "avatax_id": json_data["id"], + "avatax_rate": json_data.get("value", ""), + "exemption_code_id": json_data.get("customerUsageType", ""), + "is_all_juris": json_data.get("isAllJuris", False), + "state": "done", + } + # Search for tax code and exemption code in Odoo models + tax_code_id = ( + self.env["product.tax.code"].search( + [("name", "=", json_data.get("taxCode", "") or "")], limit=1 + ) + or None + ) + if tax_code_id: + avatax_rule_data["avatax_tax_code"] = tax_code_id.id + exemption_code_id = ( + self.env["exemption.code"].search( + [("code", "=", json_data.get("customerUsageType", "") or "")], limit=1 + ) + or None + ) + if exemption_code_id: + avatax_rule_data["exemption_code_id"] = exemption_code_id.id + # Search for country and state in Odoo models + state_id = ( + self.env["res.country.state"].search( + [ + ("country_id.code", "=", json_data.get("country", "") or ""), + ("code", "=", json_data.get("region", "") or ""), + ], + limit=1, + ) + or None + ) + if state_id: + avatax_rule_data["state_id"] = state_id.id + avatax_rule_data["taxable"] = state_id.avatax_nexus + return avatax_rule_data + + def import_tax_rules(self): + avatax_custom_rule_model = self.env["exemption.code.rule"] + avatax_restpoint = AvaTaxRESTService(config=self) + rules = avatax_custom_rule_model.search( + [("avatax_id", "!=", False), ("state", "=", "done")], + ) + include_option = None + if rules: + filter_rules = "id not in (" + filter_rules += ", ".join(map(str, rules.mapped("avatax_id"))) + filter_rules += ")" + include_option = "$filter=" + filter_rules + r = avatax_restpoint.client.list_tax_rules( + self.avatax_company_id, include_option + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = "Code: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + raise UserError(error_message) + mapped_data = [] + for rule_vals in result["value"]: + if rule_vals.get("customerUsageType", "") and rule_vals.get("region", ""): + mapped_data.append(self.map_rule_vals_to_fields(rule_vals)) + avatax_custom_rule_model.create(mapped_data) + + def download_exemptions(self): + if not self.ids: + self = self.search([("exemption_export", "=", True)], limit=1) + if not self.exemption_export: + raise UserError( + _("Avatax Exemption export is disabled in Avatax configuration") + ) + + avatax_restpoint = AvaTaxRESTService(config=self) + count = 0 + result_vals = [] + include_option = None + while True: + r = avatax_restpoint.client.query_certificates( + self.avatax_company_id, include_option + ) + result = r.json() + count += 100 + if "error" in result: + error = result["error"] + error_message = "Code: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + raise UserError(error_message) + result_vals += result["value"] + if result["@recordsetCount"] <= count: + break + else: + include_option = "$skip=" + str(count) + + exemptions = ( + self.env["res.partner.exemption.line"] + .sudo() + .search([("avatax_id", "!=", False)]) + ) + for exemption in result_vals: + avatax_id = exemption["id"] + if avatax_id not in exemptions.mapped("avatax_id"): + self.with_delay( + description="Download Exemption: %s" % (avatax_id) + )._search_create_exemption_line(avatax_id) + + def _export_base_rule_based_on_type(self, rule): + error_message = False + if not rule.state_id.avatax_code: + raise FailedJobError("Avatax code for State not setup") + if not rule.exemption_code_id.flag: + raise FailedJobError("Taxed by Default is disabled in Exemption Code") + avatax_restpoint = AvaTaxRESTService(config=self) + + avatax_value = 0 + rule_type = "ExemptEntityRule" + if rule.taxable: + avatax_value = 1 + elif rule.avatax_rate == 100.0: + avatax_value = 1 + elif rule.avatax_rate: + rule_type = "RateOverrideRule" + avatax_value = rule.avatax_rate / 100 + tax_rule_info = { + "companyId": self.avatax_company_id, + "taxCode": rule.avatax_tax_code.name or None, + "taxTypeId": "BothSalesAndUseTax", + "taxRuleTypeId": rule_type, + "jurisCode": rule.state_id.avatax_code, + "jurisName": rule.state_id.avatax_name, + "jurisTypeId": "STA", + "jurisdictionTypeId": "State", + "isAllJuris": rule.is_all_juris, + "value": avatax_value, + "cap": 0, + "threshold": 0, + "effectiveDate": fields.Datetime.to_string(fields.Date.today()), + "description": "%s - %s - %s" + % (rule.state_id.avatax_name, rule.exemption_code_id.code, rule.name), + "country": rule.state_id.country_id.code, + "region": rule.state_id.code, + "stateFIPS": rule.state_id.avatax_code, + "taxTypeGroup": "SalesAndUse", + "customerUsageType": rule.exemption_code_id.code, + "taxSubType": "ALL", + } + r = avatax_restpoint.client.create_tax_rules( + self.avatax_company_id, [tax_rule_info] + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Rule: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + rule.name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + rule.write( + { + "avatax_id": result[0]["id"], + "state": "done", + } + ) + + return result + + def _cancel_custom_rule(self, rule): + error_message = False + if not rule.avatax_id: + raise FailedJobError("Avatax Custom Rule ID not available") + avatax_restpoint = AvaTaxRESTService(config=self) + + r = avatax_restpoint.client.delete_tax_rule( + self.avatax_company_id, rule.avatax_id + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Rule: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + rule.name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + rule.write( + { + "avatax_id": False, + "state": "cancel", + } + ) + + return result + + def _export_tax_item(self, product): + error_message = False + if not self.tax_item_export: + raise FailedJobError("Tax Item Export is disabled in Avatax configuration") + if product.avatax_item_id: + return "Product exported with Avatax ID: %s" % (product.avatax_item_id) + avatax_restpoint = AvaTaxRESTService(config=self) + + item_info = { + "itemCode": product.default_code, + "taxCode": product.tax_code_id.name or product.categ_id.tax_code_id.name, + "description": product.name, + } + r = avatax_restpoint.client.create_items(self.avatax_company_id, item_info) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Product: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + product.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + product.with_context(skip_job_creation=True).write( + { + "avatax_item_id": result[0]["id"], + } + ) + + return result + + def _delete_tax_item(self, product): + error_message = False + if not self.tax_item_export: + raise FailedJobError("Tax Item Export is disabled in Avatax configuration") + if not product.avatax_item_id: + return "Avatax ID not available in Product: %s" % (product.display_name) + avatax_restpoint = AvaTaxRESTService(config=self) + + r = avatax_restpoint.client.delete_item( + self.avatax_company_id, product.avatax_item_id + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Product: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + product.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + product.with_context(skip_job_creation=True).write( + { + "avatax_item_id": False, + } + ) + + return result + + def _update_tax_item(self, tax_item_id, product): + if not self.tax_item_export: + raise FailedJobError("Tax Item Export is disabled in Avatax configuration") + error_message = False + avatax_restpoint = AvaTaxRESTService(config=self) + + item_info = { + "itemCode": product.default_code, + "taxCode": product.tax_code_id.name or product.categ_id.tax_code_id.name, + "description": product.name, + } + r = avatax_restpoint.client.update_item( + self.avatax_company_id, tax_item_id, item_info + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Product: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + product.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + + return result + + def _export_avatax_customer(self, partner): + error_message = False + if not self.exemption_export: + raise FailedJobError( + "Avatax Exemption export is disabled in Avatax configuration" + ) + + avatax_restpoint = AvaTaxRESTService(config=self) + if partner.avatax_id: + return "Avatax Customer ID: %s" % (partner.avatax_id) + customer_info = [ + { + "customerCode": partner.customer_code, + "alternateId": partner.id, + "name": partner.name, + "line1": partner.street, + "city": partner.city, + "postalCode": partner.zip, + "phoneNumber": partner.phone, + "emailAddress": partner.email, + "contactName": partner.name, + "country": partner.country_id.code, + "region": partner.state_id.code, + } + ] + r = avatax_restpoint.client.create_customers( + self.avatax_company_id, customer_info + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Partner: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + partner.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + partner.with_context(skip_job_creation=True).write( + { + "avatax_id": result[0]["id"], + } + ) + + return result + + def _export_avatax_exemption_line(self, exemption_line): + error_message = False + if not self.exemption_export: + raise FailedJobError( + "Avatax Exemption export is disabled in Avatax configuration" + ) + + avatax_restpoint = AvaTaxRESTService(config=self) + if exemption_line.avatax_id: + return "Avatax Customer ID: %s" % (exemption_line.avatax_id) + exemption_line_info = [ + { + "signedDate": fields.Datetime.to_string( + exemption_line.exemption_id.effective_date + ), + "expirationDate": fields.Datetime.to_string( + exemption_line.exemption_id.expiry_date + ), + "filename": exemption_line.name, + "valid": True, + "exemptionNumber": exemption_line.exemption_number + if exemption_line.add_exemption_number + else exemption_line.exemption_id.exemption_number, + "exemptPercentage": 100.0, + "validatedExemptionReason": { + "name": exemption_line.exemption_id.business_type.name, + }, + "exemptionReason": { + "name": exemption_line.exemption_id.business_type.name, + }, + "exposureZone": { + "name": exemption_line.state_id.name, + }, + "pages": [None], + } + ] + r = avatax_restpoint.client.create_certificates( + self.avatax_company_id, exemption_line_info + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Exemption: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + exemption_line.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + exemption_line.write( + { + "avatax_id": result[0]["id"], + } + ) + + self.with_delay( + priority=6, + max_retries=2, + description="Link Customer %s with Exemption %s" + % (exemption_line.partner_id.display_name, exemption_line.name), + ).link_certificates_to_customer(exemption_line) + + return result + + def link_certificates_to_customer(self, exemption_line): + error_message = False + if not self.exemption_export: + raise FailedJobError( + "Avatax Exemption export is disabled in Avatax configuration" + ) + if not exemption_line.exemption_id.partner_id.avatax_id: + raise FailedJobError("Avatax Customer export has failed") + + avatax_restpoint = AvaTaxRESTService(config=self) + r = avatax_restpoint.client.link_certificates_to_customer( + self.avatax_company_id, + exemption_line.exemption_id.partner_id.customer_code, + {"certificates": [exemption_line.avatax_id]}, + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = ( + "Exemption: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + exemption_line.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + exemption_line.write( + { + "linked_to_customer": True, + } + ) + if all( + exemption_line.exemption_id.exemption_line_ids.mapped("linked_to_customer") + ): + exemption_line.exemption_id.write( + { + "state": "done", + } + ) + return result + + def _update_avatax_exemption_line_status(self, exemption_line, exemption_status): + error_message = False + if not self.exemption_export: + raise FailedJobError( + "Avatax Exemption export is disabled in Avatax configuration" + ) + + avatax_restpoint = AvaTaxRESTService(config=self) + if not exemption_line.avatax_id: + raise FailedJobError("Avatax Exemption ID is not found") + + r1 = avatax_restpoint.client.get_certificate( + self.avatax_company_id, exemption_line.avatax_id + ) + result1 = r1.json() + if "error" in result1: + error = result1["error"] + error_message = ( + "Exemption: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + exemption_line.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + exemption_line_info = dict(result1) + exemption_line_info["valid"] = exemption_status + r2 = avatax_restpoint.client.update_certificate( + self.avatax_company_id, exemption_line.avatax_id, exemption_line_info + ) + result2 = r2.json() + if "error" in result2: + error = result2["error"] + error_message = ( + "Exemption: %s\nCode: %s\nMessage: %s\nTarget: %s\nDetails;%s" + % ( + exemption_line.display_name, + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + ) + raise FailedJobError(error_message) + exemption_line.write( + { + "avatax_status": exemption_status, + } + ) + exemption_line.exemption_id.write( + { + "state": "done" if exemption_status else "cancel", + } + ) + + return result2 + + def _search_create_exemption_line(self, avatax_id): + exemption_sudo = self.env["res.partner.exemption"].sudo() + partner_sudo = self.env["res.partner"].sudo() + error_message = False + if not self.exemption_export: + raise FailedJobError( + "Avatax Exemption export is disabled in Avatax configuration" + ) + + avatax_restpoint = AvaTaxRESTService(config=self) + r = avatax_restpoint.client.get_certificate( + self.avatax_company_id, avatax_id, "$include=customers" + ) + result = r.json() + if "error" in result: + error = result["error"] + error_message = "Code: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + error.get("code", False), + error.get("message", False), + error.get("target", False), + error.get("details", False), + ) + raise FailedJobError(error_message) + if result.get("customers", []): + customer_info = result["customers"][0] + partner = partner_sudo.search( + [("avatax_id", "=", customer_info["id"])], limit=1 + ) + if partner: + partner.customer_code = customer_info["customerCode"] + if not partner: + partner = partner_sudo.search( + [("customer_code", "=", customer_info["customerCode"])], limit=1 + ) + if not partner: + partner = partner_sudo.search( + [("customer_code", "=", "%s:0" % (customer_info["customerCode"]))], + limit=1, + ) + if not partner: + state = self.env["res.country.state"] + if "region" in customer_info: + state = ( + self.env["res.country.state"] + .sudo() + .search( + [ + ("code", "=", customer_info["region"]), + ("country_id.code", "=", customer_info["country"]), + ], + limit=1, + ) + ) + partner_vals = { + "name": customer_info["name"], + "street": customer_info["line1"], + "city": customer_info["city"], + "zip": customer_info["postalCode"], + "state_id": state.id, + "country_id": state.country_id.id, + "email": customer_info.get("emailAddress", False), + "phone": customer_info.get("phoneNumber", False), + "avatax_id": customer_info["id"], + "customer_code": customer_info["customerCode"], + } + partner = partner_sudo.create(partner_vals) + + # Check if exemption is already available in system + exemption_line = ( + self.env["res.partner.exemption.line"] + .sudo() + .search([("avatax_id", "=", result["id"])], limit=1) + ) + if exemption_line: + return "Exemption Already Downloaded\nSearch Response: %s" % (result) + exposure_zone_info = result["exposureZone"] + exposure_state = ( + self.env["res.country.state"] + .sudo() + .search( + [ + ("code", "=", exposure_zone_info["region"]), + ("country_id.code", "=", exposure_zone_info["country"]), + ], + limit=1, + ) + ) + business_type = ( + self.env["res.partner.exemption.business.type"] + .sudo() + .search([("avatax_id", "=", result["exemptionReason"]["id"])], limit=1) + ) + exemption_line_vals = { + "state_id": exposure_state.id, + "avatax_id": result["id"], + "avatax_status": result["valid"], + "linked_to_customer": True, + } + exemption_sudo.create( + { + "partner_id": partner.id, + "business_type": business_type.id, + "exemption_code_id": business_type.exemption_code_id.id, + "state_ids": [(6, 0, [exposure_state.id])], + "exemption_number": result["exemptionNumber"], + "effective_date": result["signedDate"], + "expiry_date": result["expirationDate"], + "state": "done" if result["valid"] else "cancel", + "exemption_line_ids": [(0, 0, exemption_line_vals)], + } + ) + return result + else: + raise FailedJobError("Exemption ID is not linked with a customer in Avatax") diff --git a/account_avatax_exemption/models/exemption.py b/account_avatax_exemption/models/exemption.py new file mode 100644 index 000000000..f4241c464 --- /dev/null +++ b/account_avatax_exemption/models/exemption.py @@ -0,0 +1,308 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class ExemptionRule(models.Model): + _name = "exemption.code.rule" + _description = "Avatax Custom Rules" + + name = fields.Char(index=True, default=lambda self: _("New")) + state = fields.Selection( + [ + ("draft", "Draft"), + ("progress", "In Progress"), + ("done", "Done"), + ("cancel", "Cancelled"), + ], + default="draft", + ) + exemption_code_id = fields.Many2one( + "exemption.code", + string="Entity Use Code", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + state_id = fields.Many2one( + "res.country.state", + readonly=True, + states={"draft": [("readonly", False)]}, + string="Region", + ) + avatax_id = fields.Char("Avatax Rule ID", readonly=True, copy=False) + avatax_tax_code = fields.Many2one( + "product.tax.code", readonly=True, states={"draft": [("readonly", False)]} + ) + is_all_juris = fields.Boolean( + default=True, readonly=True, states={"draft": [("readonly", False)]} + ) + avatax_rate = fields.Float(readonly=True, states={"draft": [("readonly", False)]}) + taxable = fields.Boolean() + + @api.constrains("avatax_rate") + def _check_avatax_rate(self): + """ + Prevent the Avatax rate with range 0 to 100 + """ + for record in self: + if record.avatax_rate < 0 or record.avatax_rate > 100: + raise ValidationError(_("Avatax rate range is from 0 to 100")) + + @api.model + def create(self, vals): + if vals.get("name", _("New")) == _("New"): + vals["name"] = self.env["ir.sequence"].next_by_code( + "exemption.code.rule.sequence" + ) or _("New") + return super().create(vals) + + def export_exemption_rule(self): + if self.filtered(lambda x: x.state != "draft"): + raise UserError(_("Rule is not in Draft state to Export Custom Rule")) + self.write({"state": "progress"}) + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_rule_export", "=", True)], limit=1) + ) + if not avalara_salestax: + raise UserError( + _("Avatax Exemption Rule export is disabled in Avatax configuration") + ) + avalara_salestax.export_new_exemption_rules( + rules=self.filtered(lambda x: not x.avatax_id) + ) + return True + + def cancel_exemption_rule(self): + self.ensure_one() + if self.state != "done": + raise UserError(_("Rule is not in Done state to Cancel Custom Rule")) + self.write({"state": "progress"}) + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_rule_export", "=", True)], limit=1) + ) + if not avalara_salestax: + raise UserError( + _("Avatax Exemption Rule export is disabled in Avatax configuration") + ) + avalara_salestax.with_delay( + priority=5, max_retries=2, description="Cancel Custom Rule %s" % (self.name) + )._cancel_custom_rule(self) + return True + + def enable_exemption_rule(self): + if self.filtered(lambda x: x.state != "cancel"): + raise UserError( + _("Rule is not in Cancelled state to Re-Export Custom Rule") + ) + self.write({"state": "progress"}) + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_rule_export", "=", True)], limit=1) + ) + if not avalara_salestax: + raise UserError( + _("Avatax Exemption Rule export is disabled in Avatax configuration") + ) + avalara_salestax.export_new_exemption_rules( + rules=self.filtered(lambda x: not x.avatax_id) + ) + return True + + def reset_to_draft(self): + self.write( + { + "state": "draft", + } + ) + + def cancel_exemption_rule_failed(self): + self.ensure_one() + queue_job_sudo = self.env["queue.job"].sudo() + queue_job = queue_job_sudo.search( + [ + ("method_name", "=", "_export_base_rule_based_on_type"), + ("state", "!=", "done"), + ("args", "ilike", "%[" + str(self.id) + "]%"), + ], + limit=1, + ) + + if queue_job: + queue_job.write( + { + "state": "done", + } + ) + self.write( + { + "state": "cancel", + } + ) + + +class ExemptionCode(models.Model): + _inherit = "exemption.code" + + flag = fields.Boolean( + "Taxed by default", + copy=False, + help="helps to add custom rules for the nexus Avatax states", + ) + rule_ids = fields.One2many("exemption.code.rule", "exemption_code_id") + + def create_rules(self): + self.ensure_one() + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_rule_export", "=", True)], limit=1) + ) + if not avalara_salestax: + raise UserError( + _("Avatax Exemption Rule export is disabled in Avatax configuration") + ) + for rule in self.rule_ids.filtered(lambda x: x.state == "draft"): + rule.export_exemption_rule() + return True + + +class ResPartnerExemption(models.Model): + _inherit = "res.partner.exemption" + + exemption_code_id = fields.Many2one( + related="business_type.exemption_code_id", + string="Entity Use Code", + readonly=True, + ) + + @api.onchange("partner_id") + def onchange_partner_id(self): + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_export", "=", True)], limit=1) + ) + if avalara_salestax.use_commercial_entity: + self.partner_id = self.partner_id.commercial_partner_id.id + return {"domain": {"partner_id": [("parent_id", "=", False)]}} + + def search_exemption_line(self, avatax_id): + exemption_line = ( + self.env["res.partner.exemption.line"] + .sudo() + .search([("avatax_id", "=", avatax_id)], limit=1) + ) + if exemption_line: + return "It is already Downloaded" + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_export", "=", True)], limit=1) + ) + if avalara_salestax: + job = avalara_salestax.with_delay( + description="Download Exemption: %s" % (avatax_id) + )._search_create_exemption_line(avatax_id) + return "Success" if job else "Failed" + else: + return "Exemption Export is disabled in Avatax configuration!" + + def export_exemption(self): + self.ensure_one() + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_export", "=", True)], limit=1) + ) + if not avalara_salestax: + raise UserError( + _("Avatax Exemption export is disabled in Avatax configuration") + ) + if not self.partner_id.customer_code: + raise UserError(_("No Customer code added in Partner")) + if not self.exemption_line_ids: + raise UserError(_("No Exemption Lines added")) + if self.partner_id and not self.partner_id.avatax_id: + avalara_salestax.with_delay( + priority=0, + max_retries=2, + description="Export Customer %s" % (self.partner_id.display_name), + )._export_avatax_customer(self.partner_id) + for exemption_line in self.exemption_line_ids: + if not exemption_line.avatax_id: + avalara_salestax.with_delay( + priority=5, + max_retries=2, + description="Export Exemption Line %s" % (exemption_line.name), + )._export_avatax_exemption_line(exemption_line) + self.write({"state": "progress"}) + return True + + def cancel_exemption(self): + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_export", "=", True)], limit=1) + ) + if not avalara_salestax: + raise UserError( + _("Avatax Exemption export is disabled in Avatax configuration") + ) + if self.state == "done": + for exemption_line in self.exemption_line_ids: + avalara_salestax.with_delay( + priority=5, + max_retries=2, + description="Disable Exemption Line %s" % (exemption_line.name), + )._update_avatax_exemption_line_status(exemption_line, False) + self.write({"state": "progress"}) + elif self.state == "progress": + self.write({"state": "cancel"}) + else: + raise UserError(_("Exemption status needs to be in Done status to cancel")) + return True + + def enable_exemption(self): + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_export", "=", True)], limit=1) + ) + if not avalara_salestax: + raise UserError( + _("Avatax Exemption export is disabled in Avatax configuration") + ) + if self.state == "cancel": + for exemption_line in self.exemption_line_ids: + avalara_salestax.with_delay( + priority=5, + max_retries=2, + description="Enable Exemption Line %s" % (exemption_line.name), + )._update_avatax_exemption_line_status(exemption_line, True) + self.write({"state": "progress"}) + else: + raise UserError( + _("Exemption status needs to be in Cancel status to enable") + ) + return True + + +class ResPartnerExemptionBusinessType(models.Model): + _inherit = "res.partner.exemption.business.type" + + exemption_code_id = fields.Many2one("exemption.code", string="Entity Use Code") + + +class ResPartnerExemptionType(models.Model): + _inherit = "res.partner.exemption.type" + + exemption_code_id = fields.Many2one( + related="business_type.exemption_code_id", + string="Entity Use Code", + readonly=True, + ) diff --git a/account_avatax_exemption/models/product.py b/account_avatax_exemption/models/product.py new file mode 100644 index 000000000..5def6b502 --- /dev/null +++ b/account_avatax_exemption/models/product.py @@ -0,0 +1,108 @@ +from odoo import fields, models + + +class ProductTaxCode(models.Model): + _inherit = "product.tax.code" + + rule_ids = fields.One2many( + "exemption.code.rule", + "avatax_tax_code", + "Avatax Rules", + ) + + +class ProductCategory(models.Model): + _inherit = "product.category" + + def write(self, vals): + res = super().write(vals) + if "tax_code_id" in vals: + products = self.env["product.product"].search( + [("categ_id", "in", self.ids)] + ) + if products: + products.create_job_taxitem() + return res + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def write(self, vals): + res = super().write(vals) + if "tax_code_id" in vals or "categ_id" in vals: + for template in self: + template.product_variant_ids.create_job_taxitem() + return res + + +class ProductProduct(models.Model): + _inherit = "product.product" + + avatax_item_id = fields.Char( + "Avatax TaxItem", + copy=False, + readonly=True, + ) + + def create_job_taxitem(self): + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("tax_item_export", "=", True)], limit=1) + ) + queue_job_sudo = self.env["queue.job"].sudo() + if not avalara_salestax or self._context.get("skip_job_creation", False): + return + self = self.with_context(skip_job_creation=True) + for product in self.filtered(lambda p: p.default_code): + if product.categ_id.tax_code_id or product.tax_code_id: + if product.avatax_item_id: + job = queue_job_sudo.search( + [ + ("method_name", "=", "_update_tax_item"), + ("state", "!=", "done"), + ("args", "ilike", "%[" + str(product.id) + "]%"), + ], + limit=1, + ) + if not job: + avalara_salestax.with_delay( + description="Update Tax Item %s" % (product.display_name) + )._update_tax_item(product.avatax_item_id, product) + else: + job = queue_job_sudo.search( + [ + ("method_name", "=", "_export_tax_item"), + ("state", "!=", "done"), + ("args", "ilike", "%[" + str(product.id) + "]%"), + ], + limit=1, + ) + if not job: + avalara_salestax.with_delay( + description="Export Tax Item %s" % (product.display_name) + )._export_tax_item(product) + elif ( + product.avatax_item_id + and not product.categ_id.tax_code_id + and not product.tax_code_id + ): + job = queue_job_sudo.search( + [ + ("method_name", "=", "_delete_tax_item"), + ("state", "!=", "done"), + ("args", "ilike", "%[" + str(product.id) + "]%"), + ], + limit=1, + ) + if not job: + avalara_salestax.with_delay( + description="Delete Tax Item %s" % (product.display_name) + )._delete_tax_item(product) + + def write(self, vals): + res = super().write(vals) + if "tax_code_id" in vals or "categ_id" in vals: + self.create_job_taxitem() + return res diff --git a/account_avatax_exemption/models/queue_job.py b/account_avatax_exemption/models/queue_job.py new file mode 100644 index 000000000..fcc934431 --- /dev/null +++ b/account_avatax_exemption/models/queue_job.py @@ -0,0 +1,43 @@ +from odoo import models + + +class QueueJob(models.Model): + _inherit = "queue.job" + + def _related_action_avatax_rule(self): + rule = self.args[0] + action = self.env.ref( + "account_avatax_exemption.exemption_rule_act_window" + ).read([])[0] + action.update( + { + "view_mode": "form", + "res_id": rule.id, + "domain": [("id", "=", rule.id)], + } + ) + return action + + def _related_action_avatax_tax_item(self): + product = self.args[0] + action = self.env.ref("product.product_normal_action_sell").read([])[0] + action.update( + { + "view_mode": "form", + "res_id": product.id, + "domain": [("id", "=", product.id)], + } + ) + return action + + def _related_action_avatax_customer(self): + partner = self.args[0] + action = self.env.ref("account.res_partner_action_customer").read([])[0] + action.update( + { + "view_mode": "form", + "res_id": partner.id, + "domain": [("id", "=", partner.id)], + } + ) + return action diff --git a/account_avatax_exemption/models/res_country_state.py b/account_avatax_exemption/models/res_country_state.py new file mode 100644 index 000000000..a865a4bc9 --- /dev/null +++ b/account_avatax_exemption/models/res_country_state.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class CountryState(models.Model): + _inherit = "res.country.state" + + rule_ids = fields.One2many("exemption.code.rule", "state_id", "Avatax Rules") diff --git a/account_avatax_exemption/readme/CONTRIBUTORS.rst b/account_avatax_exemption/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..bd20edaf6 --- /dev/null +++ b/account_avatax_exemption/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* Sodexis + + * Atchuthan Ubendran + * Stephan Keller + * SodexisTeam diff --git a/account_avatax_exemption/readme/DESCRIPTION.rst b/account_avatax_exemption/readme/DESCRIPTION.rst new file mode 100644 index 000000000..fd8ef207a --- /dev/null +++ b/account_avatax_exemption/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module is a component of the Avatax Exemption Integration with odoo app. + + * Export Exemption customer in Avatax + * Export Exemptions for customer based on nexus region + * Export Custom rules based on avatax nexus regions + * Export Product Taxcodes to Avatax diff --git a/account_avatax_exemption/readme/USAGE.rst b/account_avatax_exemption/readme/USAGE.rst new file mode 100644 index 000000000..e69de29bb diff --git a/account_avatax_exemption/security/ir.model.access.csv b/account_avatax_exemption/security/ir.model.access.csv new file mode 100644 index 000000000..1753bb5f8 --- /dev/null +++ b/account_avatax_exemption/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_exemption_code_rule,access_exemption_code_rule,model_exemption_code_rule,base.group_user,1,1,1,1 +access_portal_exemption_code,access_portal_exemption_code,account_avatax_oca.model_exemption_code,,1,0,0,0 diff --git a/account_avatax_exemption/security/security.xml b/account_avatax_exemption/security/security.xml new file mode 100644 index 000000000..338732861 --- /dev/null +++ b/account_avatax_exemption/security/security.xml @@ -0,0 +1,14 @@ + + + + Show Tax Exempt/Exemption number/Exemption code + Show the Exemption fields on SO and Invoice and Contact + + + + diff --git a/account_avatax_exemption/static/description/icon.png b/account_avatax_exemption/static/description/icon.png new file mode 100644 index 000000000..048650389 Binary files /dev/null and b/account_avatax_exemption/static/description/icon.png differ diff --git a/account_avatax_exemption/static/description/index.html b/account_avatax_exemption/static/description/index.html new file mode 100644 index 000000000..f2ddcfc6c --- /dev/null +++ b/account_avatax_exemption/static/description/index.html @@ -0,0 +1,436 @@ + + + + + +Avatax Exemptions + + + +
+

Avatax Exemptions

+ + +

Beta License: AGPL-3 OCA/account-fiscal-rule Translate me on Weblate Try me on Runboat

+

This module is a component of the Avatax Exemption Integration with odoo app.

+
+
    +
  • Export Exemption customer in Avatax
  • +
  • Export Exemptions for customer based on nexus region
  • +
  • Export Custom rules based on avatax nexus regions
  • +
  • Export Product Taxcodes to Avatax
  • +
+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Sodexis
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

This module is part of the OCA/account-fiscal-rule project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_avatax_exemption/static/description/menu_icon.png b/account_avatax_exemption/static/description/menu_icon.png new file mode 100644 index 000000000..bba613f30 Binary files /dev/null and b/account_avatax_exemption/static/description/menu_icon.png differ diff --git a/account_avatax_exemption/views/account_move_view.xml b/account_avatax_exemption/views/account_move_view.xml new file mode 100644 index 000000000..a58f53206 --- /dev/null +++ b/account_avatax_exemption/views/account_move_view.xml @@ -0,0 +1,33 @@ + + + + account.move.form.view.exemption.hide + account.move + + + + account_avatax_exemption.group_show_tax_exempt_exempt_number_exempt_code + + + account_avatax_exemption.group_show_tax_exempt_exempt_number_exempt_code + + + + + + + + + + + diff --git a/account_avatax_exemption/views/avalara_exemption_view.xml b/account_avatax_exemption/views/avalara_exemption_view.xml new file mode 100644 index 000000000..819219b64 --- /dev/null +++ b/account_avatax_exemption/views/avalara_exemption_view.xml @@ -0,0 +1,197 @@ + + + res.partner.exemption.form.view + res.partner.exemption + form + + + +