diff --git a/.gitignore b/.gitignore
index b6e47617de1..f79670dfc2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,10 @@ dmypy.json
# Pyre type checker
.pyre/
+
+node_modules
+.eslintignore
+.eslintrc.json
+jsconfig.json
+package-lock.json
+package.json
diff --git a/modular_types/__init__.py b/modular_types/__init__.py
new file mode 100644
index 00000000000..9b4296142f4
--- /dev/null
+++ b/modular_types/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/modular_types/__manifest__.py b/modular_types/__manifest__.py
new file mode 100644
index 00000000000..3a869125661
--- /dev/null
+++ b/modular_types/__manifest__.py
@@ -0,0 +1,17 @@
+{
+ 'name': 'Modular Types',
+ 'version': '1.0',
+ 'category': 'Manufacturing',
+ 'author': 'haman',
+ 'depends': ['product', 'mrp', 'sale_management'],
+ 'license': 'LGPL-3',
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/product_views.xml',
+ 'views/mrp_bom_views.xml',
+ 'views/sale_order_views.xml',
+ 'views/mrp_modular_views.xml',
+ 'wizard/modular_type_wizard_views.xml',
+ ],
+ 'installable': True,
+}
diff --git a/modular_types/models/__init__.py b/modular_types/models/__init__.py
new file mode 100644
index 00000000000..129153f7222
--- /dev/null
+++ b/modular_types/models/__init__.py
@@ -0,0 +1,6 @@
+from . import product_template
+from . import modular_type
+from . import mrp_bom
+from . import stock_move
+from . import sale_order_line
+from . import sale_order_line_modular_value
diff --git a/modular_types/models/modular_type.py b/modular_types/models/modular_type.py
new file mode 100644
index 00000000000..d60b905094b
--- /dev/null
+++ b/modular_types/models/modular_type.py
@@ -0,0 +1,8 @@
+from odoo import fields, models
+
+
+class ModularType(models.Model):
+ _name = 'modular.type'
+ _description = 'Modular Type'
+
+ name = fields.Char(required=True)
diff --git a/modular_types/models/mrp_bom.py b/modular_types/models/mrp_bom.py
new file mode 100644
index 00000000000..ed225bcabfc
--- /dev/null
+++ b/modular_types/models/mrp_bom.py
@@ -0,0 +1,13 @@
+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_types if line.product_id else False
diff --git a/modular_types/models/product_template.py b/modular_types/models/product_template.py
new file mode 100644
index 00000000000..6b47c774257
--- /dev/null
+++ b/modular_types/models/product_template.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ modular_types = fields.Many2many('modular.type')
diff --git a/modular_types/models/sale_order_line.py b/modular_types/models/sale_order_line.py
new file mode 100644
index 00000000000..40fef45e4c8
--- /dev/null
+++ b/modular_types/models/sale_order_line.py
@@ -0,0 +1,15 @@
+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_types')
+ def _compute_has_modular_type(self):
+ for line in self:
+ line.has_modular_type = bool(
+ line.product_template_id.modular_types
+ )
diff --git a/modular_types/models/sale_order_line_modular_value.py b/modular_types/models/sale_order_line_modular_value.py
new file mode 100644
index 00000000000..b9e7953f18d
--- /dev/null
+++ b/modular_types/models/sale_order_line_modular_value.py
@@ -0,0 +1,10 @@
+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')
+ modular_type_id = fields.Many2one('modular.type')
+ value = fields.Float()
diff --git a/modular_types/models/stock_move.py b/modular_types/models/stock_move.py
new file mode 100644
index 00000000000..c9f40e28e85
--- /dev/null
+++ b/modular_types/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:
+ move.modular_type_id = move.raw_material_production_id.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
+ bom = mo.bom_id
+ bom_line = bom.bom_line_ids.filtered(
+ lambda l: l.product_id == move.product_id
+ )[:1]
+ base_qty = bom_line.product_qty * mo.product_qty
+ move.base_bom_qty = base_qty
+ modular_map = {
+ mv.modular_type_id.id: mv.value
+ for mv in so_line.modular_value_ids
+ }
+ if move.modular_type_id and move.modular_type_id.id in modular_map:
+ move.product_uom_qty = (
+ base_qty * modular_map[move.modular_type_id.id]
+ )
+ return moves
diff --git a/modular_types/security/ir.model.access.csv b/modular_types/security/ir.model.access.csv
new file mode 100644
index 00000000000..d646feb23ce
--- /dev/null
+++ b/modular_types/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
+modular_types.access_modular_type,access_modular_type,modular_types.model_modular_type,base.group_user,1,1,1,1
+access_modular_type_wizard,modular.type.wizard,model_modular_type_wizard,base.group_user,1,1,1,1
+access_modular_type_wizard_line,modular.type.wizard.line,model_modular_type_wizard_line,base.group_user,1,1,1,1
+access_sale_order_line_modular_value,sale.order.line.modular.value,model_sale_order_line_modular_value,base.group_user,1,1,1,1
diff --git a/modular_types/views/mrp_bom_views.xml b/modular_types/views/mrp_bom_views.xml
new file mode 100644
index 00000000000..cff7d45edf1
--- /dev/null
+++ b/modular_types/views/mrp_bom_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ mrp.bom.form.modular.type
+ mrp.bom
+
+
+
+
+
+
+
+
+
diff --git a/modular_types/views/mrp_modular_views.xml b/modular_types/views/mrp_modular_views.xml
new file mode 100644
index 00000000000..227303768a1
--- /dev/null
+++ b/modular_types/views/mrp_modular_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ mrp.production.view.form.inherit
+ mrp.production
+
+
+
+
+
+
+
+
+
diff --git a/modular_types/views/product_views.xml b/modular_types/views/product_views.xml
new file mode 100644
index 00000000000..b38fc5588ca
--- /dev/null
+++ b/modular_types/views/product_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ product.template.form.modular.types
+ product.template
+
+
+
+
+
+
+
+
+
diff --git a/modular_types/views/sale_order_views.xml b/modular_types/views/sale_order_views.xml
new file mode 100644
index 00000000000..a2b22cf6f95
--- /dev/null
+++ b/modular_types/views/sale_order_views.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ Modular Type Value
+ modular.type.wizard
+ form
+ new
+
+
+ sale.order.form.modular.button
+ sale.order
+
+
+
+
+
+
+
+
+
diff --git a/modular_types/wizard/__init__.py b/modular_types/wizard/__init__.py
new file mode 100644
index 00000000000..66e64aedc69
--- /dev/null
+++ b/modular_types/wizard/__init__.py
@@ -0,0 +1 @@
+from . import modular_type_wizard
diff --git a/modular_types/wizard/modular_type_wizard.py b/modular_types/wizard/modular_type_wizard.py
new file mode 100644
index 00000000000..3f4152b40d5
--- /dev/null
+++ b/modular_types/wizard/modular_type_wizard.py
@@ -0,0 +1,53 @@
+from odoo import api, exceptions, fields, models
+
+
+class ModularTypeWizard(models.TransientModel):
+ _name = 'modular.type.wizard'
+ _description = 'Modular Type Wizard'
+
+ product_id = fields.Many2one('product.template', readonly=True)
+ wizard_line_ids = fields.One2many('modular.type.wizard.line', 'wizard_id')
+
+ @api.model
+ def default_get(self, fields_list):
+ res = super().default_get(fields_list)
+ order_line = self.env['sale.order.line'].browse(
+ self.env.context.get('active_order_line_id')
+ )
+ product = order_line.product_template_id
+ res.update({
+ 'product_id': product.id,
+ 'wizard_line_ids': [
+ (0, 0, {'modular_type_id': mt.id, 'value': 0})
+ for mt in product.modular_types
+ ]
+ })
+ return res
+
+ def add_modular_value(self):
+ active_order_line_id = self.env.context.get('active_order_line_id')
+ if not self.product_id:
+ raise exceptions.UserError("Sales order line not found.")
+ for line in self.wizard_line_ids:
+ existing = self.env['sale.order.line.modular.value'].search([
+ ('order_line_id', '=', active_order_line_id),
+ ('modular_type_id', '=', line.modular_type_id.id),
+ ], limit=1)
+ if existing:
+ existing.value = line.value
+ else:
+ self.env['sale.order.line.modular.value'].create({
+ 'order_line_id': active_order_line_id,
+ 'modular_type_id': line.modular_type_id.id,
+ 'value': line.value,
+ })
+ return
+
+
+class ModularTypeWizardLine(models.TransientModel):
+ _name = 'modular.type.wizard.line'
+ _description = 'Modular Type Wizard Line'
+
+ wizard_id = fields.Many2one('modular.type.wizard')
+ modular_type_id = fields.Many2one('modular.type')
+ value = fields.Float()
diff --git a/modular_types/wizard/modular_type_wizard_views.xml b/modular_types/wizard/modular_type_wizard_views.xml
new file mode 100644
index 00000000000..921ac6b6f14
--- /dev/null
+++ b/modular_types/wizard/modular_type_wizard_views.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ modular.type.wizard.form
+ modular.type.wizard
+
+
+
+
+
+ Set Modular Type Value
+ modular.type.wizard
+ form
+
+
+
+