diff --git a/product_kits/__init__.py b/product_kits/__init__.py
new file mode 100644
index 0000000000..9b4296142f
--- /dev/null
+++ b/product_kits/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/product_kits/__manifest__.py b/product_kits/__manifest__.py
new file mode 100644
index 0000000000..66db49de79
--- /dev/null
+++ b/product_kits/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ 'name': 'Product Kits',
+ 'version': '1.0',
+ 'depends': ['base', 'sale_management'],
+ 'author': 'Aryan Donga (ardo)',
+ 'description': 'Simple module to create and sell product kits',
+ 'application': False,
+ 'installable': True,
+ 'license': 'LGPL-3',
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'report/sale_order_report.xml',
+ 'report/report_invoice.xml',
+ 'report/sale_order_portal_view.xml',
+ 'views/product_template_views.xml',
+ 'wizard/product_kit_wizard_views.xml',
+ 'views/sale_order_views.xml'
+ ],
+}
diff --git a/product_kits/models/__init__.py b/product_kits/models/__init__.py
new file mode 100644
index 0000000000..8f2f8c0cbc
--- /dev/null
+++ b/product_kits/models/__init__.py
@@ -0,0 +1,3 @@
+from . import product_template
+from . import sale_order_line
+from . import sale_order
diff --git a/product_kits/models/product_template.py b/product_kits/models/product_template.py
new file mode 100644
index 0000000000..faea924ab0
--- /dev/null
+++ b/product_kits/models/product_template.py
@@ -0,0 +1,8 @@
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ is_kit = fields.Boolean(string='Is Kit', default=False)
+ sub_product_ids = fields.Many2many('product.product', string='Sub Products')
diff --git a/product_kits/models/sale_order.py b/product_kits/models/sale_order.py
new file mode 100644
index 0000000000..393067814c
--- /dev/null
+++ b/product_kits/models/sale_order.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ print_in_report = fields.Boolean(string='Print in report')
diff --git a/product_kits/models/sale_order_line.py b/product_kits/models/sale_order_line.py
new file mode 100644
index 0000000000..03323caed7
--- /dev/null
+++ b/product_kits/models/sale_order_line.py
@@ -0,0 +1,30 @@
+from odoo import api, fields, models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = 'sale.order.line'
+
+ is_kit = fields.Boolean(compute='_compute_is_kit', store=True)
+ parent_kit_line_id = fields.Many2one(
+ comodel_name='sale.order.line',
+ string='Parent Kit Line',
+ ondelete='cascade',
+ index=True,
+ )
+
+ # Optional fields for sub-products
+ sub_product_line_ids = fields.One2many(
+ comodel_name='sale.order.line',
+ inverse_name='parent_kit_line_id',
+ string='Sub Product Lines',
+ copy=True,
+ )
+ custom_sub_product_price = fields.Float(
+ string='Custom Sub Product Price',
+ help='Overridden price of the sub product line.',
+ )
+
+ @api.depends('product_id')
+ def _compute_is_kit(self):
+ for line in self:
+ line.is_kit = line.product_id.is_kit if line.product_id else False
diff --git a/product_kits/report/report_invoice.xml b/product_kits/report/report_invoice.xml
new file mode 100644
index 0000000000..a17973fd97
--- /dev/null
+++ b/product_kits/report/report_invoice.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ (not any(line.sale_line_ids.mapped('parent_kit_line_id'))) or any(
+ line.sale_line_ids.mapped('order_id.print_in_report'))
+
+
+
+
diff --git a/product_kits/report/sale_order_portal_view.xml b/product_kits/report/sale_order_portal_view.xml
new file mode 100644
index 0000000000..5adb8b77e0
--- /dev/null
+++ b/product_kits/report/sale_order_portal_view.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ (not line.parent_kit_line_id) or sale_order.print_in_report
+
+
+
+
diff --git a/product_kits/report/sale_order_report.xml b/product_kits/report/sale_order_report.xml
new file mode 100644
index 0000000000..1268f13bf8
--- /dev/null
+++ b/product_kits/report/sale_order_report.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ (not line.parent_kit_line_id) or doc.print_in_report
+
+
+
+
diff --git a/product_kits/security/ir.model.access.csv b/product_kits/security/ir.model.access.csv
new file mode 100644
index 0000000000..df4195f38e
--- /dev/null
+++ b/product_kits/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
+product_kits.access_product_kit_wizard,access_product_kit_wizard,product_kits.model_product_kit_wizard,base.group_user,1,1,1,0
+product_kits.access_product_kit_wizard_line,access_product_kit_wizard_line,product_kits.model_product_kit_wizard_line,base.group_user,1,1,1,0
diff --git a/product_kits/views/product_template_views.xml b/product_kits/views/product_template_views.xml
new file mode 100644
index 0000000000..75e3b2fdbe
--- /dev/null
+++ b/product_kits/views/product_template_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ product.template.form.view.kits
+ product.template
+
+
+
+
+
+
+
+
+
diff --git a/product_kits/views/sale_order_views.xml b/product_kits/views/sale_order_views.xml
new file mode 100644
index 0000000000..0698ed1f5d
--- /dev/null
+++ b/product_kits/views/sale_order_views.xml
@@ -0,0 +1,33 @@
+
+
+
+ sale.order.form.view.kits
+ sale.order
+
+
+
+
+
+
+ parent_kit_line_id
+
+
+ parent_kit_line_id
+
+
+ parent_kit_line_id
+
+
+ parent_kit_line_id
+
+
+ parent_kit_line_id
+
+
+
+
+
+
+
diff --git a/product_kits/wizard/__init__.py b/product_kits/wizard/__init__.py
new file mode 100644
index 0000000000..b99680c2dd
--- /dev/null
+++ b/product_kits/wizard/__init__.py
@@ -0,0 +1,2 @@
+from . import product_kit_wizard
+from . import product_kit_wizard_line
diff --git a/product_kits/wizard/product_kit_wizard.py b/product_kits/wizard/product_kit_wizard.py
new file mode 100644
index 0000000000..4e66d64bb8
--- /dev/null
+++ b/product_kits/wizard/product_kit_wizard.py
@@ -0,0 +1,115 @@
+from odoo import Command, api, fields, models
+
+
+class ProductKitWizard(models.TransientModel):
+ _name = 'product.kit.wizard'
+ _description = 'Product Kit Wizard to configure sub-products'
+
+ product_name = fields.Char(readonly=True)
+ sale_order_line_id = fields.Many2one(
+ comodel_name='sale.order.line', string='Sale Order Line', required=True
+ )
+ wizard_line_ids = fields.One2many(
+ comodel_name='product.kit.wizard.line',
+ inverse_name='wizard_id',
+ string='Product Kit Wizard Lines',
+ required=True,
+ )
+
+ @api.depends('sale_order_line_id')
+ def _compute_product_name(self):
+ """
+ Compute the product name based on the sale order line.
+ """
+ for wizard in self:
+ if (
+ wizard.sale_order_line_id
+ and wizard.sale_order_line_id.product_template_id
+ ):
+ wizard.product_name = wizard.sale_order_line_id.product_template_id.name
+ else:
+ wizard.product_name = 'UNKNOWN PRODUCT'
+
+ @api.model
+ def default_get(self, field_ids):
+ """
+ Override default_get to set default values for the wizard.
+ Populates the wizard with existing values if available.
+ If no existing values are found, it initializes the wizard with 0 qty
+ """
+ result = super().default_get(field_ids)
+ if self.env.context.get(
+ 'active_model'
+ ) == 'sale.order.line' and self.env.context.get('active_id'):
+ so_line = self.env['sale.order.line'].browse(
+ self.env.context.get('active_id')
+ )
+ existing_sub_lines = self.env['sale.order.line'].search([
+ ('parent_kit_line_id', '=', so_line.id)
+ ])
+ sub_products = so_line.product_id.sub_product_ids
+
+ # Use the existing sub-product lines if available
+ # Otherwise, create new lines with 0 qty and default price
+ wizard_lines = []
+ for component in sub_products:
+ component_line = existing_sub_lines.filtered(
+ lambda x: x.product_id.id == component.id
+ )
+ if component_line:
+ wizard_lines.append({
+ 'product_id': component_line.product_id.id,
+ 'quantity': component_line.product_uom_qty,
+ 'price': component_line.custom_sub_product_price
+ or component.lst_price,
+ })
+ else:
+ wizard_lines.append({
+ 'product_id': component.id,
+ 'quantity': 0.0,
+ 'price': component.lst_price,
+ })
+
+ wiz_lines = self.env['product.kit.wizard.line'].create(wizard_lines)
+
+ result.update({
+ 'product_name': so_line.product_template_id.name,
+ 'sale_order_line_id': so_line.id,
+ 'wizard_line_ids': [Command.set(wiz_lines.ids)],
+ })
+ return result
+
+ def save_configuration(self):
+ self.ensure_one()
+ kit_so_line = self.sale_order_line_id
+ kit_price = 0
+ sub_so_lines = kit_so_line.sub_product_line_ids
+ current_sequence = kit_so_line.sequence
+
+ for line in self.wizard_line_ids:
+ sub_product_line = sub_so_lines.filtered(
+ lambda x: x.product_id.id == line.product_id.id
+ )
+ kit_price += line.price * line.quantity
+
+ # If the sub-product line already exists, update it
+ # Otherwise, create a new one
+ if not sub_product_line:
+ self.env['sale.order.line'].create({
+ 'order_id': kit_so_line.order_id.id,
+ 'parent_kit_line_id': kit_so_line.id,
+ 'product_id': line.product_id.id,
+ 'product_uom_qty': line.quantity,
+ 'price_unit': 0,
+ 'custom_sub_product_price': line.price,
+ 'sequence': current_sequence,
+ })
+ else:
+ sub_product_line.write({
+ 'product_uom_qty': line.quantity,
+ 'custom_sub_product_price': line.price,
+ 'price_unit': 0,
+ })
+
+ kit_so_line.write({'price_unit': kit_price})
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/product_kits/wizard/product_kit_wizard_line.py b/product_kits/wizard/product_kit_wizard_line.py
new file mode 100644
index 0000000000..fe9b7b7ef4
--- /dev/null
+++ b/product_kits/wizard/product_kit_wizard_line.py
@@ -0,0 +1,15 @@
+from odoo import fields, models
+
+
+class ProductKitWizardLine(models.TransientModel):
+ _name = 'product.kit.wizard.line'
+ _description = 'Key-Value Line for Product Kit Wizard'
+
+ wizard_id = fields.Many2one(
+ 'product.kit.wizard', string='Wizard', ondelete='cascade'
+ )
+ product_id = fields.Many2one(
+ 'product.product', string='Product', required=True, ondelete='cascade'
+ )
+ quantity = fields.Float(string='Quantity', required=True)
+ price = fields.Float(string='Price', required=True)
diff --git a/product_kits/wizard/product_kit_wizard_views.xml b/product_kits/wizard/product_kit_wizard_views.xml
new file mode 100644
index 0000000000..22b9b738df
--- /dev/null
+++ b/product_kits/wizard/product_kit_wizard_views.xml
@@ -0,0 +1,36 @@
+
+
+
+ Configure Sub Products
+ product.kit.wizard
+ form
+ new
+
+
+
+ product.kit.wizard.form
+ product.kit.wizard
+
+
+
+
+