refactor: replace context-based location filtering with direct RPC-based location resolution for improved reliability
This commit is contained in:
parent
d34bc37428
commit
3c2955fb13
@ -7,114 +7,49 @@ _logger = logging.getLogger(__name__)
|
|||||||
class StockLocation(models.Model):
|
class StockLocation(models.Model):
|
||||||
_inherit = 'stock.location'
|
_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 = []
|
allowed_ids = []
|
||||||
source_name = "None"
|
source_name = "None"
|
||||||
|
|
||||||
# 1. MO Lookup
|
# 1. MO Lookup (Direct Database Query)
|
||||||
if mo_id:
|
if mo_id:
|
||||||
mo = self.env['mrp.production'].sudo().browse(mo_id)
|
mo = self.env['mrp.production'].sudo().browse(mo_id)
|
||||||
if mo.exists() and mo.allowed_source_location_ids:
|
if mo.exists() and mo.allowed_source_location_ids:
|
||||||
allowed_ids = mo.allowed_source_location_ids.ids
|
allowed_ids = mo.allowed_source_location_ids.ids
|
||||||
source_name = f"MO {mo.name}"
|
source_name = f"MO {mo.name}"
|
||||||
|
|
||||||
# 2. Context ID Fallback
|
# 2. Picking Type Fallback
|
||||||
if not allowed_ids:
|
if not allowed_ids and picking_type_id:
|
||||||
target_keys = ['allowed_source_location_ids', 'default_allowed_source_location_ids']
|
pt = self.env['stock.picking.type'].sudo().browse(picking_type_id)
|
||||||
for key in target_keys:
|
if pt.exists():
|
||||||
val = ctx.get(key)
|
if pt.default_location_src_ids:
|
||||||
if val:
|
allowed_ids = pt.default_location_src_ids.ids
|
||||||
if isinstance(val, list):
|
source_name = f"PT {pt.display_name}"
|
||||||
if all(isinstance(x, int) for x in val):
|
elif pt.default_location_src_id:
|
||||||
allowed_ids = val
|
allowed_ids = [pt.default_location_src_id.id]
|
||||||
else:
|
source_name = f"PT {pt.display_name} (M21)"
|
||||||
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:
|
if allowed_ids:
|
||||||
source_name = f"Context {key}"
|
_logger.info(f"DEBUG_RESTRICT: Identified {len(allowed_ids)} Allowed Locations for {source_name}: {allowed_ids}")
|
||||||
break
|
|
||||||
|
|
||||||
# 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
|
return allowed_ids
|
||||||
|
|
||||||
# 4. Fallback to default
|
return []
|
||||||
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)
|
|
||||||
|
|
||||||
class StockLot(models.Model):
|
class StockLot(models.Model):
|
||||||
_inherit = 'stock.lot'
|
_inherit = 'stock.lot'
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
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
|
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()
|
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:
|
if allowed_ids:
|
||||||
quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)]
|
quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)]
|
||||||
if ctx.get('default_product_id'):
|
if ctx.get('default_product_id'):
|
||||||
@ -126,9 +61,11 @@ class StockLot(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):
|
||||||
|
# We KEEP the Lot search override for the search catalogs as well.
|
||||||
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()
|
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:
|
if allowed_ids:
|
||||||
quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)]
|
quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)]
|
||||||
if ctx.get('default_product_id'):
|
if ctx.get('default_product_id'):
|
||||||
|
|||||||
@ -12,34 +12,53 @@ patch(SMLX2ManyField.prototype, {
|
|||||||
return super.onAdd({ context, editable });
|
return super.onAdd({ context, editable });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("DEBUG_RESTRICT: Intercepting SMLX2ManyField.onAdd");
|
console.log("DEBUG_RESTRICT: Intercepting SMLX2ManyField.onAdd (Attempt 17)");
|
||||||
console.log("DEBUG_RESTRICT: Record Data:", this.props.record.data);
|
|
||||||
|
|
||||||
// 1. Sync Dirty Data (Internal Odoo Logic)
|
// 1. Sync Dirty Data (Internal Odoo Logic)
|
||||||
await this.updateDirtyQuantsData();
|
await this.updateDirtyQuantsData();
|
||||||
|
|
||||||
// 2. Resolve Manufacturing Order ID (ID identification)
|
// 2. Resolve Manufacturing Order ID
|
||||||
const mo_id = (this.props.context.active_mo_id ||
|
const mo_id = (this.props.context.active_mo_id ||
|
||||||
this.props.context.default_production_id ||
|
this.props.context.default_production_id ||
|
||||||
this.props.record.data.raw_material_production_id?.[0]);
|
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 = {
|
||||||
...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: mo_id,
|
active_mo_id: mo_id,
|
||||||
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: 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 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
|
// 5. Construct Domain (The JS-Only Filter)
|
||||||
|
const targetLocation = allowedIds.length > 0 ? allowedIds : this.props.context.default_location_id;
|
||||||
|
|
||||||
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", this.props.context.default_location_id],
|
["location_id", "child_of", targetLocation],
|
||||||
["quantity", ">", 0.0],
|
["quantity", ">", 0.0],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -47,7 +66,7 @@ patch(SMLX2ManyField.prototype, {
|
|||||||
domain.push(["on_hand", "=", true]);
|
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) {
|
if (this.dirtyQuantsData.size) {
|
||||||
const notFullyUsed = [];
|
const notFullyUsed = [];
|
||||||
const fullyUsed = [];
|
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 });
|
return this.selectCreate({ domain, context, title });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user