diff --git a/product_type_kit/__init__.py b/product_type_kit/__init__.py
new file mode 100644
index 00000000000..aee8895e7a3
--- /dev/null
+++ b/product_type_kit/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizards
diff --git a/product_type_kit/__manifest__.py b/product_type_kit/__manifest__.py
new file mode 100644
index 00000000000..bc04b060684
--- /dev/null
+++ b/product_type_kit/__manifest__.py
@@ -0,0 +1,17 @@
+{
+ 'name': 'Product Type Kit',
+ 'version': '1.0',
+ 'category': 'Sales',
+ 'license': 'LGPL-3',
+ 'summary': 'Custom product kit functionality without using BoM',
+ 'depends': ['sale', 'product'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/product_template_views.xml',
+ 'views/sale_order_views.xml',
+ 'views/sale_order_line_views.xml',
+ 'views/kit_subproduct_wizards.xml',
+ ],
+ 'installable': True,
+ 'application': True,
+}
diff --git a/product_type_kit/models/__init__.py b/product_type_kit/models/__init__.py
new file mode 100644
index 00000000000..53fa79af356
--- /dev/null
+++ b/product_type_kit/models/__init__.py
@@ -0,0 +1,3 @@
+from . import product_template
+from . import sale_order
+from . import sale_order_line
diff --git a/product_type_kit/models/product_template.py b/product_type_kit/models/product_template.py
new file mode 100644
index 00000000000..3aeffd94610
--- /dev/null
+++ b/product_type_kit/models/product_template.py
@@ -0,0 +1,16 @@
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ is_kit = fields.Boolean(string='Is Kit')
+ sub_product_ids = fields.Many2many(
+ 'product.product',
+ 'product_kit_sub_products_rel',
+ 'kit_id',
+ 'sub_product_id',
+ string='Sub Products',
+ domain="[('type', '=', 'product')]"
+ )
+ show_subproducts_on_report = fields.Boolean(string='Print Subproducts on Report')
diff --git a/product_type_kit/models/sale_order.py b/product_type_kit/models/sale_order.py
new file mode 100644
index 00000000000..31b6729e6a6
--- /dev/null
+++ b/product_type_kit/models/sale_order.py
@@ -0,0 +1,18 @@
+from odoo import api, fields, models
+
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ print_kit_details = fields.Boolean(string="Print Kit Details")
+ display_in_report = fields.Boolean(string="Print in report?", default=True)
+
+ @api.model
+ def unlink(self):
+ for order in self:
+ for line in order.order_line:
+ if not line.is_subproduct:
+ # delete sub lines linked to this main line
+ sub_lines = order.order_line.filtered(lambda l: l.kit_parent_line_id == line.id)
+ sub_lines.unlink()
+ return super().unlink()
diff --git a/product_type_kit/models/sale_order_line.py b/product_type_kit/models/sale_order_line.py
new file mode 100644
index 00000000000..a1018d81189
--- /dev/null
+++ b/product_type_kit/models/sale_order_line.py
@@ -0,0 +1,42 @@
+# models/sale_order_line.py
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order.line'
+
+ kit_parent_line_id = fields.Many2one('sale.order.line', string='Parent Kit Line', ondelete='cascade')
+ is_kit_product = fields.Boolean(
+ string="Is Kit Product", compute="_compute_is_kit", store=True
+ )
+ is_subproduct = fields.Boolean(string="Is Sub Product", default=False)
+
+ @api.depends("product_id")
+ def _compute_is_kit(self):
+ for line in self:
+ line.is_kit_product = line.product_id.product_tmpl_id.is_kit
+
+ def write(self, vals):
+ for line in self:
+ if line.kit_parent_line_id:
+ raise UserError("Sub product lines cannot be edited manually.")
+ return super().write(vals)
+
+ def open_kit_wizard(self):
+ self.ensure_one()
+ return {
+ 'name': f'Configure Kit: {self.product_id.name}',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'kit.sub.product.wizard',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {
+ 'default_order_line_id': self.id,
+ }
+ }
+
+ @api.onchange('kit_parent_line_id')
+ def _onchange_disable_edit(self):
+ if self.kit_parent_line_id:
+ self.update({'price_unit': 0})
diff --git a/product_type_kit/security/ir.model.access.csv b/product_type_kit/security/ir.model.access.csv
new file mode 100644
index 00000000000..ec1e4e8cc81
--- /dev/null
+++ b/product_type_kit/security/ir.model.access.csv
@@ -0,0 +1,6 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_product_template,access_product_template,model_product_template,base.group_user,1,1,1,1
+access_sale_order_line,access_sale_order_line,model_sale_order_line,base.group_user,1,1,1,1
+access_sale_order,access_sale_order,model_sale_order,base.group_user,1,1,1,1
+access_kit_sub_product_wizard,access_kit_sub_product_wizard,model_kit_sub_product_wizard,,1,1,1,1
+access_kit_sub_product_line,access_kit_sub_product_line,model_kit_sub_product_line,,1,1,1,1
diff --git a/product_type_kit/views/kit_subproduct_wizards.xml b/product_type_kit/views/kit_subproduct_wizards.xml
new file mode 100644
index 00000000000..ba25d6f0d58
--- /dev/null
+++ b/product_type_kit/views/kit_subproduct_wizards.xml
@@ -0,0 +1,25 @@
+
+
+
+ kit.sub.product.wizard
+ kit.sub.product.wizard
+
+
+
+
+
diff --git a/product_type_kit/views/product_template_views.xml b/product_type_kit/views/product_template_views.xml
new file mode 100644
index 00000000000..f2dddce8f68
--- /dev/null
+++ b/product_type_kit/views/product_template_views.xml
@@ -0,0 +1,14 @@
+
+
+
+ product.template.form.kit
+ product.template
+
+
+
+
+
+
+
+
+
diff --git a/product_type_kit/views/sale_order_line_views.xml b/product_type_kit/views/sale_order_line_views.xml
new file mode 100644
index 00000000000..73ff97bb4f6
--- /dev/null
+++ b/product_type_kit/views/sale_order_line_views.xml
@@ -0,0 +1,18 @@
+
+
+
+ sale.order.line.tree.readonly.kit
+ sale.order
+
+
+
+ 1
+ {'readonly': [('is_subproduct', '=', True)]}
+
+
+ 1
+ {'readonly': [('is_subproduct', '=', True)]}
+
+
+
+
diff --git a/product_type_kit/views/sale_order_views.xml b/product_type_kit/views/sale_order_views.xml
new file mode 100644
index 00000000000..b2a7bd970ac
--- /dev/null
+++ b/product_type_kit/views/sale_order_views.xml
@@ -0,0 +1,16 @@
+
+
+
+ sale.order.line.kit.button
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
diff --git a/product_type_kit/wizards/__init__.py b/product_type_kit/wizards/__init__.py
new file mode 100644
index 00000000000..cdd104a5a2d
--- /dev/null
+++ b/product_type_kit/wizards/__init__.py
@@ -0,0 +1,2 @@
+from . import kit_sub_product_line
+from . import kit_subproduct_wizards
diff --git a/product_type_kit/wizards/kit_sub_product_line.py b/product_type_kit/wizards/kit_sub_product_line.py
new file mode 100644
index 00000000000..3f3ef3e0a60
--- /dev/null
+++ b/product_type_kit/wizards/kit_sub_product_line.py
@@ -0,0 +1,11 @@
+from odoo import fields, models
+
+
+class KitSubProductLine(models.TransientModel):
+ _name = 'kit.sub.product.line'
+ _description = 'Kit Sub Product Line'
+
+ wizard_id = fields.Many2one('kit.sub.product.wizard', required=True, ondelete='cascade')
+ product_id = fields.Many2one('product.product', required=True)
+ quantity = fields.Float(string='Quantity', default=1.0)
+ price_unit = fields.Float(string='Price', default=0.0)
diff --git a/product_type_kit/wizards/kit_subproduct_wizards.py b/product_type_kit/wizards/kit_subproduct_wizards.py
new file mode 100644
index 00000000000..8860c385d38
--- /dev/null
+++ b/product_type_kit/wizards/kit_subproduct_wizards.py
@@ -0,0 +1,46 @@
+# wizards/kit_sub_product_wizard.py
+from odoo import api, fields, models
+
+
+class KitSubProductWizard(models.TransientModel):
+ _name = 'kit.sub.product.wizard'
+ _description = 'Kit Sub Products Wizard'
+
+ order_line_id = fields.Many2one('sale.order.line', required=True)
+ sub_product_lines = fields.One2many('kit.sub.product.line', 'wizard_id')
+
+ @api.model
+ def default_get(self, fields_list):
+ res = super().default_get(fields_list)
+ line = self.env['sale.order.line'].browse(self.env.context['default_order_line_id'])
+ previous_sub_lines = self.env['sale.order.line'].search([
+ ('kit_parent_line_id', '=', line.id)
+ ])
+ sub_lines = []
+ for sub_product in line.product_id.sub_product_ids:
+ prev_line = previous_sub_lines.filtered(lambda l: l.product_id == sub_product)
+ sub_lines.append((0, 0, {
+ 'product_id': sub_product.id,
+ 'quantity': prev_line.product_uom_qty if prev_line else 1.0,
+ 'price_unit': prev_line.price_unit if prev_line else sub_product.lst_price,
+ }))
+ res['sub_product_lines'] = sub_lines
+ return res
+
+ def action_confirm(self):
+ SaleOrderLine = self.env['sale.order.line']
+ # Remove existing kit sub lines
+ existing_lines = SaleOrderLine.search([('kit_parent_line_id', '=', self.order_line_id.id)])
+ existing_lines.unlink()
+
+ for sub_line in self.sub_product_lines:
+ SaleOrderLine.create({
+ 'order_id': self.order_line_id.order_id.id,
+ 'product_id': sub_line.product_id.id,
+ 'product_uom_qty': sub_line.quantity,
+ 'price_unit': 0.0,
+ 'kit_parent_line_id': self.order_line_id.id,
+ })
+
+ total_price = sum(l.quantity * l.price_unit for l in self.sub_product_lines)
+ self.order_line_id.price_unit = total_price