diff --git a/modular_type/__init__.py b/modular_type/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/modular_type/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/modular_type/__manifest__.py b/modular_type/__manifest__.py new file mode 100644 index 00000000000..40e7f8aeefb --- /dev/null +++ b/modular_type/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Modular MRP", + "version": "1.0", + "description": """Add modular types on products for MRP quantity multiplication.""", + "author": "Soham", + "depends": [ + "product", + "mrp", + "sale_management", + "sale_mrp", + ], + "data": [ + "security/ir.model.access.csv", + "views/product_template_views.xml", + "views/mrp_bom_views.xml", + "views/sale_line_modular_value_wizard_views.xml", + "views/sale_order_views.xml", + ], + "license": "LGPL-3", +} diff --git a/modular_type/models/__init__.py b/modular_type/models/__init__.py new file mode 100644 index 00000000000..49b5d794aa5 --- /dev/null +++ b/modular_type/models/__init__.py @@ -0,0 +1,6 @@ +from . import modular_type +from . import product_template +from . import mrp_bom_line +from . import sale_order_line +from . import sale_order_line_modular_value +from . import sale_order diff --git a/modular_type/models/modular_type.py b/modular_type/models/modular_type.py new file mode 100644 index 00000000000..44d4abafa3d --- /dev/null +++ b/modular_type/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_type/models/mrp_bom_line.py b/modular_type/models/mrp_bom_line.py new file mode 100644 index 00000000000..29d08d15ce4 --- /dev/null +++ b/modular_type/models/mrp_bom_line.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class MrpBomLine(models.Model): + _inherit = "mrp.bom.line" + + modular_type_id = fields.Many2one( + "modular.type", + string="Modular Type", + ) diff --git a/modular_type/models/product_template.py b/modular_type/models/product_template.py new file mode 100644 index 00000000000..0c31b6bd246 --- /dev/null +++ b/modular_type/models/product_template.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + modular_type_ids = fields.Many2many( + "modular.type", + string="Modular Types", + ) diff --git a/modular_type/models/sale_order.py b/modular_type/models/sale_order.py new file mode 100644 index 00000000000..20d79697c30 --- /dev/null +++ b/modular_type/models/sale_order.py @@ -0,0 +1,12 @@ +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_confirm(self): + result = super().action_confirm() + + self.order_line._apply_modular_values_to_productions() + + return result diff --git a/modular_type/models/sale_order_line.py b/modular_type/models/sale_order_line.py new file mode 100644 index 00000000000..84e55940b5d --- /dev/null +++ b/modular_type/models/sale_order_line.py @@ -0,0 +1,49 @@ +from odoo import fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + modular_value_ids = fields.One2many( + "sale.order.line.modular.value", + "sale_line_id", + string="Modular Values", + ) + + def action_open_modular_value_wizard(self): + self.ensure_one() + + return { + "type": "ir.actions.act_window", + "name": "Set Modular Values", + "res_model": "sale.line.modular.value.wizard", + "view_mode": "form", + "target": "new", + "context": { + "default_sale_line_id": self.id, + }, + } + + def _apply_modular_values_to_productions(self): + for line in self: + productions = self.env["mrp.production"].search([ + ("origin", "=", line.order_id.name), + ("product_id", "=", line.product_id.id), + ]) + + modular_values = { + v.modular_type_id.id: v.value + for v in line.modular_value_ids + } + + for move in productions.move_raw_ids.filtered( + lambda m: m.bom_line_id.modular_type_id + ): + multiplier = modular_values.get( + move.bom_line_id.modular_type_id.id + ) + + if multiplier is not None: + move.product_uom_qty = move.bom_line_id.product_qty * multiplier + else: + move.product_uom_qty = 0.0 diff --git a/modular_type/models/sale_order_line_modular_value.py b/modular_type/models/sale_order_line_modular_value.py new file mode 100644 index 00000000000..fbb9284f8df --- /dev/null +++ b/modular_type/models/sale_order_line_modular_value.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class SaleOrderLineModularValue(models.Model): + _name = "sale.order.line.modular.value" + _description = "Sale Order Line Modular Value" + + sale_line_id = fields.Many2one( + "sale.order.line", + string="Sale Order Line", + required=True, + ondelete="cascade", + ) + + modular_type_id = fields.Many2one( + "modular.type", + string="Modular Type", + required=True, + ) + + value = fields.Float( + string="Value", + default=1.0, + required=True, + ) diff --git a/modular_type/security/ir.model.access.csv b/modular_type/security/ir.model.access.csv new file mode 100644 index 00000000000..2c76a71f643 --- /dev/null +++ b/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_type_user,access.modular.type.user,model_modular_type,base.group_user,1,1,1,1 +access_sale_order_line_modular_value_user,access.sale.order.line.modular.value.user,model_sale_order_line_modular_value,base.group_user,1,1,1,1 +access_sale_line_modular_value_wizard_user,access.sale.line.modular.value.wizard.user,model_sale_line_modular_value_wizard,base.group_user,1,1,1,1 +access_sale_line_modular_value_wizard_line_user,access.sale.line.modular.value.wizard.line.user,model_sale_line_modular_value_wizard_line,base.group_user,1,1,1,1 diff --git a/modular_type/tests/__init__.py b/modular_type/tests/__init__.py new file mode 100644 index 00000000000..f2c01d9b8e0 --- /dev/null +++ b/modular_type/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_modular_type diff --git a/modular_type/tests/common.py b/modular_type/tests/common.py new file mode 100644 index 00000000000..a372dc1656c --- /dev/null +++ b/modular_type/tests/common.py @@ -0,0 +1,109 @@ +from odoo.tests import TransactionCase + + +class ModularTypeCommon(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.mto_route = cls.env.ref("stock.route_warehouse0_mto") + cls.manufacture_route = cls.env.ref("mrp.route_warehouse0_manufacture") + + cls.type_sections = cls.env["modular.type"].create({ + "name": "Sections" + }) + cls.type_meters = cls.env["modular.type"].create({ + "name": "Meters" + }) + + cls.product = cls.env["product.template"].create({ + "name": "Test Fencing Product", + "type": "consu", + "modular_type_ids": [(6, 0, [ + cls.type_sections.id, + cls.type_meters.id, + ])], + "route_ids": [(6, 0, [ + cls.mto_route.id, + cls.manufacture_route.id, + ])], + }) + + cls.product_no_modular = cls.env["product.template"].create({ + "name": "Normal Product", + "type": "consu", + "route_ids": [(6, 0, [ + cls.mto_route.id, + cls.manufacture_route.id, + ])], + }) + + cls.component_sections = cls.env["product.product"].create({ + "name": "Railing", + "type": "consu", + }) + cls.component_meters = cls.env["product.product"].create({ + "name": "Liner", + "type": "consu", + }) + cls.component_no_type = cls.env["product.product"].create({ + "name": "Screw", + "type": "consu", + }) + + cls.bom = cls.env["mrp.bom"].create({ + "product_tmpl_id": cls.product.id, + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + (0, 0, { + "product_id": cls.component_sections.id, + "product_qty": 5.0, + "modular_type_id": cls.type_sections.id, + }), + (0, 0, { + "product_id": cls.component_meters.id, + "product_qty": 2.0, + "modular_type_id": cls.type_meters.id, + }), + (0, 0, { + "product_id": cls.component_no_type.id, + "product_qty": 10.0, + }), + ], + }) + + cls.bom_no_modular = cls.env["mrp.bom"].create({ + "product_tmpl_id": cls.product_no_modular.id, + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + (0, 0, { + "product_id": cls.component_no_type.id, + "product_qty": 10.0, + }), + ], + }) + + cls.customer = cls.env["res.partner"].create({ + "name": "Test Customer" + }) + + def _create_so(self, product_tmpl, qty=1.0, **values): + """Helper — create a sale order with one line""" + return self.env["sale.order"].create({ + "partner_id": self.customer.id, + "order_line": [(0, 0, { + "product_id": product_tmpl.product_variant_ids[0].id, + "product_uom_qty": qty, + })], + **values, + }) + + def _get_mo(self, so, so_line): + """Helper — fetch MO created from a confirmed SO line""" + return self.env["mrp.production"].search([ + ("origin", "=", so.name), + ("product_id", "=", so_line.product_id.id), + ]) diff --git a/modular_type/tests/test_modular_type.py b/modular_type/tests/test_modular_type.py new file mode 100644 index 00000000000..df17e784ebe --- /dev/null +++ b/modular_type/tests/test_modular_type.py @@ -0,0 +1,81 @@ +from odoo.tests import tagged +from .common import ModularTypeCommon + + +@tagged("post_install", "-at_install", "modular_type") +class TestModularType(ModularTypeCommon): + + def test_01_modular_values_applied_to_mo(self): + so = self._create_so(self.product) + so_line = so.order_line[0] + + so_line.write({ + "modular_value_ids": [ + (0, 0, { + "modular_type_id": self.type_sections.id, + "value": 6.0, + }), + (0, 0, { + "modular_type_id": self.type_meters.id, + "value": 3.0, + }), + ] + }) + + so.action_confirm() + so_line._apply_modular_values_to_productions() + + mo = self._get_mo(so, so_line) + self.assertTrue(mo, "MO should be created after SO confirm") + + for move in mo.move_raw_ids: + if move.bom_line_id.modular_type_id == self.type_sections: + self.assertEqual( + move.product_uom_qty, 30.0, + "Sections: 5 × 6 = 30" + ) + elif move.bom_line_id.modular_type_id == self.type_meters: + self.assertEqual( + move.product_uom_qty, 6.0, + "Meters: 2 × 3 = 6" + ) + + def test_02_no_values_set_qty_is_zero(self): + so = self._create_so(self.product) + so_line = so.order_line[0] + + self.assertFalse( + so_line.modular_value_ids, + "No modular values should be set" + ) + + so.action_confirm() + so_line._apply_modular_values_to_productions() + + mo = self._get_mo(so, so_line) + self.assertTrue(mo, "MO should still be created") + + for move in mo.move_raw_ids.filtered( + lambda m: m.bom_line_id.modular_type_id + ): + self.assertEqual( + move.product_uom_qty, 0.0, + f"{move.product_id.name} qty should be 0 when no values set" + ) + + def test_03_no_modular_type_standard_qty(self): + so = self._create_so(self.product_no_modular, qty=2.0) + so_line = so.order_line[0] + + so.action_confirm() + + so_line._apply_modular_values_to_productions() + + mo = self._get_mo(so, so_line) + self.assertTrue(mo, "MO should be created") + + for move in mo.move_raw_ids: + self.assertEqual( + move.product_uom_qty, 20.0, + "Standard product should follow normal qty calculation" + ) diff --git a/modular_type/views/mrp_bom_views.xml b/modular_type/views/mrp_bom_views.xml new file mode 100644 index 00000000000..e63cfeddb98 --- /dev/null +++ b/modular_type/views/mrp_bom_views.xml @@ -0,0 +1,17 @@ + + + + + mrp.bom.form.inherit.modular.type + mrp.bom + + + + + + + + + + + diff --git a/modular_type/views/product_template_views.xml b/modular_type/views/product_template_views.xml new file mode 100644 index 00000000000..4f18216f5ee --- /dev/null +++ b/modular_type/views/product_template_views.xml @@ -0,0 +1,21 @@ + + + + + product.template.form.inherit.modular.type + product.template + + + + + + + + + + + diff --git a/modular_type/views/sale_line_modular_value_wizard_views.xml b/modular_type/views/sale_line_modular_value_wizard_views.xml new file mode 100644 index 00000000000..a923fd97c52 --- /dev/null +++ b/modular_type/views/sale_line_modular_value_wizard_views.xml @@ -0,0 +1,30 @@ + + + + + sale.line.modular.value.wizard.form + sale.line.modular.value.wizard + +
+ + + + + + + + + + + + + + +
+
+
+ +
diff --git a/modular_type/views/sale_order_views.xml b/modular_type/views/sale_order_views.xml new file mode 100644 index 00000000000..413ef7424e1 --- /dev/null +++ b/modular_type/views/sale_order_views.xml @@ -0,0 +1,23 @@ + + + + + sale.order.form.inherit.modular.value + sale.order + + + + +