From 69ec589db0d39e447553d6a46c3d3e1c025c2414 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 13 Feb 2026 14:37:52 +0700 Subject: [PATCH] first commmit --- .gitignore | 7 ++ README.md | 19 +++++ __init__.py | 1 + __manifest__.py | 23 ++++++ models/__init__.py | 1 + models/stock_location.py | 74 +++++++++++++++++++ .../src/js/stock_move_line_list_controller.js | 13 ++++ .../js/stock_move_line_x2_many_field_patch.js | 35 +++++++++ views/stock_move_line_views.xml | 64 ++++++++++++++++ 9 files changed, 237 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 models/__init__.py create mode 100644 models/stock_location.py create mode 100644 static/src/js/stock_move_line_list_controller.js create mode 100644 static/src/js/stock_move_line_x2_many_field_patch.js create mode 100644 views/stock_move_line_views.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4885be8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +*.pyo +*~ +__pycache__/ +.vscode/ +.idea/ +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..5429e40 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# 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. + +## 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. + +## Dependencies +- `stock` +- `mrp` +- `stock_picking_type_m2m` (Used to retrieve the list of allowed locations) + +## 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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..05386f5 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,23 @@ +{ + 'name': 'Restrict Stock Pick Source Location', + 'version': '19.0.1.0.0', + 'summary': 'Restrict stock picking to exact source location', + 'description': """ + This module restricts the selection of source location/quant in stock moves to strictly match + the source location of the picking, preventing selection from child locations. + """, + 'author': 'Antigravity', + 'category': 'Inventory/Inventory', + 'depends': ['stock', 'stock_picking_type_m2m', 'mrp'], + 'data': [ + 'views/stock_move_line_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'stock_restrict_source_location/static/src/js/stock_move_line_x2_many_field_patch.js', + ], + }, + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..88493e3 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1 @@ +from . import stock_location diff --git a/models/stock_location.py b/models/stock_location.py new file mode 100644 index 0000000..e264531 --- /dev/null +++ b/models/stock_location.py @@ -0,0 +1,74 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.fields import Domain +import logging + +_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' + + @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 + + 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 + + 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 + +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}") + domain = domain or [] + + # 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") + + return super()._name_search(name=name, domain=domain, operator=operator, limit=limit, order=order) + +_logger.info("="*80) +_logger.info("STOCK_RESTRICT_SOURCE_LOCATION: stock_location.py module loaded successfully!") +_logger.info("="*80) diff --git a/static/src/js/stock_move_line_list_controller.js b/static/src/js/stock_move_line_list_controller.js new file mode 100644 index 0000000..e1c15bd --- /dev/null +++ b/static/src/js/stock_move_line_list_controller.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { StockMoveLineListController } from "@stock/views/stock_move_line_list/stock_move_line_list_controller"; + +patch(StockMoveLineListController.prototype, { + async openRecord(record) { + // Intercept opening of the record to inject context if needed + // This is a placeholder as we might need to patch the action regarding the "Add a line" button + // which usually triggers a list view editable. + return super.openRecord(record); + } +}); diff --git a/static/src/js/stock_move_line_x2_many_field_patch.js b/static/src/js/stock_move_line_x2_many_field_patch.js new file mode 100644 index 0000000..4b2cfa0 --- /dev/null +++ b/static/src/js/stock_move_line_x2_many_field_patch.js @@ -0,0 +1,35 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { SMLX2ManyField } from "@stock/fields/stock_move_line_x2_many_field"; + +patch(SMLX2ManyField.prototype, { + async onAdd({ context, editable } = {}) { + // Inject active_mo_id into context + if (this.props.context.active_mo_id) { + context = { + ...context, + active_mo_id: this.props.context.active_mo_id, + }; + } else if (this.props.record.model.config.resModel === "stock.move" && this.props.record.data.raw_material_production_id) { + // Fallback: try to get MO ID from the move record if available (though raw_material_production_id might be a relation) + // The most reliable way is if it was passed in props.context which we did in XML + if (this.props.record.data.raw_material_production_id[0]) { + context = { + ...context, + active_mo_id: this.props.record.data.raw_material_production_id[0], + }; + } + } + + // Also inject picking_type_id if available in props.context + if (this.props.context.default_picking_type_id) { + context = { + ...context, + default_picking_type_id: this.props.context.default_picking_type_id, + }; + } + + return super.onAdd({ context, editable }); + } +}); diff --git a/views/stock_move_line_views.xml b/views/stock_move_line_views.xml new file mode 100644 index 0000000..0b34c05 --- /dev/null +++ b/views/stock_move_line_views.xml @@ -0,0 +1,64 @@ + + + + stock.move.operations.form.restrict + stock.move + + + + + + + + + + stock.move.mrp.operations.form.restrict + stock.move + + + + + + + + {'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} + + + + + + stock.move.line.detailed.operation.tree.restrict + stock.move.line + + + + + + + [('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')} + + + [('id', 'in', allowed_source_location_ids)] + + + + + + stock.move.line.operation.tree.restrict + stock.move.line + + + + + + + [('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')} + + + [('id', 'in', allowed_source_location_ids)] + + + +