first commmit

This commit is contained in:
Suherdy Yacob 2026-02-13 14:37:52 +07:00
commit 69ec589db0
9 changed files with 237 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*.pyc
*.pyo
*~
__pycache__/
.vscode/
.idea/
*.swp

19
README.md Normal file
View File

@ -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.

1
__init__.py Normal file
View File

@ -0,0 +1 @@
from . import models

23
__manifest__.py Normal file
View File

@ -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',
}

1
models/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import stock_location

74
models/stock_location.py Normal file
View File

@ -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)

View File

@ -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);
}
});

View File

@ -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 });
}
});

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_move_operations_restrict" model="ir.ui.view">
<field name="name">stock.move.operations.form.restrict</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_stock_move_operations"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='move_line_ids']" position="before">
<field name="allowed_source_location_ids" invisible="1"/>
</xpath>
</field>
</record>
<record id="view_mrp_stock_move_operations_restrict" model="ir.ui.view">
<field name="name">stock.move.mrp.operations.form.restrict</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="mrp.view_mrp_stock_move_operations"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='allowed_source_location_ids']" position="replace"/>
<xpath expr="//field[@name='move_line_ids']" position="before">
<field name="allowed_source_location_ids" invisible="1"/>
</xpath>
<xpath expr="//field[@name='move_line_ids']" position="attributes">
<attribute name="context">{'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}</attribute>
</xpath>
</field>
</record>
<record id="view_stock_move_line_detailed_operation_tree_restrict" model="ir.ui.view">
<field name="name">stock.move.line.detailed.operation.tree.restrict</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_stock_move_line_detailed_operation_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='picking_id']" position="after">
<field name="allowed_source_location_ids" invisible="1"/>
</xpath>
<xpath expr="//field[@name='quant_id']" position="attributes">
<attribute name="domain">[('product_id', '=', product_id), ('location_id', 'in', allowed_source_location_ids)]</attribute>
<attribute name="context">{'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')}</attribute>
</xpath>
<xpath expr="//field[@name='location_id']" position="attributes">
<attribute name="domain">[('id', 'in', allowed_source_location_ids)]</attribute>
</xpath>
</field>
</record>
<record id="view_stock_move_line_operation_tree_restrict" model="ir.ui.view">
<field name="name">stock.move.line.operation.tree.restrict</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_stock_move_line_operation_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='move_id']" position="after">
<field name="allowed_source_location_ids" invisible="1"/>
</xpath>
<xpath expr="//field[@name='quant_id']" position="attributes">
<attribute name="domain">[('product_id', '=', product_id), ('location_id', 'in', allowed_source_location_ids)]</attribute>
<attribute name="context">{'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')}</attribute>
</xpath>
<xpath expr="//field[@name='location_id']" position="attributes">
<attribute name="domain">[('id', 'in', allowed_source_location_ids)]</attribute>
</xpath>
</field>
</record>
</odoo>