refactor: restrict location filtering to UI-only searches and remove backend reservation overrides to prevent validation errors.
This commit is contained in:
parent
c036b7cb9c
commit
e341adf41e
28
README.md
28
README.md
@ -1,19 +1,29 @@
|
|||||||
# Stock Restrict Source Location
|
# Stock Restrict Source Location
|
||||||
|
|
||||||
This module implements strict location filtering for Stock moves and Manufacturing Orders based on the configuration set in the Operation Type.
|
This module implements targeted location filtering for Stock Moves and Manufacturing Orders, ensuring users can only pick components and stock from authorized source locations defined in the Operation Type.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- **UI-Level Restriction**: Restricts the "Pick From" location selection in both standard Transfers and MO "Detailed Operations" dialogs.
|
|
||||||
- **Background Reservation Restriction**: Overrides the reservation engine (`_get_gather_domain`) to ensure auto-pick (Check Availability) only pulls stock from authorized locations.
|
- **Surgical UI Filtering**: Restricts the "Pick From" (Quant) and "Lot/Serial Number" (Lot) selection in both standard Transfers and MO "Detailed Operations" dialogs.
|
||||||
- **MO Support**: Specifically handles Manufacturing Orders by injecting the `active_mo_id` and `active_move_id` into the context.
|
- **Backend Sync Safety**: Specifically designed to bypass restrictions for background Odoo operations (reservation, unreservation, record-linking) to prevent "Missing Product" validation errors.
|
||||||
- **Frontend Patch**: Includes a JavaScript patch (`stock_move_line_x2_many_field_patch.js`) to ensure picking type context is maintained during frontend searches.
|
- **Context-Aware MO Support**: Handles the complex Manufacturing Order "Components" view by injecting the correct picking type and source location into the search context.
|
||||||
|
- **Frontend Patch**: Includes a JavaScript patch to ensure location context is correctly passed from the UI to the backend search methods.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- `stock`
|
- `stock`
|
||||||
- `mrp`
|
- `mrp`
|
||||||
- `stock_picking_type_m2m` (Used to retrieve the list of allowed locations)
|
- `stock_picking_type_m2m` (Used to configure the list of allowed locations on the Operation Type)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
1. Configure **Allowed Source Locations** on your Operation Type (e.g., Manufacturing).
|
|
||||||
2. Create an MO or Transfer using that Operation Type.
|
1. **Configure**: On a **Picking Type** (Operation Type), set the **Allowed Source Locations** (M2M field).
|
||||||
3. Both manual selection and the "Check Availability" button will now be restricted to precisely the locations you defined.
|
2. **Operation**: Create an MO or Transfer using that Picking Type.
|
||||||
|
3. **Lot Selection**: When a user clicks to select a Lot or a Quant in the UI, the list will be filtered to only show availability from the allowed locations.
|
||||||
|
4. **Backend**: Odoo's internal "Check Availability" and "Save" processes will still function normally, ensuring that reservations are linked correctly to existing stock.
|
||||||
|
|
||||||
|
## Technical Implementation (V19)
|
||||||
|
|
||||||
|
- Overrides `StockQuant` and `StockLot` `_search` methods with a **UI-only bypass**.
|
||||||
|
- Only restricts queries that include UI indicators (like `params`, `search_view_ref`, or `bin_size`).
|
||||||
|
- Bypasses restrictions for the system user (`uid=False`) and searches by direct ID to maintain ORM integrity.
|
||||||
|
|||||||
@ -84,64 +84,46 @@ class StockQuant(models.Model):
|
|||||||
|
|
||||||
return allowed_location_ids
|
return allowed_location_ids
|
||||||
|
|
||||||
@api.model_create_multi
|
def _search(self, domain, offset=0, limit=None, order=None, *args, **kwargs):
|
||||||
def create(self, vals_list):
|
"""Override to apply location restrictions during search (e.g. catalog or list selection)"""
|
||||||
"""Diagnostic override to catch the 'Missing Product' error root cause"""
|
|
||||||
for vals in vals_list:
|
|
||||||
if not vals.get('product_id'):
|
|
||||||
# LOG THE CALLER for debugging
|
|
||||||
_logger.error("DEBUG_RESTRICT: Creating StockQuant WITHOUT product_id!")
|
|
||||||
_logger.error(f"DEBUG_RESTRICT: Vals: {vals}")
|
|
||||||
_logger.error(f"DEBUG_RESTRICT: Context: {self.env.context}")
|
|
||||||
# We do NOT raise here to avoid breaking Odoo's original error UX,
|
|
||||||
# but this will appear in logs or help us see the issue.
|
|
||||||
return super().create(vals_list)
|
|
||||||
|
|
||||||
@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)"""
|
|
||||||
result_domain = super()._get_gather_domain(product_id, location_id, lot_id, package_id, owner_id, strict)
|
|
||||||
|
|
||||||
ctx = self.env.context
|
ctx = self.env.context
|
||||||
# 0. Bypass if skip flag is set OR we are in a system operation
|
|
||||||
if ctx.get('skip_location_restriction') or not ctx.get('uid'):
|
|
||||||
return result_domain
|
|
||||||
|
|
||||||
# 1. FIX: If reserving from a non-internal location, DO NOT apply internal restrictions.
|
# 0. CRITICAL BYPASS: Only restrict if it's a UI Search (params, bin_size, etc.)
|
||||||
if location_id and location_id.usage != 'internal':
|
# AND we are not searching for specific record IDs (backend sync).
|
||||||
return result_domain
|
search_by_id = any(isinstance(leaf, (list, tuple)) and leaf[0] == 'id' for leaf in domain)
|
||||||
|
is_ui_search = (ctx.get('params') or
|
||||||
|
ctx.get('bin_size') or
|
||||||
|
ctx.get('search_view_ref') or
|
||||||
|
ctx.get('list_view_ref'))
|
||||||
|
|
||||||
|
if (search_by_id or
|
||||||
|
ctx.get('skip_location_restriction') or
|
||||||
|
not is_ui_search or
|
||||||
|
not ctx.get('uid')):
|
||||||
|
return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs)
|
||||||
|
|
||||||
allowed_location_ids = self._get_allowed_locations()
|
allowed_location_ids = self._get_allowed_locations()
|
||||||
if allowed_location_ids:
|
if allowed_location_ids:
|
||||||
# 2. Smart Domain Swap: find if there's already a location_id restriction (like child_of)
|
# 1. Smart Domain Swap: find if there's already a location_id restriction (like child_of)
|
||||||
found_collision = False
|
found_collision = False
|
||||||
new_domain = []
|
new_domain = []
|
||||||
for leaf in result_domain:
|
for leaf in domain:
|
||||||
if (isinstance(leaf, (list, tuple)) and
|
if (isinstance(leaf, (list, tuple)) and
|
||||||
len(leaf) == 3 and
|
len(leaf) == 3 and
|
||||||
leaf[0] == 'location_id' and
|
leaf[0] == 'location_id' and
|
||||||
leaf[1] in ('child_of', '=', 'in')):
|
leaf[1] == 'child_of'):
|
||||||
|
|
||||||
new_domain.append(('location_id', 'in', allowed_location_ids))
|
new_domain.append(('location_id', 'in', allowed_location_ids))
|
||||||
found_collision = True
|
found_collision = True
|
||||||
else:
|
else:
|
||||||
new_domain.append(leaf)
|
new_domain.append(leaf)
|
||||||
|
|
||||||
if found_collision:
|
if found_collision:
|
||||||
result_domain = new_domain
|
domain = new_domain
|
||||||
else:
|
else:
|
||||||
result_domain = Domain.AND([result_domain, [('location_id', 'in', allowed_location_ids)]])
|
domain = Domain.AND([domain, [('location_id', 'in', allowed_location_ids)]])
|
||||||
|
|
||||||
return result_domain
|
return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs)
|
||||||
|
|
||||||
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)"""
|
|
||||||
ctx = self.env.context
|
|
||||||
# 0. CRITICAL FIX: If searching for specific IDs OR skip flag set OR NOT UI Search, bypass custom filtering
|
|
||||||
search_by_id = any(isinstance(leaf, (list, tuple)) and leaf[0] == 'id' for leaf in domain)
|
|
||||||
is_ui_search = ctx.get('params') or ctx.get('search_view_ref') or ctx.get('list_view_ref')
|
|
||||||
|
|
||||||
if search_by_id or ctx.get('skip_location_restriction') or not is_ui_search:
|
|
||||||
return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs)
|
|
||||||
|
|
||||||
allowed_location_ids = self._get_allowed_locations()
|
allowed_location_ids = self._get_allowed_locations()
|
||||||
if allowed_location_ids:
|
if allowed_location_ids:
|
||||||
@ -191,10 +173,16 @@ class StockLot(models.Model):
|
|||||||
|
|
||||||
# 0. NEW: Detect UI Search vs Backend Sync
|
# 0. NEW: Detect UI Search vs Backend Sync
|
||||||
search_by_id = any(isinstance(leaf, (list, tuple)) and leaf[0] == 'id' for leaf in domain)
|
search_by_id = any(isinstance(leaf, (list, tuple)) and leaf[0] == 'id' for leaf in domain)
|
||||||
is_ui_search = ctx.get('params') or ctx.get('search_view_ref') or ctx.get('list_view_ref')
|
is_ui_search = (ctx.get('params') or
|
||||||
|
ctx.get('bin_size') or
|
||||||
|
ctx.get('search_view_ref') or
|
||||||
|
ctx.get('list_view_ref'))
|
||||||
|
|
||||||
# FIX: If we are searching for specific IDs OR skip flag set OR NOT UI search, bypass
|
# FIX: If we are searching for specific IDs OR skip flag set OR NOT UI search, bypass
|
||||||
if search_by_id or ctx.get('skip_location_restriction') or not is_ui_search:
|
if (search_by_id or
|
||||||
|
ctx.get('skip_location_restriction') or
|
||||||
|
not is_ui_search or
|
||||||
|
not ctx.get('uid')):
|
||||||
return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs)
|
return super()._search(domain, offset=offset, limit=limit, order=order, *args, **kwargs)
|
||||||
|
|
||||||
# 1. Identify which locations we should look into for quants
|
# 1. Identify which locations we should look into for quants
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user