From fd5bdf151c230d69fff2d25289c93fc620f22622 Mon Sep 17 00:00:00 2001 From: sris-odoo Date: Mon, 14 Apr 2025 12:52:45 +0530 Subject: [PATCH 1/2] [ADD] product_kit: Add option to set as kit In this commit - Added boolean field `isKit` to mark a product as a kit - Introduced a Many2many field to assign sub-products to the kit - Sub-product field is conditionally visible only when `isKit` is enabled --- product_kit/__init__.py | 1 + product_kit/__manifest__.py | 16 ++++++++++++++++ product_kit/models/__init__.py | 1 + product_kit/models/product_template.py | 16 ++++++++++++++++ product_kit/views/product_views.xml | 15 +++++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 product_kit/__init__.py create mode 100644 product_kit/__manifest__.py create mode 100644 product_kit/models/__init__.py create mode 100644 product_kit/models/product_template.py create mode 100644 product_kit/views/product_views.xml diff --git a/product_kit/__init__.py b/product_kit/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/product_kit/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_kit/__manifest__.py b/product_kit/__manifest__.py new file mode 100644 index 00000000000..36ea661cdc0 --- /dev/null +++ b/product_kit/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': "Product As Kit", + 'version': '1.0', + 'depends': ['product', 'sale_management'], + 'author': "Rishav Shah", + 'category': 'product', + 'description': """ + Add new new product type as kit + """, + 'installable': True, + 'application': True, + 'license': 'LGPL-3', + 'data': [ + 'views/product_views.xml', + ], +} diff --git a/product_kit/models/__init__.py b/product_kit/models/__init__.py new file mode 100644 index 00000000000..e8fa8f6bf1e --- /dev/null +++ b/product_kit/models/__init__.py @@ -0,0 +1 @@ +from . import product_template diff --git a/product_kit/models/product_template.py b/product_kit/models/product_template.py new file mode 100644 index 00000000000..727a9928f52 --- /dev/null +++ b/product_kit/models/product_template.py @@ -0,0 +1,16 @@ +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + isKit = fields.Boolean(string="is Kit", default=False) + subProduct = fields.Many2many('product.product', string="Sub Products") + subProductVisibility = fields.Boolean(string="is Kit", default=False, compute='_compute_sub_product_visiblity') + + @api.depends_context('isKit') + def _compute_sub_product_visiblity(self): + if self.isKit: + self.subProductVisibility = True + else: + self.subProductVisibility = False diff --git a/product_kit/views/product_views.xml b/product_kit/views/product_views.xml new file mode 100644 index 00000000000..d4b5bcd94ab --- /dev/null +++ b/product_kit/views/product_views.xml @@ -0,0 +1,15 @@ + + + + product.template.form.view.inherit + product.template + + + + + + + + + + From 9659ebbbc0f8292d500597b3a8dc45f355ef07a9 Mon Sep 17 00:00:00 2001 From: sris-odoo Date: Tue, 15 Apr 2025 17:08:21 +0530 Subject: [PATCH 2/2] [ADD] product_kit: kit product wizard and subproduct handling In this commit - Added a button for kit-type products in the sale order line - Button opens a popup wizard displaying all related sub-products - Button is invisible if the product is unmarked as a kit or SO is confirmed - Wizard fields are editable for quantity and price adjustments - Confirming the wizard adds sub-products as separate sale order lines - Automatically calculates the main product's price based on sub-products - Sub-product sale order lines have their price set to 0 - Added a "Print in Report" boolean field on the sale order - If enabled, sub-product info is displayed in invoice report and preview page - When the main kit product is deleted, related sub-product lines are deleted --- product_kit/__manifest__.py | 7 +- product_kit/models/__init__.py | 3 + product_kit/models/kit_products_wizard.py | 105 ++++++++++++++++++ product_kit/models/product_template.py | 2 +- product_kit/models/sale_order.py | 7 ++ product_kit/models/sale_order_line.py | 36 ++++++ product_kit/security/ir.model.access.csv | 3 + product_kit/views/invoice_report.xml | 10 ++ .../views/kit_products_wizard_views.xml | 39 +++++++ product_kit/views/product_views.xml | 3 +- product_kit/views/sale_order_report.xml | 17 +++ product_kit/views/sale_order_views.xml | 26 +++++ 12 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 product_kit/models/kit_products_wizard.py create mode 100644 product_kit/models/sale_order.py create mode 100644 product_kit/models/sale_order_line.py create mode 100644 product_kit/security/ir.model.access.csv create mode 100644 product_kit/views/invoice_report.xml create mode 100644 product_kit/views/kit_products_wizard_views.xml create mode 100644 product_kit/views/sale_order_report.xml create mode 100644 product_kit/views/sale_order_views.xml diff --git a/product_kit/__manifest__.py b/product_kit/__manifest__.py index 36ea661cdc0..40e69e7a32c 100644 --- a/product_kit/__manifest__.py +++ b/product_kit/__manifest__.py @@ -1,7 +1,7 @@ { 'name': "Product As Kit", 'version': '1.0', - 'depends': ['product', 'sale_management'], + 'depends': ['stock', 'sale_management'], 'author': "Rishav Shah", 'category': 'product', 'description': """ @@ -11,6 +11,11 @@ 'application': True, 'license': 'LGPL-3', 'data': [ + 'security/ir.model.access.csv', 'views/product_views.xml', + 'views/sale_order_views.xml', + 'views/kit_products_wizard_views.xml', + 'views/sale_order_report.xml', + 'views/invoice_report.xml', ], } diff --git a/product_kit/models/__init__.py b/product_kit/models/__init__.py index e8fa8f6bf1e..14dd0c6fcdb 100644 --- a/product_kit/models/__init__.py +++ b/product_kit/models/__init__.py @@ -1 +1,4 @@ from . import product_template +from . import sale_order_line +from . import sale_order +from . import kit_products_wizard diff --git a/product_kit/models/kit_products_wizard.py b/product_kit/models/kit_products_wizard.py new file mode 100644 index 00000000000..9a2e4490279 --- /dev/null +++ b/product_kit/models/kit_products_wizard.py @@ -0,0 +1,105 @@ +from odoo import models, fields, api + + +class KitProductsWizardLine(models.TransientModel): + _name = "kit.products.wizard.line" + _description = "Kit Products Wizard Line" + + wizard_line_id = fields.Many2one("kit.products.wizard", required=True) + product_id = fields.Many2one("product.product", string="Product") + product_quantity = fields.Float(string="Quantity") + price = fields.Float(string="Unit Price") + + +class KitProductsWizard(models.TransientModel): + _name = "kit.products.wizard" + _description = "Kit Products Wizard" + + sale_order_id = fields.Many2one("sale.order", string="Sale Order", readonly=True) + product_template_id = fields.Many2one("product.template", string="Product", readonly=True) + subProduct_ids = fields.One2many("kit.products.wizard.line", "wizard_line_id", string="Sub Products") + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + sale_order_id = self.env.context.get("default_sale_order_id") + product_template_id = self.env.context.get("default_product_template_id") + + res.update({ + "sale_order_id": sale_order_id, + "product_template_id": product_template_id, + }) + + if sale_order_id and product_template_id: + product_template = self.env["product.template"].browse(product_template_id) + sub_products = product_template.subProduct_ids + + existing_lines = self.env["sale.order.line"].search([ + ("order_id", "=", sale_order_id), + ("product_id", "in", sub_products.ids) + ]) + + line_data = [] + for sub_product in sub_products: + existing_line = existing_lines.filtered(lambda l: l.product_id.id == sub_product.id) + line_data.append((0, 0, { + "product_id": sub_product.id, + "product_quantity": existing_line.product_uom_qty if existing_line else 1, + "price": existing_line.last_updated_price if existing_line else sub_product.list_price, + })) + + res["subProduct_ids"] = line_data + + return res + + def action_open_wizard_popup(self): + self.ensure_one() + + so_line_model = self.env["sale.order.line"] + total_price = 0.0 + + for sub in self.subProduct_ids: + sub_total = sub.product_quantity * sub.price + total_price += sub_total + + line_vals = { + "product_uom_qty": sub.product_quantity, + "price_unit": 0.0, + "last_updated_price": sub.price, + } + + existing_line = so_line_model.search([ + ("order_id", "=", self.sale_order_id.id), + ("product_id", "=", sub.product_id.id), + ("is_subProduct", "=", True), + ], limit=1) + + if existing_line: + existing_line.write(line_vals) + else: + line_vals.update({ + "name": sub.product_id.name, + "order_id": self.sale_order_id.id, + "product_id": sub.product_id.id, + "is_subProduct": True, + }) + so_line_model.create(line_vals) + + main_product = self.product_template_id.product_variant_id + main_line = so_line_model.search([ + ("order_id", "=", self.sale_order_id.id), + ("product_id", "=", main_product.id), + ], limit=1) + + if main_line: + main_line.price_unit = total_price * main_line.product_uom_qty + else: + so_line_model.create({ + "order_id": self.sale_order_id.id, + "product_id": main_product.id, + "product_uom_qty": 1, + "price_unit": total_price, + "name": self.product_template_id.name, + }) + + return {"type": "ir.actions.act_window_close"} diff --git a/product_kit/models/product_template.py b/product_kit/models/product_template.py index 727a9928f52..197b64cbc6c 100644 --- a/product_kit/models/product_template.py +++ b/product_kit/models/product_template.py @@ -5,7 +5,7 @@ class ProductTemplate(models.Model): _inherit = 'product.template' isKit = fields.Boolean(string="is Kit", default=False) - subProduct = fields.Many2many('product.product', string="Sub Products") + subProduct_ids = fields.Many2many('product.product', string="Sub Products") subProductVisibility = fields.Boolean(string="is Kit", default=False, compute='_compute_sub_product_visiblity') @api.depends_context('isKit') diff --git a/product_kit/models/sale_order.py b/product_kit/models/sale_order.py new file mode 100644 index 00000000000..116ac78e7b4 --- /dev/null +++ b/product_kit/models/sale_order.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + is_kit_printable = fields.Boolean(string="Print in report ?", default=True) diff --git a/product_kit/models/sale_order_line.py b/product_kit/models/sale_order_line.py new file mode 100644 index 00000000000..48ac89658e3 --- /dev/null +++ b/product_kit/models/sale_order_line.py @@ -0,0 +1,36 @@ +from odoo import models, fields + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + isKit = fields.Boolean(related="product_template_id.isKit", store=True) + is_subProduct = fields.Boolean(string="Created from Wizard", default=False) + last_updated_price = fields.Float("Kit Price", help="Custom price stored for use in kit wizard") + + def action_open_kit_wizard(self): + return { + "name": "Kit Products", + "type": "ir.actions.act_window", + "res_model": "kit.products.wizard", + "view_mode": "form", + "target": "new", + "context": { + "default_sale_order_id": self.order_id.id, + "default_product_template_id": self.product_template_id.id, + } + } + + def unlink(self): + """When main product line is deleted, all the related wizard-generated sale order lines are deleted.""" + main_product_line = self.filtered(lambda line: not line.is_subProduct) + + for sub_product_lines in main_product_line: + kit_lines = self.search([ + ("order_id", "=", sub_product_lines.order_id.id), + ("is_subProduct", "=", True), + ("product_id", "in", sub_product_lines.product_id.subProduct_ids.ids), + ]) + kit_lines.unlink() + + return super().unlink() diff --git a/product_kit/security/ir.model.access.csv b/product_kit/security/ir.model.access.csv new file mode 100644 index 00000000000..d1323fe661e --- /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 +product_kit.access_kit_products_wizard_line,access_kit_products_wizard_line,product_kit.model_kit_products_wizard_line,base.group_user,1,1,1,1 +product_kit.access_kit_products_wizard,access_kit_products_wizard,product_kit.model_kit_products_wizard,base.group_user,1,1,1,1 diff --git a/product_kit/views/invoice_report.xml b/product_kit/views/invoice_report.xml new file mode 100644 index 00000000000..b09e5cd1d5a --- /dev/null +++ b/product_kit/views/invoice_report.xml @@ -0,0 +1,10 @@ + + + + diff --git a/product_kit/views/kit_products_wizard_views.xml b/product_kit/views/kit_products_wizard_views.xml new file mode 100644 index 00000000000..c2bb57747e8 --- /dev/null +++ b/product_kit/views/kit_products_wizard_views.xml @@ -0,0 +1,39 @@ + + + + Kit Product + kit.products.wizard + form + new + + + view_kit_products_wizard + kit.products.wizard + +
+ + +

PRODUCT

+

+
+ +

SubProducts

+
+ + + + + + + + + +
+
+
+
+
+
+
diff --git a/product_kit/views/product_views.xml b/product_kit/views/product_views.xml index d4b5bcd94ab..3f1c23bcd88 100644 --- a/product_kit/views/product_views.xml +++ b/product_kit/views/product_views.xml @@ -7,9 +7,8 @@ - + - diff --git a/product_kit/views/sale_order_report.xml b/product_kit/views/sale_order_report.xml new file mode 100644 index 00000000000..f39fb587f23 --- /dev/null +++ b/product_kit/views/sale_order_report.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/product_kit/views/sale_order_views.xml b/product_kit/views/sale_order_views.xml new file mode 100644 index 00000000000..faebc76e630 --- /dev/null +++ b/product_kit/views/sale_order_views.xml @@ -0,0 +1,26 @@ + + + + sale.order.form.view.inherit + sale.order + + + + + + +