From a530b56dcbaf3bdef6bafcb7c0a1b08c0017209d Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Wed, 11 Feb 2026 11:56:58 +0700 Subject: [PATCH] first commit --- .gitignore | 4 ++ README.md | 20 ++++++++ __init__.py | 1 + __manifest__.py | 19 +++++++ models/__init__.py | 3 ++ models/mrp_bom.py | 10 ++++ models/mrp_production.py | 61 ++++++++++++++++++++++ models/mrp_routing_workcenter.py | 22 ++++++++ tests/__init__.py | 1 + tests/test_shared_ops.py | 71 ++++++++++++++++++++++++++ views/mrp_bom_views.xml | 23 +++++++++ views/mrp_routing_workcenter_views.xml | 16 ++++++ 12 files changed, 251 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 models/__init__.py create mode 100644 models/mrp_bom.py create mode 100644 models/mrp_production.py create mode 100644 models/mrp_routing_workcenter.py create mode 100644 tests/__init__.py create mode 100644 tests/test_shared_ops.py create mode 100644 views/mrp_bom_views.xml create mode 100644 views/mrp_routing_workcenter_views.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9d950d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +__pycache__ +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9e2453 --- /dev/null +++ b/README.md @@ -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. 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..e28b6ae --- /dev/null +++ b/__manifest__.py @@ -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', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e8f4cf5 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,3 @@ +from . import mrp_routing_workcenter +from . import mrp_bom +from . import mrp_production diff --git a/models/mrp_bom.py b/models/mrp_bom.py new file mode 100644 index 0000000..fc80507 --- /dev/null +++ b/models/mrp_bom.py @@ -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.' + ) diff --git a/models/mrp_production.py b/models/mrp_production.py new file mode 100644 index 0000000..2e0b880 --- /dev/null +++ b/models/mrp_production.py @@ -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] + diff --git a/models/mrp_routing_workcenter.py b/models/mrp_routing_workcenter.py new file mode 100644 index 0000000..b8fc83a --- /dev/null +++ b/models/mrp_routing_workcenter.py @@ -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] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c0fb669 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +from . import test_shared_ops diff --git a/tests/test_shared_ops.py b/tests/test_shared_ops.py new file mode 100644 index 0000000..7c18c30 --- /dev/null +++ b/tests/test_shared_ops.py @@ -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") + diff --git a/views/mrp_bom_views.xml b/views/mrp_bom_views.xml new file mode 100644 index 0000000..38da455 --- /dev/null +++ b/views/mrp_bom_views.xml @@ -0,0 +1,23 @@ + + + + mrp.bom.form.inherit.shared + mrp.bom + + + + + + + + + + + + + + + + + + diff --git a/views/mrp_routing_workcenter_views.xml b/views/mrp_routing_workcenter_views.xml new file mode 100644 index 0000000..60453b2 --- /dev/null +++ b/views/mrp_routing_workcenter_views.xml @@ -0,0 +1,16 @@ + + + + mrp.routing.workcenter.form.inherit.shared + mrp.routing.workcenter + + + + 0 + + + + + + +