From 9bc12801ba3653da632f040e00028d2929cbc461 Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Mon, 25 May 2026 18:31:13 +0530 Subject: [PATCH] [ADD] new_product_type: add configurable kit product support Standard Odoo requires the Manufacturing (MRP) and Bill of Materials (BoM) modules to handle kit products. Users who only do trading or pure sales without manufacturing need a lightweight way to sell product bundles, manage their pricing dynamically, and control their visibility on invoices without overhead. WHAT: - Added a new product type for kits without MRP dependencies. - Created a configuration wizard on sale order lines to manage kit items, quantities, and pricing before confirmation. - Implemented parent-child relations between sale order lines to link kits with sub-products. - Made sub-product lines readonly and protected them from direct deletion to ensure data integrity. - Updated report logic (sale orders, portal, invoices) with a dedicated print option to show or hide sub-products. --- new_product_type/__init__.py | 2 + new_product_type/__manifest__.py | 17 +++ new_product_type/models/__init__.py | 4 + new_product_type/models/account_move.py | 15 +++ new_product_type/models/product_template.py | 15 +++ new_product_type/models/sale_order.py | 9 ++ new_product_type/models/sale_order_line.py | 39 +++++++ new_product_type/report/invoice_report.xml | 14 +++ .../report/sale_order_portal_report.xml | 14 +++ new_product_type/report/sale_order_report.xml | 14 +++ new_product_type/security/ir.model.access.csv | 3 + .../views/product_kit_wizard_views.xml | 29 +++++ new_product_type/views/product_views.xml | 17 +++ .../views/sale_order_line_views.xml | 38 +++++++ new_product_type/wizard/__init__.py | 2 + new_product_type/wizard/product_kit_wizard.py | 103 ++++++++++++++++++ .../wizard/product_kit_wizard_line.py | 24 ++++ 17 files changed, 359 insertions(+) create mode 100644 new_product_type/__init__.py create mode 100644 new_product_type/__manifest__.py create mode 100644 new_product_type/models/__init__.py create mode 100644 new_product_type/models/account_move.py create mode 100644 new_product_type/models/product_template.py create mode 100644 new_product_type/models/sale_order.py create mode 100644 new_product_type/models/sale_order_line.py create mode 100644 new_product_type/report/invoice_report.xml create mode 100644 new_product_type/report/sale_order_portal_report.xml create mode 100644 new_product_type/report/sale_order_report.xml create mode 100644 new_product_type/security/ir.model.access.csv create mode 100644 new_product_type/views/product_kit_wizard_views.xml create mode 100644 new_product_type/views/product_views.xml create mode 100644 new_product_type/views/sale_order_line_views.xml create mode 100644 new_product_type/wizard/__init__.py create mode 100644 new_product_type/wizard/product_kit_wizard.py create mode 100644 new_product_type/wizard/product_kit_wizard_line.py diff --git a/new_product_type/__init__.py b/new_product_type/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/new_product_type/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/new_product_type/__manifest__.py b/new_product_type/__manifest__.py new file mode 100644 index 00000000000..f52f5756308 --- /dev/null +++ b/new_product_type/__manifest__.py @@ -0,0 +1,17 @@ +{ + "name": "New Product Type", + "version": "1.0", + 'author': "habar", + "depends": ["sale", "product", 'account'], + "data": [ + 'security/ir.model.access.csv', + "report/sale_order_portal_report.xml", + "report/sale_order_report.xml", + "views/product_kit_wizard_views.xml", + "views/product_views.xml", + "report/invoice_report.xml", + "views/sale_order_line_views.xml", + ], + "installable": True, + 'license': 'LGPL-3', +} diff --git a/new_product_type/models/__init__.py b/new_product_type/models/__init__.py new file mode 100644 index 00000000000..fbe8e5e1443 --- /dev/null +++ b/new_product_type/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_template +from . import sale_order_line +from . import sale_order +from . import account_move diff --git a/new_product_type/models/account_move.py b/new_product_type/models/account_move.py new file mode 100644 index 00000000000..de869dd41d7 --- /dev/null +++ b/new_product_type/models/account_move.py @@ -0,0 +1,15 @@ +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def show_in_report(self): + self.ensure_one() + + sale_line = self.sale_line_ids[:1] + + return ( + not sale_line.is_kit_product + or sale_line.order_id.print_in_report + ) diff --git a/new_product_type/models/product_template.py b/new_product_type/models/product_template.py new file mode 100644 index 00000000000..c7a5541261a --- /dev/null +++ b/new_product_type/models/product_template.py @@ -0,0 +1,15 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_kit = fields.Boolean() + sub_product = fields.Many2many("product.product") + + @api.constrains("sub_product") + def _check_no_self_product_reference(self): + for record in self: + if record.product_variant_id in record.sub_product: + raise ValidationError("A product cannot be added as a sub-product in its own kit.") diff --git a/new_product_type/models/sale_order.py b/new_product_type/models/sale_order.py new file mode 100644 index 00000000000..2e48c082fe9 --- /dev/null +++ b/new_product_type/models/sale_order.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + print_in_report = fields.Boolean( + string="Print Kit Products" + ) diff --git a/new_product_type/models/sale_order_line.py b/new_product_type/models/sale_order_line.py new file mode 100644 index 00000000000..9692e61413a --- /dev/null +++ b/new_product_type/models/sale_order_line.py @@ -0,0 +1,39 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + is_kit = fields.Boolean( + related="product_id.product_tmpl_id.is_kit", + store=True + ) + is_kit_product = fields.Boolean() + kit_parent_line_id = fields.Many2one("sale.order.line", ondelete="cascade") + extra_price = fields.Float(default=0.0) + + @api.ondelete(at_uninstall=False) + def _check_kit_product_restriction(self): + for line in self: + if line.is_kit_product: + raise UserError(_("You cannot delete a kit sub product directly.")) + + def show_in_report(self): + self.ensure_one() + return ( + not self.is_kit_product + or self.order_id.print_in_report + ) + + def action_open_kit_wizard(self): + return { + "type": "ir.actions.act_window", + "name": "Configure Kit", + "res_model": "product.kit.wizard", + "view_mode": "form", + "target": "new", + "context": { + "active_id": self.id + }, + } diff --git a/new_product_type/report/invoice_report.xml b/new_product_type/report/invoice_report.xml new file mode 100644 index 00000000000..c09d9f8915c --- /dev/null +++ b/new_product_type/report/invoice_report.xml @@ -0,0 +1,14 @@ + + + + diff --git a/new_product_type/report/sale_order_portal_report.xml b/new_product_type/report/sale_order_portal_report.xml new file mode 100644 index 00000000000..00a5ac89e06 --- /dev/null +++ b/new_product_type/report/sale_order_portal_report.xml @@ -0,0 +1,14 @@ + + + + diff --git a/new_product_type/report/sale_order_report.xml b/new_product_type/report/sale_order_report.xml new file mode 100644 index 00000000000..3f793a7b4a3 --- /dev/null +++ b/new_product_type/report/sale_order_report.xml @@ -0,0 +1,14 @@ + + + + diff --git a/new_product_type/security/ir.model.access.csv b/new_product_type/security/ir.model.access.csv new file mode 100644 index 00000000000..1a7a4f167f2 --- /dev/null +++ b/new_product_type/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_create,perm_write,perm_unlink +access_product_kit_wizard,access_product_kit_wizard,model_product_kit_wizard,base.group_user,1,1,1,1 +access_product_kit_wizard_line,access_product_kit_wizard_line,model_product_kit_wizard_line,base.group_user,1,1,1,1 diff --git a/new_product_type/views/product_kit_wizard_views.xml b/new_product_type/views/product_kit_wizard_views.xml new file mode 100644 index 00000000000..61da1b4b60f --- /dev/null +++ b/new_product_type/views/product_kit_wizard_views.xml @@ -0,0 +1,29 @@ + + + + product.kit.wizard.form + product.kit.wizard + +
+ + + + + + + + + + + + + + +
+
+
+
+
+
diff --git a/new_product_type/views/product_views.xml b/new_product_type/views/product_views.xml new file mode 100644 index 00000000000..542f7836845 --- /dev/null +++ b/new_product_type/views/product_views.xml @@ -0,0 +1,17 @@ + + + + product.template.view.form + product.template + + + + + + + + + + + diff --git a/new_product_type/views/sale_order_line_views.xml b/new_product_type/views/sale_order_line_views.xml new file mode 100644 index 00000000000..d0ba5c40477 --- /dev/null +++ b/new_product_type/views/sale_order_line_views.xml @@ -0,0 +1,38 @@ + + + + sale.order.line.form.view + sale.order + + + + is_kit_product + + +