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
|
||||
|
||||
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
|
||||
- **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.
|
||||
- **MO Support**: Specifically handles Manufacturing Orders by injecting the `active_mo_id` and `active_move_id` into the context.
|
||||
- **Frontend Patch**: Includes a JavaScript patch (`stock_move_line_x2_many_field_patch.js`) to ensure picking type context is maintained during frontend searches.
|
||||
|
||||
- **Surgical UI Filtering**: Restricts the "Pick From" (Quant) and "Lot/Serial Number" (Lot) selection in both standard Transfers and MO "Detailed Operations" dialogs.
|
||||
- **Backend Sync Safety**: Specifically designed to bypass restrictions for background Odoo operations (reservation, unreservation, record-linking) to prevent "Missing Product" validation errors.
|
||||
- **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
|
||||
|
||||
- `stock`
|
||||
- `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
|
||||
1. Configure **Allowed Source Locations** on your Operation Type (e.g., Manufacturing).
|
||||
2. Create an MO or Transfer using that Operation Type.
|
||||
3. Both manual selection and the "Check Availability" button will now be restricted to precisely the locations you defined.
|
||||
|
||||
1. **Configure**: On a **Picking Type** (Operation Type), set the **Allowed Source Locations** (M2M field).
|
||||
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,63 +84,45 @@ class StockQuant(models.Model):
|
||||
|
||||
return allowed_location_ids
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""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)
|
||||
|
||||
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. 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.
|
||||
if location_id and location_id.usage != 'internal':
|
||||
return result_domain
|
||||
# 0. CRITICAL BYPASS: Only restrict if it's a UI Search (params, bin_size, etc.)
|
||||
# AND we are not searching for specific record IDs (backend sync).
|
||||
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()
|
||||
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
|
||||
new_domain = []
|
||||
for leaf in result_domain:
|
||||
for leaf in domain:
|
||||
if (isinstance(leaf, (list, tuple)) and
|
||||
len(leaf) == 3 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))
|
||||
found_collision = True
|
||||
else:
|
||||
new_domain.append(leaf)
|
||||
|
||||
if found_collision:
|
||||
result_domain = new_domain
|
||||
domain = new_domain
|
||||
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
|
||||
|
||||
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()
|
||||
@ -191,10 +173,16 @@ class StockLot(models.Model):
|
||||
|
||||
# 0. NEW: Detect UI Search vs Backend Sync
|
||||
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
|
||||
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)
|
||||
|
||||
# 1. Identify which locations we should look into for quants
|
||||
|
||||
Loading…
Reference in New Issue
Block a user