diff --git a/models/stock_location.py b/models/stock_location.py index c3945f1..3543a44 100644 --- a/models/stock_location.py +++ b/models/stock_location.py @@ -1,7 +1,6 @@ import logging from odoo import api, fields, models, _ from odoo.osv import expression -from odoo.exceptions import UserError, ValidationError _logger = logging.getLogger(__name__) @@ -10,68 +9,70 @@ class StockLocation(models.Model): def _get_allowed_locations(self): """ - Exhaustive lookup for allowed locations with deep diagnostic logging. + Server-side lookup for allowed locations with direct database priority. """ ctx = self.env.context - # 1. PRIORITY: Explicitly passed IDs from UI (Context / JS) - target_keys = [ - 'allowed_source_location_ids', - 'default_allowed_source_location_ids', - 'allowed_location_ids' - ] + # 1. HIGHEST PRIORITY: Direct Manufacturing Order Database Lookup + # This bypasses incomplete UI data by querying the MO directly via context IDs. + mo_id = (ctx.get('active_mo_id') or + ctx.get('default_mo_id') or + ctx.get('default_production_id') or + ctx.get('mo_id') or + ctx.get('production_id')) + 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 + + # 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 list of IDs - if isinstance(val, list) and all(isinstance(x, int) for x in val): - _logger.info(f"DEBUG_RESTRICT: Found IDs in context key '{key}': {val}") - return val - - # Command format: [(6, 0, [IDs]), (4, ID, 0), ...] + # 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] - if entry[0] == 4: col_ids.append(entry[1]) + if entry[0] == 6: return entry[2] # SET + if entry[0] == 4: col_ids.append(entry[1]) # LINK if col_ids: - _logger.info(f"DEBUG_RESTRICT: Extracted IDs from Command key '{key}': {col_ids}") + _logger.error(f"DEBUG_RESTRICT: Extracted IDs from context command '{key}': {col_ids}") return col_ids - # Single integer if isinstance(val, int): - _logger.info(f"DEBUG_RESTRICT: Found single ID in context key '{key}': {val}") + _logger.error(f"DEBUG_RESTRICT: Found single ID in context key '{key}': {val}") return [val] - # 2. FALLBACK: Operation Type (Picking Type) + # 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')) - mo_id = (ctx.get('active_mo_id') or ctx.get('default_mo_id') or ctx.get('default_production_id')) - - if not picking_type_id and mo_id: - mo = self.env['mrp.production'].browse(mo_id) - if mo.exists(): - picking_type_id = mo.picking_type_id.id - if picking_type_id: - pt = self.env['stock.picking.type'].browse(picking_type_id) + pt = self.env['stock.picking.type'].sudo().browse(picking_type_id) if pt.exists(): if pt.default_location_src_ids: - _logger.info(f"DEBUG_RESTRICT: Found IDs via Picking Type M2M: {pt.default_location_src_ids.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.info(f"DEBUG_RESTRICT: Found ID via Picking Type M21: {pt.default_location_src_id.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 FALLBACK: Current Source Location + # 4. FINAL FALLBACK: Current Default Source Location if ctx.get('default_location_id'): - _logger.info(f"DEBUG_RESTRICT: Falling back to default_location_id: {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 @@ -81,15 +82,26 @@ 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 DOMAIN STRIPPING to prevent Odoo's native JS from blocking us. + """ 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: - # Add location filter; ensure no existing location_id filter blocks us - # Odoo's native JS often adds its own ["location_id", "child_of", ...] - # We prioritize our list. - domain = expression.AND([domain, [('location_id', 'child_of', allowed_ids)]]) + # STRIP any existing native location_id filters from the domain. + # Odoo's JS often adds its own ["location_id", "child_of", single_id]. + # By stripping them, we ensure our multi-location list takes precedence. + stripped_domain = [] + for leaf in domain: + if isinstance(leaf, (list, tuple)) and len(leaf) == 3 and leaf[0] == 'location_id': + continue + stripped_domain.append(leaf) + + # Re-apply our multi-location filter + domain = expression.AND([stripped_domain, [('location_id', 'child_of', allowed_ids)]]) _logger.error(f"DEBUG_RESTRICT: SEARCH QUANT - Allowed: {allowed_ids} - Final Domain: {domain}") + return super().web_search_read(domain, specification, offset=offset, limit=limit, order=order, count_limit=count_limit) class StockLot(models.Model): 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 413acf0..f10f6bc 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 @@ -17,43 +17,33 @@ patch(SMLX2ManyField.prototype, { // 1. Sync Dirty Data (Internal Odoo Logic) await this.updateDirtyQuantsData(); - // 2. Resolve Allowed Locations from Record or Context - // This is key to fixing the "No record found" issue - let allowedIds = []; - const recordAllowedField = this.props.record.data.allowed_source_location_ids; - - if (recordAllowedField && recordAllowedField.records) { - allowedIds = recordAllowedField.records.map(r => r.resId); - } - - if (!allowedIds.length) { - allowedIds = this.props.context.default_allowed_source_location_ids || []; - } + // 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]); // 3. Prepare Context (Bridge to Python) context = { ...context, single_product: true, list_view_ref: "stock.view_stock_quant_tree_simple", - active_mo_id: (this.props.context.active_mo_id || - this.props.context.default_production_id || - this.props.record.data.raw_material_production_id?.[0]), + active_mo_id: mo_id, default_picking_type_id: this.props.context.default_picking_type_id, - default_allowed_source_location_ids: allowedIds, + // 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 prioritize our multi-location list. Fallback to default Odoo behavior if empty. - const targetLocation = allowedIds.length > 0 ? allowedIds : this.props.context.default_location_id; - - console.log("DEBUG_RESTRICT: Target Location(s) for Catalog Domain:", targetLocation); - + // 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. let domain = [ ["product_id", "=", this.props.record.data.product_id.id], - ["location_id", "child_of", targetLocation], + ["location_id", "child_of", this.props.context.default_location_id], ["quantity", ">", 0.0], ]; @@ -80,9 +70,9 @@ patch(SMLX2ManyField.prototype, { } } - console.log("DEBUG_RESTRICT: Final Constructed Domain:", domain); + console.log("DEBUG_RESTRICT: Passing active_mo_id:", mo_id); - // 6. Open the selection modal with our custom domain + // 6. Open the selection modal return this.selectCreate({ domain, context, title }); } });