From 79818cc0179c187500222ed7296285e68cfdb65f Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 16 Feb 2026 13:15:11 +0700 Subject: [PATCH] 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. --- models/stock_location.py | 123 +++++++++++++++++++++----------- views/stock_move_line_views.xml | 35 ++++++--- 2 files changed, 109 insertions(+), 49 deletions(-) diff --git a/models/stock_location.py b/models/stock_location.py index e264531..6132e88 100644 --- a/models/stock_location.py +++ b/models/stock_location.py @@ -8,64 +8,105 @@ _logger = logging.getLogger(__name__) # Log when this module is loaded _logger.info("="*80) _logger.info("STOCK_RESTRICT_SOURCE_LOCATION: stock_location.py module is being loaded!") -_logger.info("="*80) class StockQuant(models.Model): _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 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)""" - ctx = self.env.context - domain = super()._get_gather_domain(product_id, location_id, lot_id, package_id, owner_id, strict) - - # Try to find picking type from context or active_move_id - picking_type_id = ctx.get('default_picking_type_id') - - if not picking_type_id: - active_move_id = ctx.get('active_move_id') - if active_move_id: - move = self.env['stock.move'].browse(active_move_id) - if move.exists(): - picking_type_id = move.picking_type_id.id + result_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 - if not picking_type_id: - active_mo_id = ctx.get('active_mo_id') - if active_mo_id: - mo = self.env['mrp.production'].browse(active_mo_id) - if mo.exists(): - picking_type_id = mo.picking_type_id.id + def _search(self, domain, offset=0, limit=None, order=None, *args, **kwargs): + """Override to apply location restrictions during search (e.g. catalog or list selection)""" + allowed_location_ids = self._get_allowed_locations() - 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 - # Restrict the domain to allowed source locations - domain = ['&'] + list(domain) + [('location_id', 'in', allowed_location_ids)] - - return domain + if allowed_location_ids: + # 1. Smart Domain Swap: find if there's already a location_id restriction (like child_of) + # and REPLACE it instead of AND-ing it. This prevents collisions with Odoo's + # default 'child_of location_src_id' behavior. + found_collision = False + 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'): + + new_domain.append(('location_id', 'in', allowed_location_ids)) + found_collision = True + else: + new_domain.append(leaf) + + if found_collision: + domain = new_domain + else: + # 2. Standard AND merge if no collision found + domain = Domain.AND([domain, [('location_id', 'in', allowed_location_ids)]]) + + return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs) + class StockLocation(models.Model): _inherit = 'stock.location' @api.model def _name_search(self, name='', domain=None, operator='ilike', limit=None, order=None): - """Override to restrict locations based on picking_type_id in context""" - _logger.info(f"LOCATION SEARCH CALLED: name={name}, context keys={list(self.env.context.keys())}, domain={domain}") + """Override to restrict locations based on context""" + _logger.info(f"LOCATION SEARCH: name={name}, context={self.env.context}") 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) - picking_type_id = self.env.context.get('default_picking_type_id') - - 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") + if allowed_location_ids: + domain = Domain.AND([domain, [('id', 'in', allowed_location_ids)]]) + _logger.info(f"LOCATION SEARCH: Filtered to {allowed_location_ids}") return super()._name_search(name=name, domain=domain, operator=operator, limit=limit, order=order) diff --git a/views/stock_move_line_views.xml b/views/stock_move_line_views.xml index 0b34c05..058d53b 100644 --- a/views/stock_move_line_views.xml +++ b/views/stock_move_line_views.xml @@ -21,7 +21,7 @@ - {'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} + {'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} @@ -35,11 +35,12 @@ - [('product_id', '=', product_id), ('location_id', 'in', allowed_source_location_ids)] - {'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_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} - [('id', 'in', allowed_source_location_ids)] + + {'no_create': True} @@ -50,14 +51,32 @@ - + - [('product_id', '=', product_id), ('location_id', 'in', allowed_source_location_ids)] - {'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_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} - [('id', 'in', allowed_source_location_ids)] + + {'no_create': True} + + + + + + stock.move.line.form.restrict + stock.move.line + + + + + + + + + + {'no_create': True}