{"record":{"id":"49312703269f13af","repo":"odoo/odoo","slug":"only-read-access-to-identifiers-is-allowed","errorCode":null,"errorMessage":"Only read access to identifiers is allowed","messagePattern":"Only read access to identifiers is allowed","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"addons/account_tax_python/tools/formula_utils.py","lineNumber":90,"sourceCode":"    \"\"\"\n    def __init__(self, env):\n        self.env = env\n        super().__init__()\n\n    def visit(self, node):\n        if not isinstance(node, _NODE_WHITELIST):\n            raise ValidationError(self.env._(\"Invalid AST node: %s\", type(node).__name__))\n        super().visit(node)\n\n    def visit_Constant(self, node: ast.Constant):\n        if not isinstance(node.value, _ALLOWED_CONSTANT_T):\n            raise ValidationError(self.env._(\"Only int, float or None are allowed as constant values\"))\n\n    def visit_Name(self, node: ast.Name):\n        if node.id not in _ALLOWED_NAMES:\n            raise ValidationError(self.env._(\"Unknown identifier: %s\", str(node.id)))\n        if not isinstance(node.ctx, ast.Load):\n            raise ValidationError(self.env._(\"Only read access to identifiers is allowed\"))\n\n    def visit_Call(self, node: ast.Call):\n        if not (\n            isinstance(node.func, ast.Name)\n            and node.func.id in _ALLOWED_FUNCS\n            and isinstance(node.func.ctx, ast.Load)\n        ):\n            raise ValidationError(self.env._(\"Unknown function call\"))\n        # don't visit node.func: it's already validated and min/max aren't allowed as normal Name identifiers\n        for arg in node.args:\n            self.visit(arg)\n        if node.keywords:\n            raise ValidationError(self.env._(\"Kwargs are not allowed\"))\n\n    def visit_Subscript(self, node: ast.Subscript):\n        # Only allow string constants as subscripts (e.g., product[\"type\"])\n        # They are not allowed elsewhere in the formula\n        if not (","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/odoo/odoo/blob/1e661df964b1b264c9cef3ab28430d4785be3fda/addons/account_tax_python/tools/formula_utils.py#L72-L108","documentation":"Raised by `visit_Name` in addons/account_tax_python/tools/formula_utils.py:90 when a Name node has a context other than `ast.Load` — i.e. the formula tries to WRITE to an identifier (assignment, augmented assignment, del) instead of only reading it. The sandbox formulas are pure expressions; only reading allowed variables and assigning the final result through the dedicated mechanism is supported.","triggerScenarios":"A formula like `base = base * 2\\nresult = base * 0.21` (reassignment of `base`), `product = 1`, or `del x` — the Store/Delete context on the Name node makes `isinstance(node.ctx, ast.Load)` false and the ValidationError fires.","commonSituations":"Users writing multi-statement python habits into the formula box; old Odoo python-tax formulas (pre-sandbox) that mutated `result` or inputs; documentation/examples copied from outside the sandbox era.","solutions":["Rewrite as a single expression using allowed names: compute intermediate values inline or with min/max nesting instead of assignments.","If a variable is genuinely needed, introduce it via `result = ...` style supported constructs only — check the module docs for the accepted assignment target in your version; otherwise inline it.","Precompute derived inputs (discounted base, factors) in the tax's Python context/overrides, not inside the formula."],"exampleFix":"# before (formula)\nbase = base * 0.9\nresult = base * 0.21\n\n# after (formula) — no assignment to inputs\nresult = (base * 0.9) * 0.21","handlingStrategy":"validation","validationCode":"import ast\n\ndef formula_is_read_only(formula: str) -> bool:\n    return not any(\n        isinstance(n.ctx, (ast.Store, ast.Del))\n        for n in ast.walk(ast.parse(formula, mode='exec'))\n        if isinstance(n, ast.Name)\n    )","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Write formulas as one expression; avoid assignment to inputs entirely.","Inline intermediate calculations instead of mutating base/product/uom.","Validate read-only-ness in onchange on the tax form to fail before save."],"tags":["odoo","tax-python","ast-validation","assignment","formula"],"backgroundTag":null,"analyzedSha":"1e661df964b1b264c9cef3ab28430d4785be3fda","analyzedAt":"2026-08-15T05:22:16.142Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}