first commmit
This commit is contained in:
commit
69ec589db0
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
__pycache__/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
19
README.md
Normal file
19
README.md
Normal 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
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
23
__manifest__.py
Normal file
23
__manifest__.py
Normal 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
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import stock_location
|
||||
74
models/stock_location.py
Normal file
74
models/stock_location.py
Normal 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)
|
||||
13
static/src/js/stock_move_line_list_controller.js
Normal file
13
static/src/js/stock_move_line_list_controller.js
Normal 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);
|
||||
}
|
||||
});
|
||||
35
static/src/js/stock_move_line_x2_many_field_patch.js
Normal file
35
static/src/js/stock_move_line_x2_many_field_patch.js
Normal 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 });
|
||||
}
|
||||
});
|
||||
64
views/stock_move_line_views.xml
Normal file
64
views/stock_move_line_views.xml
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user