From b8b77974ffcd24fda38fb707ac1a5daaae1e2fc9 Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Sat, 5 Sep 2026 19:43:29 +0300 Subject: [PATCH] [FIX] kw_api_custom_endpoint: apply the endpoint domain when change() resolves by id The configured domain was applied on the list branch and ignored by the by-id branches. change() resolved its target with a bare m.search([(self.model_id_field, '=', obj_id)], limit=1) and Odoo only skips the implicit active = True when a domain mentions active, which that one does not. An archived record was therefore never found, write() on the empty recordset was a silent no-op, and data_response serialised it as {"content": [], "code": "200"} so the caller was told a write succeeded that never happened. An endpoint configured with [("active", "in", [True, False])] - which declares archived records to be in scope - still could not write to them. The same gap let a POST update a record the endpoint's domain was meant to exclude. change() now resolves through api_get_obj_domain(), which narrows the id lookup by the endpoint's own domain. No context is touched: a domain that mentions active makes Odoo skip active_test by itself. Behaviour change worth calling out: an id that resolves to nothing now answers 400: Wrong ID - the same shape response() already uses - instead of a 200 that claims a write occurred. Verified on Odoo 19.0 against a product.template endpoint: domain [("active","in",[True,False])] active -> 200, record echoed, written domain [("active","in",[True,False])] archived -> 200, record echoed, written (was: 200, content: [], no write) no domain active -> 200, record echoed, written (unchanged) no domain archived -> 400 Wrong ID (was: 200, content: [], no write) any unknown id -> 400 Wrong ID Closes #2 --- kw_api_custom_endpoint/__manifest__.py | 2 +- .../models/custom_endpoint.py | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/kw_api_custom_endpoint/__manifest__.py b/kw_api_custom_endpoint/__manifest__.py index e09ce61..0cbbe1a 100644 --- a/kw_api_custom_endpoint/__manifest__.py +++ b/kw_api_custom_endpoint/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Custom API controller', - 'version': '19.0.1.7.4', + 'version': '19.0.1.7.5', 'license': 'LGPL-3', 'category': 'Extra Tools', diff --git a/kw_api_custom_endpoint/models/custom_endpoint.py b/kw_api_custom_endpoint/models/custom_endpoint.py index 9334863..50b417d 100644 --- a/kw_api_custom_endpoint/models/custom_endpoint.py +++ b/kw_api_custom_endpoint/models/custom_endpoint.py @@ -344,6 +344,30 @@ def get_requested_domain(self, kw_api, **kw): domain.append((s_field_name, '=', v)) return domain + def api_get_obj_domain(self, obj_id): + """Domain that resolves one record by id, narrowed by this endpoint's own domain. + + The list branch has always applied ``self.domain``; the by-id branches did not, so + a record the endpoint declares to be in scope could still be invisible to them. + + This also settles ``active`` without touching the context: Odoo skips the implicit + ``active = True`` whenever a domain mentions ``active``, so an endpoint configured + with ``[("active", "in", [True, False])]`` reaches archived records, and one that + says nothing about ``active`` keeps the previous behaviour exactly. + """ + self.ensure_one() + domain = [(self.model_id_field, '=', obj_id)] + if not self.domain: + return domain + try: + endpoint_domain = safe_eval(self.domain) + except Exception as e: + _logger.debug(e) + return domain + if not endpoint_domain: + return domain + return AND([domain, endpoint_domain]) + def change(self, kw_api, obj_id=False, **kw): self.ensure_one() m = self.env[self.model_id.model].sudo() @@ -363,7 +387,11 @@ def change(self, kw_api, obj_id=False, **kw): try: if obj_id: obj_id = m.search( - [(self.model_id_field, '=', obj_id)], limit=1) + self.api_get_obj_domain(obj_id), limit=1) + if not obj_id: + return kw_api.response( + code=400, error='Wrong ID', data={'error': { + 'code': '400', 'message': 'Wrong ID'}}, ) obj_id.write(data) else: obj_id = m.create(data)