refactor: prioritize direct MO lookup and implement domain stripping to ensure location restriction consistency

This commit is contained in:
Suherdy Yacob 2026-04-03 21:57:17 +07:00
parent c4c7e5b5e2
commit e72d491660
2 changed files with 62 additions and 60 deletions

View File

@ -1,7 +1,6 @@
import logging import logging
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from odoo.osv import expression from odoo.osv import expression
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -10,68 +9,70 @@ class StockLocation(models.Model):
def _get_allowed_locations(self): 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 ctx = self.env.context
# 1. PRIORITY: Explicitly passed IDs from UI (Context / JS) # 1. HIGHEST PRIORITY: Direct Manufacturing Order Database Lookup
target_keys = [ # This bypasses incomplete UI data by querying the MO directly via context IDs.
'allowed_source_location_ids', mo_id = (ctx.get('active_mo_id') or
'default_allowed_source_location_ids', ctx.get('default_mo_id') or
'allowed_location_ids' 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: for key in target_keys:
val = ctx.get(key) val = ctx.get(key)
if not val: if not val:
continue continue
# Simple list of IDs # Simple IDs or Commands
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), ...]
if isinstance(val, list): 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 = [] col_ids = []
for entry in val: for entry in val:
if isinstance(entry, (list, tuple)): if isinstance(entry, (list, tuple)):
if entry[0] == 6: return entry[2] if entry[0] == 6: return entry[2] # SET
if entry[0] == 4: col_ids.append(entry[1]) if entry[0] == 4: col_ids.append(entry[1]) # LINK
if col_ids: 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 return col_ids
# Single integer
if isinstance(val, int): 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] 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 picking_type_id = (ctx.get('default_picking_type_id') or
ctx.get('picking_type_id') or ctx.get('picking_type_id') or
ctx.get('active_picking_type_id')) 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: 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.exists():
if pt.default_location_src_ids: 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 return pt.default_location_src_ids.ids
if pt.default_location_src_id: 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] 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'): 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 [ctx.get('default_location_id')]
return False return False
@ -81,15 +82,26 @@ class StockQuant(models.Model):
@api.model @api.model
def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None): 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 ctx = self.env.context
if not ctx.get('skip_location_restriction') and ctx.get('uid'): if not ctx.get('skip_location_restriction') and ctx.get('uid'):
allowed_ids = self.env['stock.location']._get_allowed_locations() allowed_ids = self.env['stock.location']._get_allowed_locations()
if allowed_ids: if allowed_ids:
# Add location filter; ensure no existing location_id filter blocks us # STRIP any existing native location_id filters from the domain.
# Odoo's native JS often adds its own ["location_id", "child_of", ...] # Odoo's JS often adds its own ["location_id", "child_of", single_id].
# We prioritize our list. # By stripping them, we ensure our multi-location list takes precedence.
domain = expression.AND([domain, [('location_id', 'child_of', allowed_ids)]]) 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}") _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) return super().web_search_read(domain, specification, offset=offset, limit=limit, order=order, count_limit=count_limit)
class StockLot(models.Model): class StockLot(models.Model):

View File

@ -17,43 +17,33 @@ patch(SMLX2ManyField.prototype, {
// 1. Sync Dirty Data (Internal Odoo Logic) // 1. Sync Dirty Data (Internal Odoo Logic)
await this.updateDirtyQuantsData(); await this.updateDirtyQuantsData();
// 2. Resolve Allowed Locations from Record or Context // 2. Resolve Manufacturing Order ID (ID identification)
// This is key to fixing the "No record found" issue // We look in multiple places because Odoo 19 context keys can vary.
let allowedIds = []; const mo_id = (this.props.context.active_mo_id ||
const recordAllowedField = this.props.record.data.allowed_source_location_ids; this.props.context.default_production_id ||
this.props.record.data.raw_material_production_id?.[0]);
if (recordAllowedField && recordAllowedField.records) {
allowedIds = recordAllowedField.records.map(r => r.resId);
}
if (!allowedIds.length) {
allowedIds = this.props.context.default_allowed_source_location_ids || [];
}
// 3. Prepare Context (Bridge to Python) // 3. Prepare Context (Bridge to Python)
context = { context = {
...context, ...context,
single_product: true, single_product: true,
list_view_ref: "stock.view_stock_quant_tree_simple", list_view_ref: "stock.view_stock_quant_tree_simple",
active_mo_id: (this.props.context.active_mo_id || active_mo_id: mo_id,
this.props.context.default_production_id ||
this.props.record.data.raw_material_production_id?.[0]),
default_picking_type_id: this.props.context.default_picking_type_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 productName = this.props.record.data.product_id.display_name;
const title = _t("Add line: %s", productName); const title = _t("Add line: %s", productName);
// 4. Construct Domain (The Fix) // 4. Construct Domain (The Fix)
// We prioritize our multi-location list. Fallback to default Odoo behavior if empty. // We intentionally use Odoo's default_location_id here because our Python
const targetLocation = allowedIds.length > 0 ? allowedIds : this.props.context.default_location_id; // 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.
console.log("DEBUG_RESTRICT: Target Location(s) for Catalog Domain:", targetLocation);
let domain = [ let domain = [
["product_id", "=", this.props.record.data.product_id.id], ["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], ["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 }); return this.selectCreate({ domain, context, title });
} }
}); });