From 12b8a72a89390f92275cf9dc2ec286b51115af30 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 3 Apr 2026 16:55:49 +0700 Subject: [PATCH] fix: add bypass for ID-based searches and system operations to prevent validation errors in stock location and lot filtering --- models/stock_location.py | 42 ++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/models/stock_location.py b/models/stock_location.py index e34c834..a023d9e 100644 --- a/models/stock_location.py +++ b/models/stock_location.py @@ -16,9 +16,12 @@ class StockQuant(models.Model): """Helper to extract allowed location IDs from context""" ctx = self.env.context - # FIX: If we are picking from a non-internal location (like Transit, Supplier, or Customer), - # we should NOT apply strict internal location restrictions, because the stock MUST come - # from that exact external/transit location. + # 1. Skip restrictions if we are performing a bypass or internal system operation + if ctx.get('skip_location_restriction') or ctx.get('prefetch_fields'): + return [] + + # 2. FIX: If we are picking from a non-internal location (like Transit, Supplier, or Customer), + # we should NOT apply strict internal location restrictions. loc_id = ctx.get('default_location_id') if loc_id: loc = self.env['stock.location'].sudo().browse(loc_id) @@ -27,7 +30,7 @@ class StockQuant(models.Model): allowed_location_ids = [] - # 1. Try from explicit keys often passed by UI or patches + # 3. Try from explicit keys often passed by UI or patches raw_ids = (ctx.get('allowed_source_location_ids') or ctx.get('default_allowed_source_location_ids')) if raw_ids: @@ -39,15 +42,15 @@ class StockQuant(models.Model): elif isinstance(raw_ids, int): allowed_location_ids = [raw_ids] - # 2. Try from active move + # 4. Try from active move if not allowed_location_ids: active_move_id = ctx.get('active_move_id') or ctx.get('default_move_id') if active_move_id: move = self.env['stock.move'].sudo().browse(active_move_id) if move.exists() and move.allowed_source_location_ids: allowed_location_ids = move.allowed_source_location_ids.ids - - # 3. Try from active MO + + # 5. Try from active MO if not allowed_location_ids: active_mo_id = ctx.get('active_mo_id') or ctx.get('default_raw_material_production_id') if active_mo_id: @@ -55,8 +58,7 @@ class StockQuant(models.Model): if mo.exists() and mo.allowed_source_location_ids: allowed_location_ids = mo.allowed_source_location_ids.ids - # 4. Fallback to picking type in context - # 4. Fallback to picking type in context + # 6. Fallback to picking type in context if not allowed_location_ids: picking_type_id = ctx.get('default_picking_type_id') if picking_type_id: @@ -82,12 +84,16 @@ class StockQuant(models.Model): def _search(self, domain, offset=0, limit=None, order=None, *args, **kwargs): """Override to apply location restrictions during search (e.g. catalog or list selection)""" + # CRITICAL FIX: If we are searching for specific IDs (e.g., Odoo is fetching selected records for save), + # we must NOT restrict the search results or we trigger 'Missing Product' validation errors + # on backend fallback create logic. + search_by_id = any(isinstance(leaf, (list, tuple)) and leaf[0] == 'id' for leaf in domain) + if search_by_id: + return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs) + allowed_location_ids = self._get_allowed_locations() - if allowed_location_ids: # 1. Smart Domain Swap: find if there's already a location_id restriction (like child_of) - # and REPLACE it instead of AND-ing it. This prevents collisions with Odoo's - # default 'child_of location_src_id' behavior. found_collision = False new_domain = [] for leaf in domain: @@ -104,7 +110,6 @@ class StockQuant(models.Model): if found_collision: domain = new_domain else: - # 2. Standard AND merge if no collision found domain = Domain.AND([domain, [('location_id', 'in', allowed_location_ids)]]) return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs) @@ -133,23 +138,22 @@ class StockLot(models.Model): def _search(self, domain, offset=0, limit=None, order=None, *args, **kwargs): ctx = self.env.context - # We only want to filter if the user is in a picking using lot_id directly (like Receive operations) + # FIX: If we are searching for specific IDs, bypass custom filtering to avoid backend failures + search_by_id = any(isinstance(leaf, (list, tuple)) and leaf[0] == 'id' for leaf in domain) + if search_by_id: + return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs) + active_picking_id = ctx.get('active_picking_id') loc_id = ctx.get('default_location_id') if active_picking_id and loc_id: loc = self.env['stock.location'].sudo().browse(loc_id) - - # If the source is an internal or transit location, restrict the dropdown to lots actually present there. - # If the source is a supplier, we do not filter (they could be receiving brand new lots). if loc.exists() and loc.usage != 'supplier': quant_domain = [ ('location_id', 'child_of', loc.id), ('quantity', '>', 0), ('lot_id', '!=', False) ] - - # Highly optimized query: only search quants for the specific product product_id = ctx.get('default_product_id') if product_id: quant_domain.append(('product_id', '=', product_id))