first commit
This commit is contained in:
commit
f246f885a8
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
__pycache__/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
12
README.md
Normal file
12
README.md
Normal 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
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
21
__manifest__.py
Normal file
21
__manifest__.py
Normal 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
5
models/__init__.py
Normal 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
15
models/mrp_production.py
Normal 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
31
models/stock_move.py
Normal 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
50
models/stock_move_line.py
Normal 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
15
models/stock_picking.py
Normal 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
|
||||
42
models/stock_picking_type.py
Normal file
42
models/stock_picking_type.py
Normal 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)]
|
||||
20
views/stock_picking_type_views.xml
Normal file
20
views/stock_picking_type_views.xml
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user