first commit

This commit is contained in:
Suherdy Yacob 2026-02-13 14:36:29 +07:00
commit f246f885a8
11 changed files with 219 additions and 0 deletions

7
.gitignore vendored Normal file
View File

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

12
README.md Normal file
View File

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

1
__init__.py Normal file
View File

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

21
__manifest__.py Normal file
View File

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

5
models/__init__.py Normal file
View File

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

15
models/mrp_production.py Normal file
View File

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

31
models/stock_move.py Normal file
View File

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

50
models/stock_move_line.py Normal file
View File

@ -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]}")

15
models/stock_picking.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_picking_type_form_inherit_m2m" model="ir.ui.view">
<field name="name">stock.picking.type.form.inherit.m2m</field>
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.view_picking_type_form"/>
<field name="arch" type="xml">
<xpath expr="//label[@name='default_location_src_id_label']" position="attributes">
<attribute name="for">default_location_src_ids</attribute>
</xpath>
<xpath expr="//field[@name='default_location_src_id']" position="attributes">
<attribute name="invisible">1</attribute>
<attribute name="required">0</attribute>
</xpath>
<xpath expr="//field[@name='default_location_src_id']" position="after">
<field name="default_location_src_ids" widget="many2many_tags" options="{'no_create': True}" required="1"/>
</xpath>
</field>
</record>
</odoo>