From c7554ff0420f83b6035265f7dd138818771a3f5e Mon Sep 17 00:00:00 2001 From: kdes-odoo Date: Thu, 24 Jul 2025 12:46:07 +0530 Subject: [PATCH] [ADD] sale: added product as a kit feature Product Kit Definition: Products can now be designated as kits on the product form. This allows for defining a default set of components and quantities, which serve as a template to streamline the sales process. Dynamic Order Configuration: A Products button is now available on the sale order line. This launches a wizard, enabling salespersons to dynamically adjust component quantities and pricing for that specific order. Automated Pricing: The main kit's price on the order line is automatically recalculated to accurately reflect the total price of its configured components. This ensures pricing integrity and eliminates manual calculation errors. Document Presentation Control: A new visibility toggle on the sales order provides explicit control over the presentation of customer-facing documents. Users can choose to either display the full, itemized component list or show only the main consolidated kit product on quotations and invoices. Invoice Integration: The invoice creation process has been extended to honor the visibility setting from the sales order. This ensures the final invoice presentation is consistent with the user's choice, meeting specific customer requirements for billing clarity. task-4957772 --- product_kit/__init__.py | 2 + product_kit/__manifest__.py | 20 +++++ product_kit/models/__init__.py | 4 + product_kit/models/product_template.py | 9 ++ product_kit/models/sale_invoice.py | 7 ++ product_kit/models/sale_order.py | 54 +++++++++++ product_kit/models/sale_order_line.py | 40 +++++++++ product_kit/report/sale_order_report_view.xml | 8 ++ product_kit/security/ir.model.access.csv | 3 + product_kit/views/product_views.xml | 14 +++ product_kit/views/sale_order_view.xml | 33 +++++++ product_kit/views/sale_portal_view.xml | 8 ++ product_kit/wizards/__init__.py | 2 + product_kit/wizards/sub_product_wizard.py | 89 +++++++++++++++++++ .../wizards/sub_product_wizard_line.py | 12 +++ .../wizards/sub_product_wizard_view.xml | 33 +++++++ 16 files changed, 338 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/models/sale_invoice.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/report/sale_order_report_view.xml create mode 100755 product_kit/security/ir.model.access.csv create mode 100755 product_kit/views/product_views.xml create mode 100755 product_kit/views/sale_order_view.xml create mode 100644 product_kit/views/sale_portal_view.xml create mode 100644 product_kit/wizards/__init__.py create mode 100644 product_kit/wizards/sub_product_wizard.py create mode 100644 product_kit/wizards/sub_product_wizard_line.py create mode 100755 product_kit/wizards/sub_product_wizard_view.xml 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 @@ + + + + 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 + + + +