import logging import inspect from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.osv import expression _logger = logging.getLogger(__name__) class StockQuant(models.Model): _inherit = 'stock.quant' @api.model_create_multi def create(self, vals_list): for vals in vals_list: if not vals.get('product_id'): # Capture the calling stack to see who is trying to create a product-less quant stack = "\n".join([str(s.filename.split('/')[-1]) + " line " + str(s.lineno) for s in inspect.stack()[:10]]) _logger.error(f"DEBUG_RESTRICT: STOCK.QUANT CREATE with NO product_id! STACK:\n{stack}") # Force visibility in browser raise UserError(_( "DEBUG_TRACEBACK: Detected STOCK.QUANT creation with no product_id.\n\n" "Top calling files:\n%s" ) % stack) return super().create(vals_list) def write(self, vals): if 'product_id' in vals and not vals.get('product_id'): stack = "\n".join([str(s.filename.split('/')[-1]) + " line " + str(s.lineno) for s in inspect.stack()[:10]]) _logger.error(f"DEBUG_RESTRICT: STOCK.QUANT WRITE with NO product_id! STACK:\n{stack}") raise UserError(_( "DEBUG_TRACEBACK: Detected STOCK.QUANT write with no product_id.\n\n" "Top calling files:\n%s" ) % stack) return super().write(vals) class StockLocation(models.Model): _inherit = 'stock.location' @api.model def get_allowed_locations_for_mo(self, mo_id=None, picking_type_id=None): """ Public helper for JS to fetch allowed locations. """ allowed_ids = [] source_name = "None" # 1. MO Lookup (Direct Database Query) if mo_id: 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 source_name = f"MO {mo.name}" # 2. Picking Type Fallback if not allowed_ids and picking_type_id: pt = self.env['stock.picking.type'].sudo().browse(picking_type_id) if pt.exists(): if pt.default_location_src_ids: allowed_ids = pt.default_location_src_ids.ids source_name = f"PT {pt.display_name}" elif pt.default_location_src_id: allowed_ids = [pt.default_location_src_id.id] source_name = f"PT {pt.display_name} (M21)" if allowed_ids: _logger.error(f"DEBUG_RESTRICT: Identified {len(allowed_ids)} Allowed Locations for {source_name}: {allowed_ids}") return allowed_ids return [] class StockLot(models.Model): _inherit = 'stock.lot' @api.model 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. ctx = self.env.context if not ctx.get('skip_location_restriction') and ctx.get('uid'): mo_id = (ctx.get('active_mo_id') or ctx.get('default_production_id') or ctx.get('production_id')) allowed_ids = self.env['stock.location'].sudo().get_allowed_locations_for_mo(mo_id=mo_id, picking_type_id=ctx.get('default_picking_type_id')) if allowed_ids: quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)] if ctx.get('default_product_id'): quant_domain.append(('product_id', '=', ctx.get('default_product_id'))) quants = self.env['stock.quant'].with_context(skip_location_restriction=True).sudo().search(quant_domain) args = expression.AND([args or [], [('id', 'in', quants.mapped('lot_id').ids)]]) return super().name_search(name, args=args, operator=operator, limit=limit) @api.model def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None): ctx = self.env.context if not ctx.get('skip_location_restriction') and ctx.get('uid'): mo_id = (ctx.get('active_mo_id') or ctx.get('default_production_id') or ctx.get('production_id')) allowed_ids = self.env['stock.location'].sudo().get_allowed_locations_for_mo(mo_id=mo_id, picking_type_id=ctx.get('default_picking_type_id')) if allowed_ids: quant_domain = [('location_id', 'child_of', allowed_ids), ('quantity', '>', 0), ('lot_id', '!=', False)] if ctx.get('default_product_id'): quant_domain.append(('product_id', '=', ctx.get('default_product_id'))) 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)