diff --git a/models/stock_location.py b/models/stock_location.py index 21908b1..90643dd 100644 --- a/models/stock_location.py +++ b/models/stock_location.py @@ -7,114 +7,49 @@ _logger = logging.getLogger(__name__) class StockLocation(models.Model): _inherit = 'stock.location' - def _get_allowed_locations(self): + @api.model + def _get_allowed_locations_for_mo(self, mo_id=None, picking_type_id=None): """ - Deep Data Audit: Log the names and types of every location found. + Public helper for JS to fetch allowed locations for a given MO or Picking Type. """ - ctx = self.env.context - - # 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)) - allowed_ids = [] source_name = "None" - - # 1. MO Lookup + + # 1. MO Lookup (Direct Database Query) if mo_id: 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 source_name = f"MO {mo.name}" - # 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 + # 2. Picking Type Fallback + if not allowed_ids and picking_type_id: + pt = self.env['stock.picking.type'].sudo().browse(picking_type_id) + if pt.exists(): + if pt.default_location_src_ids: + allowed_ids = pt.default_location_src_ids.ids + source_name = f"PT {pt.display_name}" + elif pt.default_location_src_id: + allowed_ids = [pt.default_location_src_id.id] + source_name = f"PT {pt.display_name} (M21)" - # 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}") + _logger.info(f"DEBUG_RESTRICT: Identified {len(allowed_ids)} Allowed Locations for {source_name}: {allowed_ids}") return allowed_ids - # 4. Fallback to default - if ctx.get('default_location_id'): - return [ctx.get('default_location_id')] - - return False - -class StockQuant(models.Model): - _inherit = 'stock.quant' - - @api.model - def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None): - """ - 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 native filters - clean_leaves = [] - product_id = None - for leaf in domain: - if isinstance(leaf, (list, tuple)): - if len(leaf) == 3: - if leaf[0] == 'location_id': continue - if leaf[0] == 'product_id': product_id = leaf[2] - clean_leaves.append(leaf) - - # Apply multi-location filter - domain = expression.AND([clean_leaves, [('location_id', 'child_of', allowed_ids)]]) - - # 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) + return [] class StockLot(models.Model): _inherit = 'stock.lot' @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): + # We KEEP the Lot search override as it is necessary for the lot dropdown selection. + # This only affects the searching of lots and has no side-effects on Quant creation. ctx = self.env.context if not ctx.get('skip_location_restriction') and ctx.get('uid'): - allowed_ids = self.env['stock.location']._get_allowed_locations() + mo_id = (ctx.get('active_mo_id') or ctx.get('default_production_id') or ctx.get('production_id')) + allowed_ids = self.env['stock.location']._get_allowed_locations_for_mo(mo_id=mo_id, picking_type_id=ctx.get('default_picking_type_id')) if allowed_ids: quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)] if ctx.get('default_product_id'): @@ -126,9 +61,11 @@ class StockLot(models.Model): @api.model def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None): + # We KEEP the Lot search override for the search catalogs as well. ctx = self.env.context if not ctx.get('skip_location_restriction') and ctx.get('uid'): - allowed_ids = self.env['stock.location']._get_allowed_locations() + mo_id = (ctx.get('active_mo_id') or ctx.get('default_production_id') or ctx.get('production_id')) + allowed_ids = self.env['stock.location']._get_allowed_locations_for_mo(mo_id=mo_id, picking_type_id=ctx.get('default_picking_type_id')) if allowed_ids: quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)] if ctx.get('default_product_id'): 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 64061e0..644a968 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 @@ -12,34 +12,53 @@ patch(SMLX2ManyField.prototype, { return super.onAdd({ context, editable }); } - console.log("DEBUG_RESTRICT: Intercepting SMLX2ManyField.onAdd"); - console.log("DEBUG_RESTRICT: Record Data:", this.props.record.data); + console.log("DEBUG_RESTRICT: Intercepting SMLX2ManyField.onAdd (Attempt 17)"); // 1. Sync Dirty Data (Internal Odoo Logic) await this.updateDirtyQuantsData(); - // 2. Resolve Manufacturing Order ID (ID identification) + // 2. Resolve Manufacturing Order ID 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) + // 3. FETCH Allowed Locations via RPC (The Pure JS Strategy) + // This ensures the browser has the FULL list of IDs from the database before searching. + let allowedIds = []; + try { + allowedIds = await this.orm.call( + "stock.location", + "_get_allowed_locations_for_mo", + [], + { + mo_id: mo_id, + picking_type_id: this.props.context.default_picking_type_id, + } + ); + console.log("DEBUG_RESTRICT: RPC returned Allowed IDs:", allowedIds); + } catch (e) { + console.error("DEBUG_RESTRICT: RPC Failed, falling back to default.", e); + } + + // 4. Prepare Context context = { ...context, single_product: true, 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, - default_allowed_source_location_ids: this.props.context.default_allowed_source_location_ids, + default_allowed_source_location_ids: allowedIds, }; const productName = this.props.record.data.product_id.display_name; const title = _t("Add line: %s", productName); - // 4. Construct Domain + // 5. Construct Domain (The JS-Only Filter) + const targetLocation = allowedIds.length > 0 ? allowedIds : this.props.context.default_location_id; + let domain = [ ["product_id", "=", this.props.record.data.product_id.id], - ["location_id", "child_of", this.props.context.default_location_id], + ["location_id", "child_of", targetLocation], ["quantity", ">", 0.0], ]; @@ -47,7 +66,7 @@ patch(SMLX2ManyField.prototype, { domain.push(["on_hand", "=", true]); } - // 5. Filter out fully used quants (Internal Odoo Logic) + // 6. Filter out fully used quants (Internal Odoo Logic) if (this.dirtyQuantsData.size) { const notFullyUsed = []; const fullyUsed = []; @@ -66,9 +85,9 @@ patch(SMLX2ManyField.prototype, { } } - console.log("DEBUG_RESTRICT: Passing active_mo_id:", mo_id); + console.log("DEBUG_RESTRICT: Final Domain (JS-Side):", domain); - // 6. Open the selection modal + // 7. Open the selection modal return this.selectCreate({ domain, context, title }); } });