feat: Centralize source location restriction logic in StockQuant and StockLocation models using a new helper method and _search override, while updating views to pass allowed_source_location_ids in context.

This commit is contained in:
Suherdy Yacob 2026-02-16 13:15:11 +07:00
parent 69ec589db0
commit 79818cc017
2 changed files with 109 additions and 49 deletions

View File

@ -8,64 +8,105 @@ _logger = logging.getLogger(__name__)
# Log when this module is loaded # Log when this module is loaded
_logger.info("="*80) _logger.info("="*80)
_logger.info("STOCK_RESTRICT_SOURCE_LOCATION: stock_location.py module is being loaded!") _logger.info("STOCK_RESTRICT_SOURCE_LOCATION: stock_location.py module is being loaded!")
_logger.info("="*80)
class StockQuant(models.Model): class StockQuant(models.Model):
_inherit = 'stock.quant' _inherit = 'stock.quant'
def _get_allowed_locations(self):
"""Helper to extract allowed location IDs from context"""
ctx = self.env.context
allowed_location_ids = []
# 1. Try from explicit keys often passed by UI or patches
raw_ids = (ctx.get('allowed_source_location_ids') or
ctx.get('default_allowed_source_location_ids'))
if raw_ids:
if isinstance(raw_ids, list):
if raw_ids and isinstance(raw_ids[0], (list, tuple)) and raw_ids[0][0] == 6:
allowed_location_ids = raw_ids[0][2]
else:
allowed_location_ids = [r for r in raw_ids if isinstance(r, int)]
elif isinstance(raw_ids, int):
allowed_location_ids = [raw_ids]
# 2. Try from active move
if not allowed_location_ids:
active_move_id = ctx.get('active_move_id') or ctx.get('default_move_id')
if active_move_id:
move = self.env['stock.move'].sudo().browse(active_move_id)
if move.exists() and move.allowed_source_location_ids:
allowed_location_ids = move.allowed_source_location_ids.ids
# 3. Try from active MO
if not allowed_location_ids:
active_mo_id = ctx.get('active_mo_id') or ctx.get('default_raw_material_production_id')
if active_mo_id:
mo = self.env['mrp.production'].sudo().browse(active_mo_id)
if mo.exists() and mo.allowed_source_location_ids:
allowed_location_ids = mo.allowed_source_location_ids.ids
# 4. Fallback to picking type in context
if not allowed_location_ids:
picking_type_id = ctx.get('default_picking_type_id')
if picking_type_id:
picking_type = self.env['stock.picking.type'].sudo().browse(picking_type_id)
if picking_type.exists() and picking_type.default_location_src_ids:
allowed_location_ids = picking_type.default_location_src_ids.ids
return allowed_location_ids
@api.model @api.model
def _get_gather_domain(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False): def _get_gather_domain(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
"""Override to apply location restrictions during reservation (gather)""" """Override to apply location restrictions during reservation (gather)"""
ctx = self.env.context result_domain = super()._get_gather_domain(product_id, location_id, lot_id, package_id, owner_id, strict)
domain = super()._get_gather_domain(product_id, location_id, lot_id, package_id, owner_id, strict) allowed_location_ids = self._get_allowed_locations()
if allowed_location_ids:
result_domain = Domain.AND([result_domain, [('location_id', 'in', allowed_location_ids)]])
return result_domain
# Try to find picking type from context or active_move_id def _search(self, domain, offset=0, limit=None, order=None, *args, **kwargs):
picking_type_id = ctx.get('default_picking_type_id') """Override to apply location restrictions during search (e.g. catalog or list selection)"""
allowed_location_ids = self._get_allowed_locations()
if not picking_type_id: if allowed_location_ids:
active_move_id = ctx.get('active_move_id') # 1. Smart Domain Swap: find if there's already a location_id restriction (like child_of)
if active_move_id: # and REPLACE it instead of AND-ing it. This prevents collisions with Odoo's
move = self.env['stock.move'].browse(active_move_id) # default 'child_of location_src_id' behavior.
if move.exists(): found_collision = False
picking_type_id = move.picking_type_id.id new_domain = []
for leaf in domain:
if (isinstance(leaf, (list, tuple)) and
len(leaf) == 3 and
leaf[0] == 'location_id' and
leaf[1] == 'child_of'):
if not picking_type_id: new_domain.append(('location_id', 'in', allowed_location_ids))
active_mo_id = ctx.get('active_mo_id') found_collision = True
if active_mo_id: else:
mo = self.env['mrp.production'].browse(active_mo_id) new_domain.append(leaf)
if mo.exists():
picking_type_id = mo.picking_type_id.id
if picking_type_id: if found_collision:
picking_type = self.env['stock.picking.type'].browse(picking_type_id) domain = new_domain
if picking_type.default_location_src_ids: else:
allowed_location_ids = picking_type.default_location_src_ids.ids # 2. Standard AND merge if no collision found
# Restrict the domain to allowed source locations domain = Domain.AND([domain, [('location_id', 'in', allowed_location_ids)]])
domain = ['&'] + list(domain) + [('location_id', 'in', allowed_location_ids)]
return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs)
return domain
class StockLocation(models.Model): class StockLocation(models.Model):
_inherit = 'stock.location' _inherit = 'stock.location'
@api.model @api.model
def _name_search(self, name='', domain=None, operator='ilike', limit=None, order=None): def _name_search(self, name='', domain=None, operator='ilike', limit=None, order=None):
"""Override to restrict locations based on picking_type_id in context""" """Override to restrict locations based on context"""
_logger.info(f"LOCATION SEARCH CALLED: name={name}, context keys={list(self.env.context.keys())}, domain={domain}") _logger.info(f"LOCATION SEARCH: name={name}, context={self.env.context}")
domain = domain or [] domain = domain or []
allowed_location_ids = self.env['stock.quant']._get_allowed_locations()
# Check if we have a picking_type_id in context (from MO or picking) if allowed_location_ids:
picking_type_id = self.env.context.get('default_picking_type_id') domain = Domain.AND([domain, [('id', 'in', allowed_location_ids)]])
_logger.info(f"LOCATION SEARCH: Filtered to {allowed_location_ids}")
if picking_type_id:
picking_type = self.env['stock.picking.type'].browse(picking_type_id)
if picking_type.default_location_src_ids:
allowed_location_ids = picking_type.default_location_src_ids.ids
_logger.info(f"LOCATION_RESTRICT LOCATION SEARCH: Filtering locations to {allowed_location_ids} for picking type {picking_type.name}")
# Add restriction to domain
domain = domain + [('id', 'in', allowed_location_ids)]
else:
_logger.info(f"LOCATION SEARCH: No picking_type_id in context")
return super()._name_search(name=name, domain=domain, operator=operator, limit=limit, order=order) return super()._name_search(name=name, domain=domain, operator=operator, limit=limit, order=order)

View File

@ -21,7 +21,7 @@
<field name="allowed_source_location_ids" invisible="1"/> <field name="allowed_source_location_ids" invisible="1"/>
</xpath> </xpath>
<xpath expr="//field[@name='move_line_ids']" position="attributes"> <xpath expr="//field[@name='move_line_ids']" position="attributes">
<attribute name="context">{'list_view_ref': 'stock.view_stock_move_line_operation_tree', 'form_view_ref': 'stock.view_move_line_mobile_form', 'default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_company_id': company_id, 'active_picking_id': picking_id, 'active_mo_id': context.get('active_mo_id'), 'default_picking_type_id': picking_type_id}</attribute> <attribute name="context">{'list_view_ref': 'stock.view_stock_move_line_operation_tree', 'form_view_ref': 'stock.view_move_line_mobile_form', 'default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_company_id': company_id, 'active_picking_id': picking_id, 'active_mo_id': context.get('active_mo_id'), 'default_picking_type_id': picking_type_id, 'default_allowed_source_location_ids': allowed_source_location_ids}</attribute>
</xpath> </xpath>
</field> </field>
</record> </record>
@ -35,11 +35,12 @@
<field name="allowed_source_location_ids" invisible="1"/> <field name="allowed_source_location_ids" invisible="1"/>
</xpath> </xpath>
<xpath expr="//field[@name='quant_id']" position="attributes"> <xpath expr="//field[@name='quant_id']" position="attributes">
<attribute name="domain">[('product_id', '=', product_id), ('location_id', 'in', allowed_source_location_ids)]</attribute> <!-- Rely on backend StockQuant._search override for location filtering -->
<attribute name="context">{'default_location_id': location_id, 'default_product_id': product_id, 'search_view_ref': 'stock.quant_search_view', 'list_view_ref': 'stock.view_stock_quant_tree', 'form_view_ref': 'stock.view_stock_quant_form', 'readonly_form': True, 'show_src_package': 1, 'active_mo_id': context.get('active_mo_id'), 'default_picking_type_id': context.get('default_picking_type_id')}</attribute> <attribute name="context">{'default_location_id': location_id, 'default_product_id': product_id, 'search_view_ref': 'stock.quant_search_view', 'list_view_ref': 'stock.view_stock_quant_tree', 'form_view_ref': 'stock.view_stock_quant_form', 'readonly_form': True, 'show_src_package': 1, 'active_mo_id': context.get('active_mo_id'), 'default_picking_type_id': context.get('default_picking_type_id'), 'default_allowed_source_location_ids': allowed_source_location_ids}</attribute>
</xpath> </xpath>
<xpath expr="//field[@name='location_id']" position="attributes"> <xpath expr="//field[@name='location_id']" position="attributes">
<attribute name="domain">[('id', 'in', allowed_source_location_ids)]</attribute> <!-- Rely on backend StockLocation._name_search override -->
<attribute name="options">{'no_create': True}</attribute>
</xpath> </xpath>
</field> </field>
</record> </record>
@ -50,14 +51,32 @@
<field name="inherit_id" ref="stock.view_stock_move_line_operation_tree"/> <field name="inherit_id" ref="stock.view_stock_move_line_operation_tree"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='move_id']" position="after"> <xpath expr="//field[@name='move_id']" position="after">
<field name="allowed_source_location_ids" invisible="1"/> <field name="allowed_source_location_ids" invisible="1"/>
</xpath> </xpath>
<xpath expr="//field[@name='quant_id']" position="attributes"> <xpath expr="//field[@name='quant_id']" position="attributes">
<attribute name="domain">[('product_id', '=', product_id), ('location_id', 'in', allowed_source_location_ids)]</attribute> <!-- Rely on backend StockQuant._search override for location filtering -->
<attribute name="context">{'default_location_id': location_id, 'default_product_id': product_id, 'search_view_ref': 'stock.quant_search_view', 'list_view_ref': 'stock.view_stock_quant_tree', 'form_view_ref': 'stock.view_stock_quant_form', 'readonly_form': True, 'show_src_package': 1, 'active_mo_id': context.get('active_mo_id'), 'default_picking_type_id': context.get('default_picking_type_id')}</attribute> <attribute name="context">{'default_location_id': location_id, 'default_product_id': product_id, 'search_view_ref': 'stock.quant_search_view', 'list_view_ref': 'stock.view_stock_quant_tree', 'form_view_ref': 'stock.view_stock_quant_form', 'readonly_form': True, 'show_src_package': 1, 'active_mo_id': context.get('active_mo_id'), 'default_picking_type_id': context.get('default_picking_type_id'), 'default_allowed_source_location_ids': allowed_source_location_ids}</attribute>
</xpath> </xpath>
<xpath expr="//field[@name='location_id']" position="attributes"> <xpath expr="//field[@name='location_id']" position="attributes">
<attribute name="domain">[('id', 'in', allowed_source_location_ids)]</attribute> <!-- Rely on backend StockLocation._name_search override -->
<attribute name="options">{'no_create': True}</attribute>
</xpath>
</field>
</record>
<record id="view_move_line_form_restrict" model="ir.ui.view">
<field name="name">stock.move.line.form.restrict</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_move_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='location_id']" position="before">
<field name="picking_type_id" invisible="1"/>
<field name="move_id" invisible="1"/>
<field name="allowed_source_location_ids" invisible="1"/>
</xpath>
<xpath expr="//field[@name='location_id']" position="attributes">
<!-- Rely on backend StockLocation._name_search override -->
<attribute name="options">{'no_create': True}</attribute>
</xpath> </xpath>
</field> </field>
</record> </record>