diff --git a/account_avatax_exemption/models/avalara_salestax.py b/account_avatax_exemption/models/avalara_salestax.py
new file mode 100644
index 000000000..87bd74791
--- /dev/null
+++ b/account_avatax_exemption/models/avalara_salestax.py
@@ -0,0 +1,866 @@
+import requests
+
+from odoo import _, fields, models
+from odoo.exceptions import UserError
+
+from odoo.addons.account_avatax.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,
+ avatax_line_override=None,
+ is_override=None,
+ currency_id=None,
+ ignore_error=None,
+ ):
+ 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,
+ avatax_line_override=avatax_line_override,
+ is_override=is_override,
+ currency_id=currency_id,
+ ignore_error=ignore_error,
+ )
+
+ 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 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_oca/__manifest__.py b/account_avatax_oca/__manifest__.py
index 1f02478fb..f979e4d9d 100644
--- a/account_avatax_oca/__manifest__.py
+++ b/account_avatax_oca/__manifest__.py
@@ -17,6 +17,7 @@
"wizard/avalara_get_company_code_view.xml",
"wizard/avalara_salestax_address_validate_view.xml",
"wizard/avalara_salestax_ping_view.xml",
+ "wizard/account_move_reversal.xml",
"views/avalara_salestax_view.xml",
"views/partner_view.xml",
"views/product_view.xml",
diff --git a/account_avatax_oca/models/account_move.py b/account_avatax_oca/models/account_move.py
index b74239942..958364d05 100644
--- a/account_avatax_oca/models/account_move.py
+++ b/account_avatax_oca/models/account_move.py
@@ -3,6 +3,7 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tests.common import Form
+from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
@@ -109,6 +110,11 @@ def onchange_warehouse_id(self):
avatax_response_log = fields.Text(
"Avatax API Response Log", readonly=True, copy=False
)
+ avatax_amt_line_override = fields.Boolean(
+ string="Use Odoo Tax",
+ default=False,
+ help="The Odoo tax will be uploaded to Avatax",
+ )
@api.model
@api.depends("company_id")
@@ -193,6 +199,9 @@ def _avatax_compute_tax(self, commit=False):
if not avatax_config:
# Skip Avatax computation if no configuration is found
return
+ avatax_line_override = (
+ self.avatax_amt_line_override and self.move_type == "out_refund"
+ )
doc_type = self._get_avatax_doc_type(commit=commit)
tax_date = self.get_origin_tax_date() or self.invoice_date
taxable_lines = self._avatax_prepare_lines(doc_type)
@@ -214,6 +223,7 @@ def _avatax_compute_tax(self, commit=False):
# TODO: can we report self.invoice_doc_no?
self.name if self.move_type == "out_refund" else "",
self.location_code or "",
+ avatax_line_override,
is_override=self.move_type == "out_refund",
currency_id=self.currency_id,
ignore_error=300 if commit else None,
@@ -233,7 +243,7 @@ def _avatax_compute_tax(self, commit=False):
avatax_config.commit_transaction(self.name, doc_type)
return tax_result
- if self.state == "draft":
+ if self.state == "draft" and not avatax_line_override:
Tax = self.env["account.tax"]
tax_result_lines = {int(x["lineNumber"]): x for x in tax_result["lines"]}
taxes_to_set = []
@@ -254,6 +264,7 @@ def _avatax_compute_tax(self, commit=False):
line_taxes = line.tax_ids.filtered(lambda x: not x.is_avatax)
taxes_to_set.append((index, line_taxes | tax))
line.avatax_amt_line = tax_result_line["tax"]
+ line.avatax_tax_type = tax_result_line["details"][0]["taxSubTypeId"]
self.with_context(check_move_validity=False).avatax_amount = tax_result[
"totalTax"
]
@@ -285,6 +296,7 @@ def avatax_compute_taxes(self, commit=False):
invoice.move_type in ["out_invoice", "out_refund"]
and invoice.fiscal_position_id.is_avatax
and (invoice.state == "draft" or commit)
+ and (not invoice.avatax_amt_line_override or commit)
):
invoice._avatax_compute_tax(commit=commit)
return True
@@ -428,11 +440,24 @@ def create(self, vals_list):
move.avatax_compute_taxes()
return moves
+ def action_reverse(self):
+ action = super().action_reverse()
+ avatax_tax_type = self.invoice_line_ids.filtered(lambda t: t.avatax_tax_type)
+ action["context"] = safe_eval(action.get("context", "{}"))
+ action["context"].update(
+ {
+ "default_avatax_amt_line_override": self.avatax_amt_line_override,
+ "hide_override": 1 if avatax_tax_type else 0,
+ }
+ )
+ return action
+
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
avatax_amt_line = fields.Float(string="AvaTax Line", copy=False)
+ avatax_tax_type = fields.Char()
def _get_avatax_amount(self, qty=None):
"""
@@ -486,6 +511,9 @@ def _avatax_prepare_line(self, sign=1, doc_type=None):
amount = sign * line._get_avatax_amount()
if line.quantity < 0:
amount = -amount
+ avatax_amt = 0.0
+ if line.move_id.move_type == "out_refund":
+ avatax_amt = -(line.price_total - line.price_subtotal)
res = {
"qty": line.quantity,
"itemcode": item_code,
@@ -495,6 +523,8 @@ def _avatax_prepare_line(self, sign=1, doc_type=None):
"id": line,
"account_id": line.account_id.id,
"tax_id": line.tax_ids,
+ "avatax_amt_line": round(avatax_amt, 2),
+ "avatax_tax_type": line.avatax_tax_type,
}
return res
diff --git a/account_avatax_oca/models/avalara_salestax.py b/account_avatax_oca/models/avalara_salestax.py
index 366446c31..a0da61a55 100644
--- a/account_avatax_oca/models/avalara_salestax.py
+++ b/account_avatax_oca/models/avalara_salestax.py
@@ -195,6 +195,7 @@ def create_transaction(
invoice_date=None,
reference_code=None,
location_code=None,
+ avatax_line_override=None,
is_override=None,
currency_id=None,
ignore_error=None,
@@ -284,6 +285,7 @@ def create_transaction(
location_code,
currency_code,
partner.vat or None,
+ avatax_line_override,
is_override,
ignore_error=ignore_error,
log_to_record=log_to_record,
diff --git a/account_avatax_oca/models/avatax_rest_api.py b/account_avatax_oca/models/avatax_rest_api.py
index 14781b89f..2fbeb6338 100644
--- a/account_avatax_oca/models/avatax_rest_api.py
+++ b/account_avatax_oca/models/avatax_rest_api.py
@@ -222,6 +222,7 @@ def get_tax(
location_code=None,
currency_code="USD",
vat=None,
+ avatax_line_override=None,
is_override=False,
ignore_error=None,
log_to_record=False,
@@ -255,6 +256,18 @@ def get_tax(
"quantity": line.get("qty", 1),
"amount": line.get("amount", 0.0),
"taxCode": line.get("tax_code"),
+ "taxOverride": {
+ "type": "TaxAmountByTaxType",
+ "reason": "Refund",
+ "taxAmountByTaxTypes": [
+ {
+ "taxTypeId": line.get("avatax_tax_type"),
+ "TaxAmount": line.get("avatax_amt_line", 0.0),
+ }
+ ],
+ }
+ if avatax_line_override and line.get("avatax_tax_type")
+ else None,
}
for line in received_lines
]
@@ -295,7 +308,7 @@ def get_tax(
"type": doc_type,
"commit": commit,
}
- if is_override and invoice_date:
+ if is_override and invoice_date and not avatax_line_override:
create_transaction.update(
{
"taxOverride": {
diff --git a/account_avatax_oca/security/avalara_salestax_security.xml b/account_avatax_oca/security/avalara_salestax_security.xml
index 63eb0db44..3dc19f1a0 100644
--- a/account_avatax_oca/security/avalara_salestax_security.xml
+++ b/account_avatax_oca/security/avalara_salestax_security.xml
@@ -27,4 +27,11 @@
model="ir.rule"
search="[('model_id', '=', ref('model_product_tax_code'))]"
/>
+
+ Can View Avatax Override
+
+
diff --git a/account_avatax_oca/views/account_move_view.xml b/account_avatax_oca/views/account_move_view.xml
index 11bb66fc6..358602544 100644
--- a/account_avatax_oca/views/account_move_view.xml
+++ b/account_avatax_oca/views/account_move_view.xml
@@ -60,6 +60,11 @@
name="invoice_doc_no"
attrs="{'invisible': [('move_type','!=','out_refund')]}"
/>
+
diff --git a/account_avatax_oca/wizard/__init__.py b/account_avatax_oca/wizard/__init__.py
index 5ff1001ae..fe1407a98 100644
--- a/account_avatax_oca/wizard/__init__.py
+++ b/account_avatax_oca/wizard/__init__.py
@@ -1,3 +1,4 @@
from . import avalara_salestax_ping
from . import avalara_salestax_address_validate
from . import avalara_get_company_code
+from . import account_move_reversal
diff --git a/account_avatax_oca/wizard/account_move_reversal.py b/account_avatax_oca/wizard/account_move_reversal.py
new file mode 100644
index 000000000..9cefea5bc
--- /dev/null
+++ b/account_avatax_oca/wizard/account_move_reversal.py
@@ -0,0 +1,20 @@
+from odoo import fields, models
+
+
+class AccountMoveReversal(models.TransientModel):
+ """
+ Account move reversal wizard, it cancel an account move by reversing it.
+ """
+
+ _inherit = "account.move.reversal"
+
+ avatax_amt_line_override = fields.Boolean(
+ string="Use Odoo Tax",
+ default=False,
+ help="The Odoo tax will be uploaded to Avatax",
+ )
+
+ def _prepare_default_reversal(self, move):
+ res = super()._prepare_default_reversal(move)
+ res.update({"avatax_amt_line_override": self.avatax_amt_line_override})
+ return res
diff --git a/account_avatax_oca/wizard/account_move_reversal.xml b/account_avatax_oca/wizard/account_move_reversal.xml
new file mode 100644
index 000000000..8c36e94c2
--- /dev/null
+++ b/account_avatax_oca/wizard/account_move_reversal.xml
@@ -0,0 +1,16 @@
+
+
+ account.move.reversal.form
+ account.move.reversal
+
+ form
+
+
+
+
+
+
+