commit f246f885a805242c1337665f95e2ad896660fb90 Author: Suherdy Yacob Date: Fri Feb 13 14:36:29 2026 +0700 first commit 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..c6e771f --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Stock Picking Type M2M + +This module extends the standard Odoo 19 Inventory functionality to support multiple default source locations for Operation Types. + +## Features +- **Many2many Source Locations**: Adds `default_location_src_ids` to `stock.picking.type`. +- **Backward Compatibility**: Maintains the standard `default_location_src_id` Many2one field, automatically synchronizing it with the first selection in the Many2many list. +- **Auto-Pick Context Propagation**: Overrides `stock.move` to ensure that the reservation engine has access to the allowed source locations during auto-pick operations. + +## Technical Details +- **Inherited Models**: `stock.picking.type`, `stock.move`. +- **Field Synchronization**: A compute method ensures that `default_location_src_id` stays in sync with `default_location_src_ids` to prevent errors in standard Odoo code that expects a single value. 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..b3da296 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'Stock Picking Type Source Location M2M', + 'version': '1.0', + 'category': 'Inventory/Inventory', + 'summary': 'Change default source location in operation types to Many2many', + 'description': """ + This module modifies the stock.picking.type model to change the + default_location_src_id field from Many2one to Many2many. + It also ensures that stock transfers (stock.picking) correctly + initialize their source location from the first available location + in the operation type. + """, + 'author': 'Antigravity', + 'depends': ['stock', 'mrp'], + 'data': [ + 'views/stock_picking_type_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..dfd9855 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +from . import stock_picking_type +from . import stock_picking +from . import mrp_production +from . import stock_move_line +from . import stock_move diff --git a/models/mrp_production.py b/models/mrp_production.py new file mode 100644 index 0000000..3b9b4f7 --- /dev/null +++ b/models/mrp_production.py @@ -0,0 +1,15 @@ +from odoo import api, fields, models + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + allowed_source_location_ids = fields.Many2many( + 'stock.location', string='Allowed Source Locations', + compute='_compute_allowed_source_location_ids', + store=True, precompute=True + ) + + @api.depends('picking_type_id', 'picking_type_id.default_location_src_ids') + def _compute_allowed_source_location_ids(self): + for production in self: + production.allowed_source_location_ids = production.picking_type_id.default_location_src_ids diff --git a/models/stock_move.py b/models/stock_move.py new file mode 100644 index 0000000..a61224e --- /dev/null +++ b/models/stock_move.py @@ -0,0 +1,31 @@ +from odoo import api, fields, models +import logging + +class StockMove(models.Model): + _inherit = 'stock.move' + + allowed_source_location_ids = fields.Many2many( + 'stock.location', string='Allowed Source Locations', + compute='_compute_allowed_source_location_ids', + store=True, precompute=True, compute_sudo=True + ) + + @api.depends('picking_type_id', 'picking_type_id.default_location_src_ids', 'picking_id.allowed_source_location_ids', 'raw_material_production_id.allowed_source_location_ids', 'production_id.allowed_source_location_ids') + def _compute_allowed_source_location_ids(self): + for move in self: + if move.picking_id: + move.allowed_source_location_ids = move.picking_id.allowed_source_location_ids + elif move.raw_material_production_id: + move.allowed_source_location_ids = move.raw_material_production_id.allowed_source_location_ids + elif move.production_id: + move.allowed_source_location_ids = move.production_id.allowed_source_location_ids + else: + move.allowed_source_location_ids = move.picking_type_id.default_location_src_ids + def _update_reserved_quantity(self, need, location_id, lot_id=None, package_id=None, owner_id=None, strict=True): + """Inject active_move_id into context to preserve picking type during reservation engine calls""" + if self.picking_type_id: + self = self.with_context( + default_picking_type_id=self.picking_type_id.id, + active_move_id=self.id + ) + return super()._update_reserved_quantity(need, location_id, lot_id, package_id, owner_id, strict) diff --git a/models/stock_move_line.py b/models/stock_move_line.py new file mode 100644 index 0000000..0bd7a50 --- /dev/null +++ b/models/stock_move_line.py @@ -0,0 +1,50 @@ +from odoo import api, fields, models +import logging + +_logger = logging.getLogger(__name__) + +class StockMoveLine(models.Model): + _inherit = 'stock.move.line' + + picking_type_id = fields.Many2one( + 'stock.picking.type', 'Operation type', compute='_compute_picking_type_id', + store=True, precompute=True, compute_sudo=True) + + allowed_source_location_ids = fields.Many2many( + 'stock.location', string='Allowed Source Locations', + compute='_compute_allowed_source_location_ids', + store=True, precompute=True + ) + + @api.depends('picking_id', 'move_id.picking_type_id') + def _compute_picking_type_id(self): + for line in self: + if line.picking_id: + line.picking_type_id = line.picking_id.picking_type_id + _logger.info(f"LOCATION_RESTRICT: Line {line.id} picking_type_id from picking: {line.picking_type_id.name if line.picking_type_id else 'None'}") + elif line.move_id: + line.picking_type_id = line.move_id.picking_type_id + _logger.info(f"LOCATION_RESTRICT: Line {line.id} picking_type_id from move: {line.picking_type_id.name if line.picking_type_id else 'None'}") + else: + line.picking_type_id = False + _logger.info(f"LOCATION_RESTRICT: Line {line.id} NO picking_type_id") + + @api.depends('picking_type_id', 'picking_type_id.default_location_src_ids') + def _compute_allowed_source_location_ids(self): + for line in self: + line.allowed_source_location_ids = line.picking_type_id.default_location_src_ids + _logger.info(f"LOCATION_RESTRICT: Line {line.id} allowed locations: {[loc.complete_name for loc in line.allowed_source_location_ids]}") + + @api.onchange('move_id') + def _onchange_move_id_restrict(self): + """Populate allowed locations from move for virtual records""" + if self.move_id and self.move_id.picking_type_id: + self.allowed_source_location_ids = self.move_id.picking_type_id.default_location_src_ids + _logger.info(f"LOCATION_RESTRICT ONCHANGE: Setting allowed from move {self.move_id.id}: {[loc.complete_name for loc in self.allowed_source_location_ids]}") + + @api.onchange('picking_id') + def _onchange_picking_id_restrict(self): + """Populate allowed locations from picking for virtual records""" + if self.picking_id and self.picking_id.picking_type_id: + self.allowed_source_location_ids = self.picking_id.picking_type_id.default_location_src_ids + _logger.info(f"LOCATION_RESTRICT ONCHANGE: Setting allowed from picking {self.picking_id.id}: {[loc.complete_name for loc in self.allowed_source_location_ids]}") diff --git a/models/stock_picking.py b/models/stock_picking.py new file mode 100644 index 0000000..8b38afc --- /dev/null +++ b/models/stock_picking.py @@ -0,0 +1,15 @@ +from odoo import api, fields, models + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + allowed_source_location_ids = fields.Many2many( + 'stock.location', string='Allowed Source Locations', + compute='_compute_allowed_source_location_ids', + store=True, precompute=True + ) + + @api.depends('picking_type_id', 'picking_type_id.default_location_src_ids') + def _compute_allowed_source_location_ids(self): + for picking in self: + picking.allowed_source_location_ids = picking.picking_type_id.default_location_src_ids diff --git a/models/stock_picking_type.py b/models/stock_picking_type.py new file mode 100644 index 0000000..44b9ced --- /dev/null +++ b/models/stock_picking_type.py @@ -0,0 +1,42 @@ +from odoo import api, fields, models, _ +from odoo.fields import Command + +class StockPickingType(models.Model): + _inherit = 'stock.picking.type' + + # Redefining the field as Many2many + # Note: We use a different relation table name to avoid conflicts with Odoo's internal schema changes + default_location_src_id = fields.Many2one( + 'stock.location', 'Default Source Location', compute='_compute_default_location_src_id', + check_company=True, store=True, readonly=False, precompute=True, required=True, + help="The primary default source location used for initial header values.") + + default_location_src_ids = fields.Many2many( + 'stock.location', 'stock_picking_type_src_location_rel', + 'picking_type_id', 'location_id', + string='Allowed Source Locations', + help="All source locations allowed for this operation type. The first one will be used as the primary default.") + + @api.depends('code', 'default_location_src_ids') + def _compute_default_location_src_id(self): + for picking_type in self: + if picking_type.default_location_src_ids: + # Sync: Primary default is the first one in the Many2many list + picking_type.default_location_src_id = picking_type.default_location_src_ids[0] + continue + + if not picking_type.warehouse_id: + picking_type.default_location_src_id = False + continue + + stock_location = picking_type.warehouse_id.lot_stock_id + if picking_type.code == 'incoming': + picking_type.default_location_src_id = self.env.ref('stock.stock_location_suppliers').id + else: + picking_type.default_location_src_id = stock_location.id + + @api.onchange('default_location_src_id') + def _onchange_default_location_src_id(self): + """ Ensure the primary default is always included in the allowed list """ + if self.default_location_src_id and self.default_location_src_id not in self.default_location_src_ids: + self.default_location_src_ids = [Command.link(self.default_location_src_id.id)] diff --git a/views/stock_picking_type_views.xml b/views/stock_picking_type_views.xml new file mode 100644 index 0000000..744f003 --- /dev/null +++ b/views/stock_picking_type_views.xml @@ -0,0 +1,20 @@ + + + + stock.picking.type.form.inherit.m2m + stock.picking.type + + + + default_location_src_ids + + + 1 + 0 + + + + + + +