From 977abadeb83c0bca7123d3bab3616937414b0a06 Mon Sep 17 00:00:00 2001 From: aykhu-odoo Date: Mon, 18 May 2026 14:52:35 +0530 Subject: [PATCH] [ADD] sale_mrp_modular_type: add modular types to scale BOM quantities from sales order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some manufacturing cases, the quantity of components depends on values decided at the time of sale (like number of sections, length, area, etc.). Currently, BOMs are fixed, so handling this kind of requirement means either duplicating BOMs or manually adjusting quantities, which is not practical and leads to mistakes. This change introduces *modular types* to handle this more cleanly. Products can be configured with modular types (like sections, meters…), and BOM lines can be linked to those types. Instead of being fixed, those BOM quantities now act like base values. When such a product is added to a sales order, the user can enter values for each modular type. These values are then used when creating the manufacturing order, where component quantities are computed as a function of: - the base BOM quantity - the modular type multiplier defined on the sales order The goal is to avoid duplicating BOMs and remove the need for manual adjustments, while still keeping the flexibility to handle different configurations per order. This keeps the flow simple for users and makes sure the manufacturing data always matches what was defined during the sale. A dedicated modular type model is introduced and linked to products (m2m) to define available dimensions, while BOM lines reference a single modular type (m2o). This separation allows reusable configuration on the product side while keeping BOM logic simple and explicit. The computation is applied at MO generation time. The computation is applied at MO generation time. --- sale_mrp_modular_type/__init__.py | 2 + sale_mrp_modular_type/__manifest__.py | 15 ++++++ sale_mrp_modular_type/models/__init__.py | 6 +++ sale_mrp_modular_type/models/modular_type.py | 8 +++ sale_mrp_modular_type/models/mrp_bom.py | 17 +++++++ .../models/product_template.py | 7 +++ .../models/sale_order_line.py | 33 ++++++++++++ .../models/sale_order_line_modular_value.py | 12 +++++ sale_mrp_modular_type/models/stock_move.py | 37 ++++++++++++++ .../security/ir.model.access.csv | 5 ++ .../views/modular_type_views.xml | 26 ++++++++++ sale_mrp_modular_type/views/mrp_bom_views.xml | 15 ++++++ .../views/product_template_views.xml | 15 ++++++ .../views/sale_order_views.xml | 28 ++++++++++ sale_mrp_modular_type/wizard/__init__.py | 1 + .../wizard/modular_type_wizard.py | 51 +++++++++++++++++++ .../wizard/modular_type_wizard_views.xml | 23 +++++++++ 17 files changed, 301 insertions(+) create mode 100644 sale_mrp_modular_type/__init__.py create mode 100644 sale_mrp_modular_type/__manifest__.py create mode 100644 sale_mrp_modular_type/models/__init__.py create mode 100644 sale_mrp_modular_type/models/modular_type.py create mode 100644 sale_mrp_modular_type/models/mrp_bom.py create mode 100644 sale_mrp_modular_type/models/product_template.py create mode 100644 sale_mrp_modular_type/models/sale_order_line.py create mode 100644 sale_mrp_modular_type/models/sale_order_line_modular_value.py create mode 100644 sale_mrp_modular_type/models/stock_move.py create mode 100644 sale_mrp_modular_type/security/ir.model.access.csv create mode 100644 sale_mrp_modular_type/views/modular_type_views.xml create mode 100644 sale_mrp_modular_type/views/mrp_bom_views.xml create mode 100644 sale_mrp_modular_type/views/product_template_views.xml create mode 100644 sale_mrp_modular_type/views/sale_order_views.xml create mode 100644 sale_mrp_modular_type/wizard/__init__.py create mode 100644 sale_mrp_modular_type/wizard/modular_type_wizard.py create mode 100644 sale_mrp_modular_type/wizard/modular_type_wizard_views.xml diff --git a/sale_mrp_modular_type/__init__.py b/sale_mrp_modular_type/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/sale_mrp_modular_type/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sale_mrp_modular_type/__manifest__.py b/sale_mrp_modular_type/__manifest__.py new file mode 100644 index 00000000000..d25d9a2c63f --- /dev/null +++ b/sale_mrp_modular_type/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': 'Modular Type', + 'description': """This module adds modular type to Manufacturing orders""", + 'author': 'aykhu', + 'license': 'LGPL-3', + 'depends': ['sale_management', 'sale_mrp'], + 'data': [ + 'security/ir.model.access.csv', + 'wizard/modular_type_wizard_views.xml', + 'views/modular_type_views.xml', + 'views/product_template_views.xml', + 'views/mrp_bom_views.xml', + 'views/sale_order_views.xml', + ], +} diff --git a/sale_mrp_modular_type/models/__init__.py b/sale_mrp_modular_type/models/__init__.py new file mode 100644 index 00000000000..75e58d3b4b8 --- /dev/null +++ b/sale_mrp_modular_type/models/__init__.py @@ -0,0 +1,6 @@ +from . import modular_type +from . import product_template +from . import mrp_bom +from . import stock_move +from . import sale_order_line +from . import sale_order_line_modular_value diff --git a/sale_mrp_modular_type/models/modular_type.py b/sale_mrp_modular_type/models/modular_type.py new file mode 100644 index 00000000000..942076017c5 --- /dev/null +++ b/sale_mrp_modular_type/models/modular_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ModularType(models.Model): + _name = 'modular.type' + _description = 'Modular Types' + + name = fields.Char() diff --git a/sale_mrp_modular_type/models/mrp_bom.py b/sale_mrp_modular_type/models/mrp_bom.py new file mode 100644 index 00000000000..24cd2c83256 --- /dev/null +++ b/sale_mrp_modular_type/models/mrp_bom.py @@ -0,0 +1,17 @@ +from odoo import api, fields, models + + +class MrpBomLine(models.Model): + _inherit = 'mrp.bom.line' + + modular_type_id = fields.Many2one( + 'modular.type', domain="[('id', 'in', available_modular_type_ids)]" + ) + available_modular_type_ids = fields.Many2many( + 'modular.type', compute='_compute_available_modular_type_ids' + ) + + @api.depends('product_id') + def _compute_available_modular_type_ids(self): + for line in self: + line.available_modular_type_ids = line.parent_product_tmpl_id.modular_type_ids diff --git a/sale_mrp_modular_type/models/product_template.py b/sale_mrp_modular_type/models/product_template.py new file mode 100644 index 00000000000..761f25ec14c --- /dev/null +++ b/sale_mrp_modular_type/models/product_template.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + modular_type_ids = fields.Many2many('modular.type') diff --git a/sale_mrp_modular_type/models/sale_order_line.py b/sale_mrp_modular_type/models/sale_order_line.py new file mode 100644 index 00000000000..58ba57cdc26 --- /dev/null +++ b/sale_mrp_modular_type/models/sale_order_line.py @@ -0,0 +1,33 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + has_modular_type = fields.Boolean( + compute='_compute_has_modular_type', store=True + ) + modular_value_ids = fields.One2many( + 'sale.order.line.modular.value', 'order_line_id' + ) + + @api.depends('product_template_id', 'product_template_id.modular_type_ids') + def _compute_has_modular_type(self): + for line in self: + line.has_modular_type = bool( + line.product_template_id.modular_type_ids + ) + + def _set_default_modular_values(self): + vals_list = [] + for line in self: + existing_types = line.modular_value_ids.mapped('modular_type_id') + missing_types = line.product_template_id.modular_type_ids - existing_types + for modular_type in missing_types: + vals_list.append({ + 'order_line_id': line.id, + 'modular_type_id': modular_type.id, + 'value': 0.0, + }) + if vals_list: + self.env['sale.order.line.modular.value'].create(vals_list) diff --git a/sale_mrp_modular_type/models/sale_order_line_modular_value.py b/sale_mrp_modular_type/models/sale_order_line_modular_value.py new file mode 100644 index 00000000000..9233253a2d2 --- /dev/null +++ b/sale_mrp_modular_type/models/sale_order_line_modular_value.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class SaleOrderLineModularValue(models.Model): + _name = 'sale.order.line.modular.value' + _description = 'Sale Order Line Modular Value' + + order_line_id = fields.Many2one( + 'sale.order.line', required=True, ondelete='cascade' + ) + modular_type_id = fields.Many2one('modular.type', required=True) + value = fields.Float(help="Quantity multiplier") diff --git a/sale_mrp_modular_type/models/stock_move.py b/sale_mrp_modular_type/models/stock_move.py new file mode 100644 index 00000000000..2b11d555e39 --- /dev/null +++ b/sale_mrp_modular_type/models/stock_move.py @@ -0,0 +1,37 @@ +from odoo import api, fields, models + + +class StockMove(models.Model): + _inherit = 'stock.move' + + modular_type_id = fields.Many2one( + 'modular.type', compute="_compute_modular_type", store=True + ) + sale_order_line_modular_value_id = fields.Many2one('sale.order.line.modular.value') + base_bom_qty = fields.Float() + + @api.depends('production_id.bom_id.bom_line_ids') + def _compute_modular_type(self): + for move in self: + mo = move.raw_material_production_id + move.modular_type_id = mo.bom_id.bom_line_ids.filtered( + lambda line: line.product_id == move.product_id).modular_type_id + + @api.model_create_multi + def create(self, vals_list): + moves = super().create(vals_list) + for move in moves: + mo = move.raw_material_production_id + so_line = mo.sale_line_id + if so_line: + so_line._set_default_modular_values() + bom_line = mo.bom_id.bom_line_ids.filtered( + lambda line: line.product_id == move.product_id + )[:1] + move.base_bom_qty = bom_line.product_qty * mo.product_qty + modular_value = so_line.modular_value_ids.filtered( + lambda mv: mv.modular_type_id == bom_line.modular_type_id + ) + if bom_line.modular_type_id and modular_value: + move.product_uom_qty = move.base_bom_qty * modular_value.value + return moves diff --git a/sale_mrp_modular_type/security/ir.model.access.csv b/sale_mrp_modular_type/security/ir.model.access.csv new file mode 100644 index 00000000000..ae1199ec1b3 --- /dev/null +++ b/sale_mrp_modular_type/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_modular_types,access_modular_types,model_modular_type,base.group_user,1,1,1,1 +access_modular_type_wizard,access_modular_type_wizard,model_modular_type_wizard,base.group_user,1,1,1,1 +access_modular_type_wizard_line,access_modular_type_wizard_line,model_modular_type_wizard_line,base.group_user,1,1,1,1 +access_sale_order_line_modular_value,access_sale_order_line_modular_value,model_sale_order_line_modular_value,base.group_user,1,1,1,1 diff --git a/sale_mrp_modular_type/views/modular_type_views.xml b/sale_mrp_modular_type/views/modular_type_views.xml new file mode 100644 index 00000000000..839cf439527 --- /dev/null +++ b/sale_mrp_modular_type/views/modular_type_views.xml @@ -0,0 +1,26 @@ + + + + + modular.type.view.form + modular.type + +
+ + +
+
+ + + Modular Type + modular.type + list,form + + + + +
diff --git a/sale_mrp_modular_type/views/mrp_bom_views.xml b/sale_mrp_modular_type/views/mrp_bom_views.xml new file mode 100644 index 00000000000..cff7d45edf1 --- /dev/null +++ b/sale_mrp_modular_type/views/mrp_bom_views.xml @@ -0,0 +1,15 @@ + + + + + mrp.bom.form.modular.type + mrp.bom + + + + + + + + + diff --git a/sale_mrp_modular_type/views/product_template_views.xml b/sale_mrp_modular_type/views/product_template_views.xml new file mode 100644 index 00000000000..96b90043c3b --- /dev/null +++ b/sale_mrp_modular_type/views/product_template_views.xml @@ -0,0 +1,15 @@ + + + + + product.template.form.modular.types + product.template + + + + + + + + + diff --git a/sale_mrp_modular_type/views/sale_order_views.xml b/sale_mrp_modular_type/views/sale_order_views.xml new file mode 100644 index 00000000000..597f30ba5d4 --- /dev/null +++ b/sale_mrp_modular_type/views/sale_order_views.xml @@ -0,0 +1,28 @@ + + + + + Modular Type Value + modular.type.wizard + form + new + + + + sale.order.form.modular.types + sale.order + + + +