first commit
This commit is contained in:
commit
a530b56dcb
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
*.log
|
||||
.DS_Store
|
||||
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Shared MRP Operations
|
||||
|
||||
This module allows you to define Manufacturing Operations (Work Center Usages) that can be shared across multiple Bills of Materials (BOMs).
|
||||
|
||||
## Features
|
||||
|
||||
- **Shared Operations**: Define a single operation and link it to multiple BOMs.
|
||||
- **Optional BOM Link**: Operations no longer require a single specific BOM link.
|
||||
- **Company Consistency**: Ensures shared operations respect company assignments.
|
||||
- **Automatic Work Orders**: Manufacturing Orders automatically generate Work Orders for both specific and shared operations.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Go to **Manufacturing** > **Configuration** > **Operations**.
|
||||
2. Create a new Operation.
|
||||
3. Leave the **Bill of Material** field empty.
|
||||
4. In the **Applies to BOMs** field, select all the BOMs that should include this operation.
|
||||
5. Save.
|
||||
|
||||
When you create a Manufacturing Order for any of the selected BOMs, this shared operation will be included as a Work Order.
|
||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
19
__manifest__.py
Normal file
19
__manifest__.py
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
'name': 'Shared MRP Operations',
|
||||
'version': '1.0',
|
||||
'category': 'Manufacturing/Manufacturing',
|
||||
'summary': 'Allow MRP Operations to be assigned to multiple BOMs',
|
||||
'description': """
|
||||
This module allows defining MRP Operations (Work Center Usages) that can be shared across multiple Bills of Materials.
|
||||
It adds a Many2many relationship between Operations and BOMs, and updates the Manufacturing Order generation logic
|
||||
to include these shared operations.
|
||||
""",
|
||||
'depends': ['mrp'],
|
||||
'data': [
|
||||
'views/mrp_routing_workcenter_views.xml',
|
||||
'views/mrp_bom_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
3
models/__init__.py
Normal file
3
models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import mrp_routing_workcenter
|
||||
from . import mrp_bom
|
||||
from . import mrp_production
|
||||
10
models/mrp_bom.py
Normal file
10
models/mrp_bom.py
Normal file
@ -0,0 +1,10 @@
|
||||
from odoo import fields, models
|
||||
|
||||
class MrpBom(models.Model):
|
||||
_inherit = 'mrp.bom'
|
||||
|
||||
shared_operation_ids = fields.Many2many(
|
||||
'mrp.routing.workcenter',
|
||||
string='Shared Operations',
|
||||
help='Operations shared across multiple BOMs.'
|
||||
)
|
||||
61
models/mrp_production.py
Normal file
61
models/mrp_production.py
Normal file
@ -0,0 +1,61 @@
|
||||
from odoo import fields, models, api, Command
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
|
||||
@api.depends('bom_id', 'product_id', 'product_qty', 'product_uom_id', 'never_product_template_attribute_value_ids')
|
||||
def _compute_workorder_ids(self):
|
||||
"""
|
||||
Override to include shared operations from bom_id.shared_operation_ids.
|
||||
This reuses the original logic but injects the shared operations effectively into the BOM
|
||||
structure considered for work orders.
|
||||
"""
|
||||
super()._compute_workorder_ids()
|
||||
|
||||
for production in self:
|
||||
if production.state != 'draft' or not production.bom_id or not production.bom_id.shared_operation_ids:
|
||||
continue
|
||||
|
||||
# Identify shared operations that are not already in workorder_ids
|
||||
# The super method might not pick them up because they are not directly in bom.operation_ids if they are only in shared_operation_ids
|
||||
# However, we need to be careful. If shared_operation_ids are just a link, we need to treat them as if they are part of the BOM.
|
||||
|
||||
# Strategy:
|
||||
# 1. Inspect existing workorders.
|
||||
# 2. Iterate shared operations.
|
||||
# 3. Create workorders for missing shared operations.
|
||||
|
||||
workorders_values = []
|
||||
deleted_workorders_ids = [] # We probably don't want to delete what super created, only add.
|
||||
|
||||
# We need to respect the same filtering logic (skip if not applicable to variant)
|
||||
# The shared operations are mrp.routing.workcenter records.
|
||||
# They have 'bom_product_template_attribute_value_ids'.
|
||||
|
||||
product = production.product_id
|
||||
|
||||
for operation in production.bom_id.shared_operation_ids:
|
||||
if operation._skip_operation_line(product, production.never_product_template_attribute_value_ids):
|
||||
continue
|
||||
|
||||
# Check if this operation is already present in workorders
|
||||
# Note: The same operation record might be used in multiple BOMs.
|
||||
# If it's already there (e.g. from super if we modify BOM structure implicitly), we skip.
|
||||
# But here, shared_operation_ids are likely NOT in bom.operation_ids (One2many inverse of bom_id).
|
||||
|
||||
existing_wo = production.workorder_ids.filtered(lambda wo: wo.operation_id == operation)
|
||||
if existing_wo:
|
||||
continue
|
||||
|
||||
workorders_values += [{
|
||||
'name': operation.name,
|
||||
'production_id': production.id,
|
||||
'workcenter_id': operation.workcenter_id.id,
|
||||
'product_uom_id': production.product_uom_id.id,
|
||||
'operation_id': operation.id,
|
||||
'state': 'ready',
|
||||
}]
|
||||
|
||||
if workorders_values:
|
||||
production.workorder_ids = [Command.create(vals) for vals in workorders_values]
|
||||
|
||||
22
models/mrp_routing_workcenter.py
Normal file
22
models/mrp_routing_workcenter.py
Normal file
@ -0,0 +1,22 @@
|
||||
from odoo import fields, models, api
|
||||
|
||||
class MrpRoutingWorkcenter(models.Model):
|
||||
_inherit = 'mrp.routing.workcenter'
|
||||
|
||||
bom_id = fields.Many2one('mrp.bom', required=False)
|
||||
bom_ids = fields.Many2many('mrp.bom', string='Applies to BOMs', help='BOMs that use this operation.')
|
||||
company_id = fields.Many2one('res.company', 'Company', related=False, store=True, readonly=False, required=True, default=lambda self: self.env.company)
|
||||
|
||||
@api.onchange('bom_id')
|
||||
def _onchange_bom_id(self):
|
||||
if self.bom_id and self.bom_id.company_id:
|
||||
self.company_id = self.bom_id.company_id
|
||||
|
||||
@api.onchange('bom_ids')
|
||||
def _onchange_bom_ids(self):
|
||||
"""
|
||||
If bom_ids contains a single BOM and bom_id is empty, set bom_id.
|
||||
This helps maintain compatibility and ease of use.
|
||||
"""
|
||||
if len(self.bom_ids) == 1 and not self.bom_id:
|
||||
self.bom_id = self.bom_ids[0]
|
||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import test_shared_ops
|
||||
71
tests/test_shared_ops.py
Normal file
71
tests/test_shared_ops.py
Normal file
@ -0,0 +1,71 @@
|
||||
import logging
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@tagged('post_install')
|
||||
class TestSharedOperations(TransactionCase):
|
||||
|
||||
def test_shared_operations(self):
|
||||
# 1. Create a Work Center
|
||||
workcenter = self.env['mrp.workcenter'].create({
|
||||
'name': 'Test Work Center',
|
||||
'time_efficiency': 100,
|
||||
'costs_hour': 10,
|
||||
})
|
||||
|
||||
# 2. Create a Shared Operation linked to Test Work Center
|
||||
shared_op = self.env['mrp.routing.workcenter'].create({
|
||||
'name': 'Shared Packaging Operation',
|
||||
'workcenter_id': workcenter.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 60,
|
||||
'bom_id': False, # Important: No specific BOM initially
|
||||
})
|
||||
|
||||
# 3. Create Product A and BOM A
|
||||
product_a = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'consu',
|
||||
})
|
||||
bom_a = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': product_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'shared_operation_ids': [(4, shared_op.id)],
|
||||
})
|
||||
|
||||
# 4. Create Product B and BOM B
|
||||
product_b = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'consu',
|
||||
})
|
||||
bom_b = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': product_b.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'shared_operation_ids': [(4, shared_op.id)],
|
||||
})
|
||||
|
||||
# 5. Create MO for BOM A and verify Work Order
|
||||
mo_a = self.env['mrp.production'].create({
|
||||
'product_id': product_a.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_a.id,
|
||||
})
|
||||
mo_a.action_confirm()
|
||||
|
||||
self.assertEqual(len(mo_a.workorder_ids), 1, "MO A should have 1 work order")
|
||||
self.assertEqual(mo_a.workorder_ids.operation_id, shared_op, "MO A work order should be the shared operation")
|
||||
|
||||
# 6. Create MO for BOM B and verify Work Order
|
||||
mo_b = self.env['mrp.production'].create({
|
||||
'product_id': product_b.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_b.id,
|
||||
})
|
||||
mo_b.action_confirm()
|
||||
|
||||
self.assertEqual(len(mo_b.workorder_ids), 1, "MO B should have 1 work order")
|
||||
self.assertEqual(mo_b.workorder_ids.operation_id, shared_op, "MO B work order should be the shared operation")
|
||||
|
||||
print("Test Shared Operations Passed Successfully")
|
||||
|
||||
23
views/mrp_bom_views.xml
Normal file
23
views/mrp_bom_views.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mrp_bom_form_view_inherit_shared" model="ir.ui.view">
|
||||
<field name="name">mrp.bom.form.inherit.shared</field>
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='operations']" position="after">
|
||||
<page name="shared_operations" string="Shared Operations">
|
||||
<field name="shared_operation_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="time_mode" optional="hide"/>
|
||||
<field name="time_cycle" widget="float_time" string="Duration (minutes)"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
16
views/mrp_routing_workcenter_views.xml
Normal file
16
views/mrp_routing_workcenter_views.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mrp_routing_workcenter_form_view_inherit_shared" model="ir.ui.view">
|
||||
<field name="name">mrp.routing.workcenter.form.inherit.shared</field>
|
||||
<field name="model">mrp.routing.workcenter</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_routing_workcenter_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='bom_id']" position="attributes">
|
||||
<attribute name="required">0</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='bom_id']" position="after">
|
||||
<field name="bom_ids" widget="many2many_tags"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user