diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..129cada5f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*requirements.txt merge=union diff --git a/account_avatax_exemption/README.rst b/account_avatax_exemption/README.rst new file mode 100644 index 000000000..7a04c2bc1 --- /dev/null +++ b/account_avatax_exemption/README.rst @@ -0,0 +1,101 @@ +================= +Avatax Exemptions +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4f33f0e81d317ea9f024e588810347c719cf5b27853ccefde85677b7ec7f6818 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.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-18-0/account-fiscal-rule-18-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=18.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: + +Usage +===== + + + +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 + +- Open Source Integrators (https://opensourceintegrators.com) + + - Nikul Chaudhary + +- Kencove (https://kencove.com) + + - Don Kendall + - Mohamed Alkobrosli + - Wai-Lun Lin + +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..1f79007c2 --- /dev/null +++ b/account_avatax_exemption/__manifest__.py @@ -0,0 +1,39 @@ +{ + "name": "Avatax Exemptions", + "version": "18.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_oca", + "account_avatax_sale_oca", + "account_avatax_exemption_base", + ], + "data": [ + "security/ir.model.access.csv", + "data/cron.xml", + "data/queue.xml", + "data/ir_sequence_data.xml", + "data/res_partner_exemption_business_type.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", + ], + "images": ["static/description/avatax_icon.png"], + "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..fe1a20955 --- /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()._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..50aa7cda9 --- /dev/null +++ b/account_avatax_exemption/data/cron.xml @@ -0,0 +1,29 @@ + + + Avatax TaxItem: Export New Items + + code + model.export_new_tax_items() + 1 + days + + + + Avatax Rules: Export New Rules + + code + model.export_new_exemption_rules() + 1 + days + + + + Avatax Exemption Download + + code + model.download_exemptions() + 1 + days + + + 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/data/res_partner_exemption_business_type.xml b/account_avatax_exemption/data/res_partner_exemption_business_type.xml new file mode 100644 index 000000000..261118be3 --- /dev/null +++ b/account_avatax_exemption/data/res_partner_exemption_business_type.xml @@ -0,0 +1,313 @@ + + + + AGRICULTURE + + 56 + + + + BANKING ONLY + + 75 + + + + CAPITAL IMPROVEMENT + + 76 + + + + CHARITABLE/EXEMPT ORG + + 57 + + + + COMMERCIAL AQUACULTURE (CN) + + 57 + + + + COMMERCIAL FISHERY (CN) + + 59 + + + + COMMON CARRIER + + 77 + + + + DIRECT MAIL + + 60 + + + + DIRECT PAY + + 61 + + + + EDUCATIONAL ORG + + 97 + + + + ENTERPRISE ZONE + + 78 + + + + EXEMPT BY CLIENT + + 79 + + + + EXEMPT BY STATUTE + + 80 + + + + EXPORTERS + + 81 + + + + EXPOSURE + + 16 + + + + EXPOSURE: EXPIRED CERT + + 19 + + + + EXPOSURE: INDIRECT CERT + + 21 + + + + EXPOSURE: INVALID CERT + + 20 + + + + EXPOSURE: MISSING CERT + + 18 + + + + EXPOSURE: NON-DELIVERABLE + + 1 + + + + FEDERAL GOV + + 62 + + + + FOREIGN DIPLOMAT + + 63 + + + + INDUSTRIAL PROD/MANUFACTURERS + + 64 + + + + LOCAL GOVERNMENT + + 65 + + + + LONG-TERM RENTAL + + 94 + + + + MATERIAL PURCHASE + + 82 + + + + MEDICAL + + 66 + + + + NON-DELIVERABLE + + 67 + + + + NON-NEXUS: EXPIRED CERT + + 25 + + + + NON-NEXUS: EXPOSURE + + 22 + + + + NON-NEXUS: INDIRECT CERT + + 27 + + + + NON-NEXUS: INVALID CERT + + 26 + + + + NON-NEXUS: MISSING CERT + + 24 + + + + NON-NEXUS: NON-DELIVERABLE + + 23 + + + + NON-RESIDENT + + 68 + + + + OTHER/CUSTOM + + 69 + + + + POLLUTION CONTROL + + 83 + + + + PRIME CONTRACTOR + + 84 + + + + R&D + + 85 + + + + RELIGIOUS/EDUCATIONAL ORG + + 98 + + + + RELIGIOUS ORG + + 99 + + + + RESALE + + 71 + + + + STATE GOV + + 100 + + + + TAXABLE + + 73 + + + + TRIBAL GOVERNMENT + + 74 + + 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..0c12f8a7d --- /dev/null +++ b/account_avatax_exemption/i18n/account_avatax_exemption.pot @@ -0,0 +1,535 @@ +# 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 +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..e2a55b781 --- /dev/null +++ b/account_avatax_exemption/models/avalara_salestax.py @@ -0,0 +1,867 @@ +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 = ( + f"{client.base_url}/api/v2/companies/{self.avatax_company_id}/items" + ) + 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=f"Export Tax Item {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=f"Export Rule {rule.name}", + )._export_base_rule_based_on_type(rule) + + 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=f"Download Exemption: {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": ( + f"{rule.state_id.avatax_name} - " + f"{rule.exemption_code_id.code} - " + f"{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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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 f"Product exported with Avatax ID: {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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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 f"Avatax ID not available in Product: {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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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 f"Avatax Customer ID: {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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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 f"Avatax Customer ID: {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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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=( + f"Link Customer {exemption_line.partner_id.display_name} " + f"with Exemption {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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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: {}\nCode: {}\nMessage: {}\nTarget: {}\nDetails;{}".format( + 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", + "=", + "{}:0".format(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 f"Exemption Already Downloaded\nSearch Response: {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..1684f2be8 --- /dev/null +++ b/account_avatax_exemption/models/exemption.py @@ -0,0 +1,317 @@ +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 + ) + state_id = fields.Many2one( + "res.country.state", + string="Region", + ) + avatax_id = fields.Char("Avatax Rule ID", readonly=True, copy=False) + avatax_tax_code = fields.Many2one("product.tax.code") + is_all_juris = fields.Boolean(default=True) + avatax_rate = fields.Float() + 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_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("name", _("New")) == _("New"): + vals["name"] = self.env["ir.sequence"].next_by_code( + "exemption.line.sequence" + ) or _("New") + return super().create(vals_list) + + 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=f"Cancel Custom Rule {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 ResPartner(models.Model): + _inherit = "res.partner" + + @api.model + def _search(self, domain, offset=0, limit=None, order=None): + context = dict(self._context) + if context.get("partner_exemption", False): + domain = domain or [] + avalara_salestax = ( + self.env["avalara.salestax"] + .sudo() + .search([("exemption_export", "=", True)], limit=1) + ) + if avalara_salestax.use_commercial_entity: + domain += [("parent_id", "=", False)] + return super()._search(domain, offset, limit, order) + + +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 + + 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=f"Download Exemption: {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=f"Export Customer {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=f"Export Exemption Line {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=f"Disable Exemption Line {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=f"Enable Exemption Line {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..ddacdeb2d --- /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=f"Update Tax Item {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=f"Export Tax Item {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=f"Delete Tax Item {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/pyproject.toml b/account_avatax_exemption/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/account_avatax_exemption/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_avatax_exemption/readme/CONTRIBUTORS.md b/account_avatax_exemption/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..8ea577fbe --- /dev/null +++ b/account_avatax_exemption/readme/CONTRIBUTORS.md @@ -0,0 +1,12 @@ +- Sodexis + - Atchuthan Ubendran \<\> + - Stephan Keller \<\> + - SodexisTeam \<\> + +- Open Source Integrators () + - Nikul Chaudhary \<\> + +- Kencove () + - Don Kendall \<\> + - Mohamed Alkobrosli \<\> + - Wai-Lun Lin \<\> diff --git a/account_avatax_exemption/readme/DESCRIPTION.md b/account_avatax_exemption/readme/DESCRIPTION.md new file mode 100644 index 000000000..839405b96 --- /dev/null +++ b/account_avatax_exemption/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +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.md b/account_avatax_exemption/readme/USAGE.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/account_avatax_exemption/readme/USAGE.md @@ -0,0 +1 @@ + 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..7638b722b --- /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,base.group_portal,1,0,0,0 diff --git a/account_avatax_exemption/static/description/avatax_icon.png b/account_avatax_exemption/static/description/avatax_icon.png new file mode 100644 index 000000000..0074d62a3 Binary files /dev/null and b/account_avatax_exemption/static/description/avatax_icon.png differ 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..fd6c2ee4a --- /dev/null +++ b/account_avatax_exemption/static/description/index.html @@ -0,0 +1,451 @@ + + + + + +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

+ +
+

Usage

+
+
+

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/views/avalara_exemption_view.xml b/account_avatax_exemption/views/avalara_exemption_view.xml new file mode 100644 index 000000000..478720eb2 --- /dev/null +++ b/account_avatax_exemption/views/avalara_exemption_view.xml @@ -0,0 +1,200 @@ + + + res.partner.exemption.form.view + res.partner.exemption + form + + + +