diff --git a/models/stock_location.py b/models/stock_location.py index a72c793..21908b1 100644 --- a/models/stock_location.py +++ b/models/stock_location.py @@ -9,69 +9,57 @@ class StockLocation(models.Model): def _get_allowed_locations(self): """ - Server-side lookup for allowed locations with direct database priority. + Deep Data Audit: Log the names and types of every location found. """ ctx = self.env.context - # 1. HIGHEST PRIORITY: Direct Manufacturing Order Database Lookup - # This bypasses incomplete UI data by querying the MO directly via context IDs. + # Identification variables mo_id = (ctx.get('active_mo_id') or - ctx.get('default_production_id') or - ctx.get('production_id') or - ctx.get('active_id') if ctx.get('active_model') == 'mrp.production' else None) + ctx.get('default_production_id') or + ctx.get('production_id') or + (ctx.get('active_id') if ctx.get('active_model') == 'mrp.production' else None)) + allowed_ids = [] + source_name = "None" + + # 1. MO Lookup if mo_id: - # sudo() ensures we can read the MO and its allowed locations regardless of field access mo = self.env['mrp.production'].sudo().browse(mo_id) if mo.exists() and mo.allowed_source_location_ids: allowed_ids = mo.allowed_source_location_ids.ids - _logger.error(f"DEBUG_RESTRICT: Found IDs via Direct MO Lookup ({mo.name}): {allowed_ids}") - return allowed_ids + source_name = f"MO {mo.name}" - # 2. FALLBACK 1: Explicit IDs passed in context (from Views/JS) - target_keys = ['allowed_source_location_ids', 'default_allowed_source_location_ids'] - for key in target_keys: - val = ctx.get(key) - if not val: - continue - - # Simple IDs or Commands - if isinstance(val, list): - if val and all(isinstance(x, int) for x in val): - _logger.error(f"DEBUG_RESTRICT: Found IDs in context key '{key}': {val}") - return val - - col_ids = [] - for entry in val: - if isinstance(entry, (list, tuple)): - if entry[0] == 6: return entry[2] # SET - if entry[0] == 4: col_ids.append(entry[1]) # LINK - if col_ids: - _logger.error(f"DEBUG_RESTRICT: Extracted IDs from context command '{key}': {col_ids}") - return col_ids - - if isinstance(val, int): - _logger.error(f"DEBUG_RESTRICT: Found single ID in context key '{key}': {val}") - return [val] + # 2. Context ID Fallback + if not allowed_ids: + target_keys = ['allowed_source_location_ids', 'default_allowed_source_location_ids'] + for key in target_keys: + val = ctx.get(key) + if val: + if isinstance(val, list): + if all(isinstance(x, int) for x in val): + allowed_ids = val + else: + for entry in val: + if isinstance(entry, (list, tuple)): + if entry[0] == 6: allowed_ids = entry[2]; break + if entry[0] == 4: allowed_ids.append(entry[1]) + elif isinstance(val, int): + allowed_ids = [val] + + if allowed_ids: + source_name = f"Context {key}" + break - # 3. FALLBACK 2: Operation Type (Picking Type) Manual Lookup - picking_type_id = (ctx.get('default_picking_type_id') or - ctx.get('picking_type_id') or - ctx.get('active_picking_type_id')) - - if picking_type_id: - pt = self.env['stock.picking.type'].sudo().browse(picking_type_id) - if pt.exists(): - if pt.default_location_src_ids: - _logger.error(f"DEBUG_RESTRICT: Found IDs via Picking Type {pt.display_name}: {pt.default_location_src_ids.ids}") - return pt.default_location_src_ids.ids - if pt.default_location_src_id: - _logger.error(f"DEBUG_RESTRICT: Found ID via Picking Type {pt.display_name} (M21): {pt.default_location_src_id.id}") - return [pt.default_location_src_id.id] + # 3. Final IDs check & Logging + if allowed_ids: + # Audit the location names + locations = self.env['stock.location'].sudo().browse(allowed_ids) + loc_names = [f"{l.display_name} (ID: {l.id})" for l in locations] + _logger.error(f"DEBUG_RESTRICT: Identified {len(loc_names)} Allowed Locations for {source_name}: {loc_names}") + return allowed_ids - # 4. FINAL FALLBACK: Current Default Source Location + # 4. Fallback to default if ctx.get('default_location_id'): - _logger.error(f"DEBUG_RESTRICT: Falling back to default_location_id: {ctx.get('default_location_id')}") return [ctx.get('default_location_id')] return False @@ -82,27 +70,40 @@ class StockQuant(models.Model): @api.model def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None): """ - UI Override with ROBUST DOMAIN STRIPPING to fix RPC_ERROR/ValueError. + UI Override with GLOBAL AUDIT to find hiding stock. """ ctx = self.env.context if not ctx.get('skip_location_restriction') and ctx.get('uid'): allowed_ids = self.env['stock.location']._get_allowed_locations() if allowed_ids: - # STRIP both native location_id filters AND their orphaned operators. - # The safest way is to extract only the non-location tuples (leaves). - # expression.AND(list_of_tuples) reconstruction a perfectly valid - # Polish notation with implicit 'AND' between our leaves. + # Strip native filters clean_leaves = [] + product_id = None for leaf in domain: if isinstance(leaf, (list, tuple)): - if len(leaf) == 3 and leaf[0] == 'location_id': - continue + if len(leaf) == 3: + if leaf[0] == 'location_id': continue + if leaf[0] == 'product_id': product_id = leaf[2] clean_leaves.append(leaf) - # Re-apply our multi-location filter using expression.AND. - # This treats clean_leaves + [location_id filter] as a fresh, valid domain. + # Apply multi-location filter domain = expression.AND([clean_leaves, [('location_id', 'child_of', allowed_ids)]]) - _logger.error(f"DEBUG_RESTRICT: SEARCH QUANT - Allowed: {allowed_ids} - Final Domain: {domain}") + + # EXECUTE SEARCH once internally to see if it's empty + # If it's empty, we do a global warehouse search to tell the user WHERE his stock is. + res = super().web_search_read(domain, specification, offset=offset, limit=limit, order=order, count_limit=count_limit) + + if res.get('length') == 0 and product_id: + # Global Audit! + _logger.error(f"DEBUG_RESTRICT: Catalog is EMPTY for product {product_id} in {allowed_ids}.") + all_quants = self.sudo().search([('product_id', '=', product_id), ('quantity', '>', 0)]) + if all_quants: + loc_summary = [f"{q.location_id.display_name} (Qty: {q.quantity})" for q in all_quants] + _logger.error(f"DEBUG_RESTRICT: AUDIT: Product was found in {len(all_quants)} OTHER locations: {loc_summary}") + else: + _logger.error(f"DEBUG_RESTRICT: AUDIT: Product has ZERO quantity currently in the entire database (sudo).") + + return res return super().web_search_read(domain, specification, offset=offset, limit=limit, order=order, count_limit=count_limit) diff --git a/static/src/js/stock_move_line_x2_many_field_patch.js b/static/src/js/stock_move_line_x2_many_field_patch.js index f10f6bc..64061e0 100644 --- a/static/src/js/stock_move_line_x2_many_field_patch.js +++ b/static/src/js/stock_move_line_x2_many_field_patch.js @@ -13,12 +13,12 @@ patch(SMLX2ManyField.prototype, { } console.log("DEBUG_RESTRICT: Intercepting SMLX2ManyField.onAdd"); + console.log("DEBUG_RESTRICT: Record Data:", this.props.record.data); // 1. Sync Dirty Data (Internal Odoo Logic) await this.updateDirtyQuantsData(); // 2. Resolve Manufacturing Order ID (ID identification) - // We look in multiple places because Odoo 19 context keys can vary. const mo_id = (this.props.context.active_mo_id || this.props.context.default_production_id || this.props.record.data.raw_material_production_id?.[0]); @@ -30,17 +30,13 @@ patch(SMLX2ManyField.prototype, { list_view_ref: "stock.view_stock_quant_tree_simple", active_mo_id: mo_id, default_picking_type_id: this.props.context.default_picking_type_id, - // Keep the original IDs as fallback default_allowed_source_location_ids: this.props.context.default_allowed_source_location_ids, }; const productName = this.props.record.data.product_id.display_name; const title = _t("Add line: %s", productName); - // 4. Construct Domain (The Fix) - // We intentionally use Odoo's default_location_id here because our Python - // code in web_search_read will STRIP this and replace it with ALL allowed IDs. - // This ensures compatibility with Odoo's native JS domain construction. + // 4. Construct Domain let domain = [ ["product_id", "=", this.props.record.data.product_id.id], ["location_id", "child_of", this.props.context.default_location_id],