refactor: enhance location restriction logic with exhaustive context lookups and diagnostic logging
This commit is contained in:
parent
84702c810f
commit
c4c7e5b5e2
@ -10,74 +10,70 @@ class StockLocation(models.Model):
|
|||||||
|
|
||||||
def _get_allowed_locations(self):
|
def _get_allowed_locations(self):
|
||||||
"""
|
"""
|
||||||
Helper to retrieve allowed locations based on the current context.
|
Exhaustive lookup for allowed locations with deep diagnostic logging.
|
||||||
PRIORITY 1: Use explicit IDs passed in context (from views).
|
|
||||||
PRIORITY 2: Manual lookup from picking type or MO.
|
|
||||||
Returns:
|
|
||||||
list: IDs of allowed locations.
|
|
||||||
False: If no restriction should be applied.
|
|
||||||
"""
|
"""
|
||||||
ctx = self.env.context
|
ctx = self.env.context
|
||||||
|
|
||||||
# 1. PRIORITY: Check the context for allowed locations (Passed by views/JS)
|
# 1. PRIORITY: Explicitly passed IDs from UI (Context / JS)
|
||||||
target_keys = ['allowed_source_location_ids', 'default_allowed_source_location_ids']
|
target_keys = [
|
||||||
|
'allowed_source_location_ids',
|
||||||
|
'default_allowed_source_location_ids',
|
||||||
|
'allowed_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
|
||||||
|
|
||||||
# Case: List of IDs or Commands
|
# 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), ...]
|
||||||
if isinstance(val, list):
|
if isinstance(val, list):
|
||||||
# Simple list of integers
|
|
||||||
if val and all(isinstance(x, int) for x in val):
|
|
||||||
return val
|
|
||||||
|
|
||||||
# Command format: [(6, 0, [IDs]), (4, ID, 0), ...]
|
|
||||||
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: # SET command
|
if entry[0] == 6: return entry[2]
|
||||||
return entry[2]
|
if entry[0] == 4: col_ids.append(entry[1])
|
||||||
if entry[0] == 4: # LINK command
|
|
||||||
col_ids.append(entry[1])
|
|
||||||
if col_ids:
|
if col_ids:
|
||||||
|
_logger.info(f"DEBUG_RESTRICT: Extracted IDs from Command key '{key}': {col_ids}")
|
||||||
return col_ids
|
return col_ids
|
||||||
|
|
||||||
# Case: Single ID
|
# Single integer
|
||||||
if isinstance(val, int):
|
if isinstance(val, int):
|
||||||
|
_logger.info(f"DEBUG_RESTRICT: Found single ID in context key '{key}': {val}")
|
||||||
return [val]
|
return [val]
|
||||||
|
|
||||||
# 2. FALLBACK: Identify the Picking Type (Operation Type)
|
# 2. FALLBACK: Operation Type (Picking Type)
|
||||||
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 IDs
|
mo_id = (ctx.get('active_mo_id') or ctx.get('default_mo_id') or ctx.get('default_production_id'))
|
||||||
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 not picking_type_id and mo_id:
|
if not picking_type_id and mo_id:
|
||||||
mo = self.env['mrp.production'].browse(mo_id)
|
mo = self.env['mrp.production'].browse(mo_id)
|
||||||
if mo.exists():
|
if mo.exists():
|
||||||
picking_type_id = mo.picking_type_id.id
|
picking_type_id = mo.picking_type_id.id
|
||||||
|
|
||||||
if not picking_type_id:
|
if picking_type_id:
|
||||||
return False
|
pt = self.env['stock.picking.type'].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}")
|
||||||
|
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}")
|
||||||
|
return [pt.default_location_src_id.id]
|
||||||
|
|
||||||
|
# 3. FINAL FALLBACK: Current Source Location
|
||||||
|
if ctx.get('default_location_id'):
|
||||||
|
_logger.info(f"DEBUG_RESTRICT: Falling back to default_location_id: {ctx.get('default_location_id')}")
|
||||||
|
return [ctx.get('default_location_id')]
|
||||||
|
|
||||||
# 3. Retrieve allowed locations from the identified picking type
|
|
||||||
picking_type = self.env['stock.picking.type'].browse(picking_type_id)
|
|
||||||
if picking_type.exists():
|
|
||||||
# Many-to-Many "Allowed Source Locations"
|
|
||||||
if picking_type.default_location_src_ids:
|
|
||||||
return picking_type.default_location_src_ids.ids
|
|
||||||
|
|
||||||
# Many-to-One "Default Source Location"
|
|
||||||
if picking_type.default_location_src_id:
|
|
||||||
return [picking_type.default_location_src_id.id]
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class StockQuant(models.Model):
|
class StockQuant(models.Model):
|
||||||
@ -85,17 +81,15 @@ 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-SURFACE OVERRIDE: Applies location filtering ONLY for the web interface.
|
|
||||||
Uses 'child_of' to support stock stored in shelves/aisles of allowed locations.
|
|
||||||
"""
|
|
||||||
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_location_ids = self.env['stock.location']._get_allowed_locations()
|
allowed_ids = self.env['stock.location']._get_allowed_locations()
|
||||||
if allowed_location_ids:
|
if allowed_ids:
|
||||||
# Add location filter to the domain ONLY if we have an explicit list
|
# Add location filter; ensure no existing location_id filter blocks us
|
||||||
domain = expression.AND([domain, [('location_id', 'child_of', allowed_location_ids)]])
|
# 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)]])
|
||||||
|
_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):
|
||||||
@ -103,59 +97,28 @@ class StockLot(models.Model):
|
|||||||
|
|
||||||
@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):
|
||||||
"""
|
|
||||||
UI-SURFACE OVERRIDE: Filters the many2one lot selection dropdown.
|
|
||||||
"""
|
|
||||||
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_location_ids = self.env['stock.location']._get_allowed_locations()
|
allowed_ids = self.env['stock.location']._get_allowed_locations()
|
||||||
|
if allowed_ids:
|
||||||
# Safe Fallback for lots
|
quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)]
|
||||||
if not allowed_location_ids and ctx.get('default_location_id'):
|
if ctx.get('default_product_id'):
|
||||||
allowed_location_ids = [ctx.get('default_location_id')]
|
quant_domain.append(('product_id', '=', ctx.get('default_product_id')))
|
||||||
|
|
||||||
if allowed_location_ids:
|
|
||||||
quant_domain = [
|
|
||||||
('location_id', 'child_of', allowed_location_ids),
|
|
||||||
('quantity', '>', 0),
|
|
||||||
('lot_id', '!=', False)
|
|
||||||
]
|
|
||||||
product_id = ctx.get('default_product_id')
|
|
||||||
if product_id:
|
|
||||||
quant_domain.append(('product_id', '=', product_id))
|
|
||||||
|
|
||||||
quants = self.env['stock.quant'].with_context(skip_location_restriction=True).sudo().search(quant_domain)
|
quants = self.env['stock.quant'].with_context(skip_location_restriction=True).sudo().search(quant_domain)
|
||||||
lot_ids = quants.mapped('lot_id').ids
|
args = expression.AND([args or [], [('id', 'in', quants.mapped('lot_id').ids)]])
|
||||||
|
|
||||||
args = expression.AND([args or [], [('id', 'in', lot_ids)]])
|
|
||||||
|
|
||||||
return super().name_search(name, args=args, operator=operator, limit=limit)
|
return super().name_search(name, args=args, operator=operator, limit=limit)
|
||||||
|
|
||||||
@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-SURFACE OVERRIDE: Applies filtering for the Lot Catalog and list views.
|
|
||||||
"""
|
|
||||||
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_location_ids = self.env['stock.location']._get_allowed_locations()
|
allowed_ids = self.env['stock.location']._get_allowed_locations()
|
||||||
|
if allowed_ids:
|
||||||
if not allowed_location_ids and ctx.get('default_location_id'):
|
quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)]
|
||||||
allowed_location_ids = [ctx.get('default_location_id')]
|
if ctx.get('default_product_id'):
|
||||||
|
quant_domain.append(('product_id', '=', ctx.get('default_product_id')))
|
||||||
if allowed_location_ids:
|
|
||||||
quant_domain = [
|
|
||||||
('location_id', 'child_of', allowed_location_ids),
|
|
||||||
('quantity', '>', 0),
|
|
||||||
('lot_id', '!=', False)
|
|
||||||
]
|
|
||||||
product_id = ctx.get('default_product_id')
|
|
||||||
if product_id:
|
|
||||||
quant_domain.append(('product_id', '=', product_id))
|
|
||||||
|
|
||||||
quants = self.env['stock.quant'].with_context(skip_location_restriction=True).sudo().search(quant_domain)
|
|
||||||
lot_ids = quants.mapped('lot_id').ids
|
|
||||||
|
|
||||||
domain = expression.AND([domain, [('id', 'in', lot_ids)]])
|
quants = self.env['stock.quant'].with_context(skip_location_restriction=True).sudo().search(quant_domain)
|
||||||
|
domain = expression.AND([domain, [('id', 'in', quants.mapped('lot_id').ids)]])
|
||||||
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)
|
||||||
|
|||||||
@ -12,29 +12,45 @@ patch(SMLX2ManyField.prototype, {
|
|||||||
return super.onAdd({ context, editable });
|
return super.onAdd({ context, editable });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("DEBUG_RESTRICT: Intercepting SMLX2ManyField.onAdd");
|
||||||
|
|
||||||
// 1. Sync Dirty Data (Internal Odoo Logic)
|
// 1. Sync Dirty Data (Internal Odoo Logic)
|
||||||
await this.updateDirtyQuantsData();
|
await this.updateDirtyQuantsData();
|
||||||
|
|
||||||
// 2. Prepare Context (Bridge to Python)
|
// 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 || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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",
|
||||||
// Pass identification keys to our Python _get_allowed_locations()
|
active_mo_id: (this.props.context.active_mo_id ||
|
||||||
active_mo_id: context?.active_mo_id || this.props.context.active_mo_id || this.props.context.default_production_id,
|
this.props.context.default_production_id ||
|
||||||
default_picking_type_id: context?.default_picking_type_id || this.props.context.default_picking_type_id,
|
this.props.record.data.raw_material_production_id?.[0]),
|
||||||
default_allowed_source_location_ids: context?.default_allowed_source_location_ids || this.props.context.default_allowed_source_location_ids,
|
default_picking_type_id: this.props.context.default_picking_type_id,
|
||||||
|
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);
|
||||||
|
|
||||||
// 3. Construct Domain (The Fix)
|
// 4. Construct Domain (The Fix)
|
||||||
// Odoo natively uses ODOO_DEFAULT_LOCATION_ID which is just ONE location.
|
// We prioritize our multi-location list. Fallback to default Odoo behavior if empty.
|
||||||
// We override this to use ALL ALLOWED locations if they exist.
|
const targetLocation = allowedIds.length > 0 ? allowedIds : this.props.context.default_location_id;
|
||||||
const allowedLocationIds = context.default_allowed_source_location_ids;
|
|
||||||
const targetLocation = allowedLocationIds || this.props.context.default_location_id;
|
|
||||||
|
|
||||||
|
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", targetLocation],
|
||||||
@ -45,7 +61,7 @@ patch(SMLX2ManyField.prototype, {
|
|||||||
domain.push(["on_hand", "=", true]);
|
domain.push(["on_hand", "=", true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Filter out fully used quants (Internal Odoo Logic)
|
// 5. Filter out fully used quants (Internal Odoo Logic)
|
||||||
if (this.dirtyQuantsData.size) {
|
if (this.dirtyQuantsData.size) {
|
||||||
const notFullyUsed = [];
|
const notFullyUsed = [];
|
||||||
const fullyUsed = [];
|
const fullyUsed = [];
|
||||||
@ -57,7 +73,6 @@ patch(SMLX2ManyField.prototype, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fullyUsed.length) {
|
if (fullyUsed.length) {
|
||||||
// Combine domains using Odoo's Domain utility
|
|
||||||
domain = Domain.and([domain, [["id", "not in", fullyUsed]]]).toList();
|
domain = Domain.and([domain, [["id", "not in", fullyUsed]]]).toList();
|
||||||
}
|
}
|
||||||
if (notFullyUsed.length) {
|
if (notFullyUsed.length) {
|
||||||
@ -65,7 +80,9 @@ patch(SMLX2ManyField.prototype, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Open the selection modal with our custom domain
|
console.log("DEBUG_RESTRICT: Final Constructed Domain:", domain);
|
||||||
|
|
||||||
|
// 6. Open the selection modal with our custom domain
|
||||||
return this.selectCreate({ domain, context, title });
|
return this.selectCreate({ domain, context, title });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user