first commit

This commit is contained in:
Suherdy Yacob 2026-02-11 11:56:58 +07:00
commit a530b56dcb
12 changed files with 251 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.pyc
__pycache__
*.log
.DS_Store

20
README.md Normal file
View 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
View File

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

19
__manifest__.py Normal file
View 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
View 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
View 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
View 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]

View 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
View File

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

71
tests/test_shared_ops.py Normal file
View 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
View 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>

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