diff --git a/product_kit/__init__.py b/product_kit/__init__.py
new file mode 100644
index 00000000000..aee8895e7a3
--- /dev/null
+++ b/product_kit/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizards
diff --git a/product_kit/__manifest__.py b/product_kit/__manifest__.py
new file mode 100644
index 00000000000..213f929d586
--- /dev/null
+++ b/product_kit/__manifest__.py
@@ -0,0 +1,20 @@
+{
+ 'name': "product_kit",
+ 'version': '1.0',
+ 'license': 'LGPL-3',
+ 'depends': ['sale_management'],
+ 'author': "Kalpan Desai",
+ 'category': 'Sales/Sales',
+ 'description': """
+ Module specifically designed to manage product as kits in Odoo.
+ """,
+ 'installable': True,
+ 'application': True,
+ 'data': [
+ 'views/product_views.xml',
+ 'wizards/sub_product_wizard_view.xml',
+ 'views/sale_order_view.xml',
+ 'views/sale_portal_view.xml',
+ 'security/ir.model.access.csv',
+ ],
+}
diff --git a/product_kit/models/__init__.py b/product_kit/models/__init__.py
new file mode 100644
index 00000000000..60ef5571f3b
--- /dev/null
+++ b/product_kit/models/__init__.py
@@ -0,0 +1,4 @@
+from . import sale_order_line
+from . import product_template
+from . import sale_order
+from . import sale_invoice
diff --git a/product_kit/models/product_template.py b/product_kit/models/product_template.py
new file mode 100644
index 00000000000..8539052f070
--- /dev/null
+++ b/product_kit/models/product_template.py
@@ -0,0 +1,9 @@
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ is_kit = fields.Boolean(string='Is Kit', help='Check this box if the product is a kit that contains other products.', default=False)
+
+ kit_product_ids = fields.Many2many('product.product', string='Sub Products', help='Products included in this kit.')
diff --git a/product_kit/models/sale_invoice.py b/product_kit/models/sale_invoice.py
new file mode 100644
index 00000000000..9fd5676945f
--- /dev/null
+++ b/product_kit/models/sale_invoice.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class AccountMove(models.Model):
+ _inherit = 'account.move'
+
+ is_printable_kit = fields.Boolean(string="Show Sub-Products", default=True)
diff --git a/product_kit/models/sale_order.py b/product_kit/models/sale_order.py
new file mode 100644
index 00000000000..4ac2ef6eed5
--- /dev/null
+++ b/product_kit/models/sale_order.py
@@ -0,0 +1,54 @@
+from odoo import api, fields, models
+
+
+class SaleOrder(models.Model):
+ _inherit = "sale.order"
+
+ is_printable_kit = fields.Boolean(
+ string="Print Kit in Report",
+ default=False,
+ help="If checked, kit products will be printed in the report."
+ )
+
+ has_kit = fields.Boolean(
+ string="Has Kit Product",
+ compute="_compute_has_kit",
+ store=True,
+ default=False,
+ )
+
+ @api.depends('order_line', 'order_line.is_kit')
+ def _compute_has_kit(self):
+ for order in self:
+ order.has_kit = any(line.is_kit for line in order.order_line)
+
+ def _prepare_invoice(self):
+ """
+ Prepare the dict of values to create the new invoice for a sales order.
+ """
+ invoice_vals = super()._prepare_invoice()
+ # Copy the value of your toggle to the invoice
+ invoice_vals['is_printable_kit'] = self.is_printable_kit
+ return invoice_vals
+
+ def _create_invoices(self, grouped=False, final=False, date=None):
+ # First, create the invoice(s) using the standard Odoo method.
+ # This will include lines for the main product and all sub-products.
+ invoices = super()._create_invoices(grouped, final, date)
+
+ # Now, loop through the newly created invoices to apply your logic
+ for invoice in invoices:
+ # Check the toggle copied from the Sale Order
+ if not invoice.is_printable_kit:
+
+ # Find all invoice lines that came from a sale order line
+ # marked as a sub-product.
+ sub_product_invoice_lines = invoice.invoice_line_ids.filtered(
+ lambda line: line.sale_line_ids and line.sale_line_ids[0].is_subproduct
+ )
+
+ # If any sub-product lines were found, delete them
+ if sub_product_invoice_lines:
+ sub_product_invoice_lines.unlink()
+
+ return invoices
diff --git a/product_kit/models/sale_order_line.py b/product_kit/models/sale_order_line.py
new file mode 100644
index 00000000000..737cae8ad3b
--- /dev/null
+++ b/product_kit/models/sale_order_line.py
@@ -0,0 +1,40 @@
+from odoo import fields, models
+from odoo.exceptions import UserError
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ is_kit = fields.Boolean(related="product_template_id.is_kit")
+ parent_product_id = fields.Many2one('product.product')
+ is_subproduct = fields.Boolean(default=False)
+
+ def unlink(self):
+ """
+ Override unlink to also delete sub-product lines when a kit line is deleted.
+ It also prevents the direct deletion of a sub-product line.
+ """
+ # Prevent direct deletion of sub-product lines.
+ sub_product_lines = self.filtered('is_subproduct')
+ if sub_product_lines:
+ # Find the parent kit lines for these sub-products
+ parent_lines = self.env['sale.order.line'].search([
+ ('order_id', 'in', sub_product_lines.mapped('order_id').ids),
+ ('product_id', 'in', sub_product_lines.mapped('parent_product_id').ids),
+ ('is_kit', '=', True)
+ ])
+ # If any of the required parent lines are not in the current deletion set, raise an error.
+ if parent_lines - self:
+ raise UserError("You cannot delete a component of a kit directly. Please remove the main kit product instead.")
+
+ # Cascade delete: find all sub-products of kits being deleted.
+ sub_products_to_delete = self.env['sale.order.line']
+ for line in self.filtered('is_kit'):
+ sub_products_to_delete |= self.search([
+ ('order_id', '=', line.order_id.id),
+ ('parent_product_id', '=', line.product_id.id),
+ ('is_subproduct', '=', True)
+ ])
+
+ all_records_to_delete = self | sub_products_to_delete
+ return super(SaleOrderLine, all_records_to_delete).unlink()
diff --git a/product_kit/report/sale_order_report_view.xml b/product_kit/report/sale_order_report_view.xml
new file mode 100644
index 00000000000..ffa9f712ad4
--- /dev/null
+++ b/product_kit/report/sale_order_report_view.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ (sale_order.is_printable_kit and line.is_subproduct) or not line.is_subproduct
+
+
+
diff --git a/product_kit/security/ir.model.access.csv b/product_kit/security/ir.model.access.csv
new file mode 100755
index 00000000000..2e34c350689
--- /dev/null
+++ b/product_kit/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_subproduct,access_subproduct,model_product_kit_subproduct_wizard,base.group_user,1,1,1,1
+access_subproduct_line,access_subproduct_line,model_product_kit_subproduct_line,base.group_user,1,1,1,1
diff --git a/product_kit/views/product_views.xml b/product_kit/views/product_views.xml
new file mode 100755
index 00000000000..625172cd296
--- /dev/null
+++ b/product_kit/views/product_views.xml
@@ -0,0 +1,14 @@
+
+
+
+ product.template.form.kit
+ product.template
+
+
+
+
+
+
+
+
+
diff --git a/product_kit/views/sale_order_view.xml b/product_kit/views/sale_order_view.xml
new file mode 100755
index 00000000000..4d843898131
--- /dev/null
+++ b/product_kit/views/sale_order_view.xml
@@ -0,0 +1,33 @@
+
+
+
+ sale.order.form.view.kit
+ sale.order
+
+
+
+
+
+
+
+ is_subproduct
+
+
+
+ is_subproduct
+
+
+
+ is_subproduct
+
+
+ is_subproduct
+
+
+
+
+
+
+
+
diff --git a/product_kit/views/sale_portal_view.xml b/product_kit/views/sale_portal_view.xml
new file mode 100644
index 00000000000..0840ae7d8b6
--- /dev/null
+++ b/product_kit/views/sale_portal_view.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ (sale_order.is_printable_kit and line.is_subproduct) or not line.is_subproduct
+
+
+
diff --git a/product_kit/wizards/__init__.py b/product_kit/wizards/__init__.py
new file mode 100644
index 00000000000..c1b1a5accbb
--- /dev/null
+++ b/product_kit/wizards/__init__.py
@@ -0,0 +1,2 @@
+from . import sub_product_wizard
+from . import sub_product_wizard_line
diff --git a/product_kit/wizards/sub_product_wizard.py b/product_kit/wizards/sub_product_wizard.py
new file mode 100644
index 00000000000..196b246e889
--- /dev/null
+++ b/product_kit/wizards/sub_product_wizard.py
@@ -0,0 +1,89 @@
+from odoo import api, fields, models
+
+
+class SubproductWizard(models.TransientModel):
+ _name = 'product_kit.subproduct.wizard'
+ _description = 'Sub-product Wizard'
+
+ product_id = fields.Many2one('product.product', string="Product", required=True)
+ sub_product_ids = fields.One2many('product_kit.subproduct.line', 'subproduct_line_id')
+ order_line_id = fields.Many2one('sale.order.line')
+
+ @api.model
+ def default_get(self, fields):
+ res = super().default_get(fields)
+
+ product_id = self.env.context.get('default_product_id')
+ order_line_id = self.env.context.get('default_order_line_id')
+ product = self.env['product.product'].browse(product_id)
+ sub_products = self.env['product_kit.subproduct.wizard'].search(
+ [('order_line_id', '=', order_line_id),
+ ('product_id', '=', product_id)],
+ order='id desc', limit=1)
+
+ sub_products_list = []
+
+ if sub_products.exists():
+ # Update wizard data
+ for subproduct, selected_product in zip(sub_products.sub_product_ids, product.kit_product_ids):
+ sub_products_list.append((0, 0, {
+ 'product_id': selected_product.id,
+ 'sub_product_name': selected_product.product_tmpl_id.name,
+ 'quantity': subproduct.quantity,
+ 'unit_price': subproduct.unit_price
+ }))
+ else:
+ # New Data
+ for selected_product in product.kit_product_ids:
+ sub_products_list.append((0, 0, {
+ 'product_id': selected_product.id,
+ 'sub_product_name': selected_product.product_tmpl_id.name,
+ 'quantity': 1.0,
+ 'unit_price': selected_product.list_price,
+ }))
+
+ res['sub_product_ids'] = sub_products_list
+ return res
+
+ def action_confirm_subproduct(self):
+ order_line = self.env['sale.order.line'].search([('id', '=', self.order_line_id.id)])
+
+ # Explicitly ensure the main parent product line is NOT a sub-product.
+ # This is the key to fixing your invoice issue.
+ order_line.write({
+ 'is_subproduct': False,
+ 'is_kit': True, # Recommended: also set a flag that it IS a kit
+ })
+
+ product = self.env['product.product'].browse(self.product_id.id)
+ order_line_subproduct = self.env['sale.order.line'].search(
+ [('order_id', '=', order_line.order_id.id),
+ ('parent_product_id', '=', self.product_id.id)])
+
+ total = 0
+ if order_line_subproduct.exists():
+ # Update Order-line
+ for sub_product_data, order_line_rec in zip(self.sub_product_ids, order_line_subproduct):
+ total += (sub_product_data.unit_price * sub_product_data.quantity)
+
+ order_line_rec.product_uom_qty = sub_product_data.quantity
+ order_line_rec.price_unit = 0
+ else:
+ # Create Order-line
+ for sub_product_data, selected_products in zip(self.sub_product_ids, product.kit_product_ids):
+ total += (sub_product_data.unit_price * sub_product_data.quantity)
+
+ self.env['sale.order.line'].create({
+ 'order_id': order_line.order_id.id,
+ 'product_id': selected_products.id,
+ 'name': selected_products.product_tmpl_id.name,
+ 'price_unit': 0,
+ 'product_uom_qty': sub_product_data.quantity,
+ 'parent_product_id': self.product_id.id,
+ 'is_subproduct': True, # This part is correct
+ 'is_kit': False,
+ 'sequence': order_line.sequence
+ })
+
+ # Update the total price on the main line
+ order_line.price_unit = total + product.product_tmpl_id.list_price
diff --git a/product_kit/wizards/sub_product_wizard_line.py b/product_kit/wizards/sub_product_wizard_line.py
new file mode 100644
index 00000000000..05193411ba5
--- /dev/null
+++ b/product_kit/wizards/sub_product_wizard_line.py
@@ -0,0 +1,12 @@
+from odoo import fields, models
+
+
+class SubproductWizardLine(models.TransientModel):
+ _name = 'product_kit.subproduct.line'
+ _description = 'Sub-product Wizard Line'
+
+ subproduct_line_id = fields.Many2one('product_kit.subproduct.wizard')
+ product_id = fields.Many2one('product.product')
+ sub_product_name = fields.Char(string="Product Name")
+ quantity = fields.Integer(string="Quantity")
+ unit_price = fields.Float(string="Unit Price")
diff --git a/product_kit/wizards/sub_product_wizard_view.xml b/product_kit/wizards/sub_product_wizard_view.xml
new file mode 100755
index 00000000000..e855f1560e1
--- /dev/null
+++ b/product_kit/wizards/sub_product_wizard_view.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ Sub-Products-Wizard
+ product_kit.subproduct.wizard
+ form
+ new
+
+
+
+ Sub-Products-Wizard
+ product_kit.subproduct.wizard
+
+
+
+
+