diff --git a/sale_kit_management/__init__.py b/sale_kit_management/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/sale_kit_management/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sale_kit_management/__manifest__.py b/sale_kit_management/__manifest__.py new file mode 100644 index 00000000000..37ba4179aea --- /dev/null +++ b/sale_kit_management/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Sale Kit Management', + 'version': "1.0", + 'category': "Sales/Sales", + 'description': "Creates a Kit Option in Products and Sales Order", + 'depends': ['sale_management', 'stock'], + 'data': [ + 'security/ir.model.access.csv', + 'wizard/sale_order_kit_views.xml', + 'views/product_views.xml', + 'views/sale_order_views.xml', + ], + 'installable': True, + 'author': "times", + 'license': "LGPL-3", +} diff --git a/sale_kit_management/models/__init__.py b/sale_kit_management/models/__init__.py new file mode 100644 index 00000000000..28136695f4f --- /dev/null +++ b/sale_kit_management/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_template +from . import product_subproduct_line +from . import sale_order_line +from . import sale_order_kit_config_line diff --git a/sale_kit_management/models/product_subproduct_line.py b/sale_kit_management/models/product_subproduct_line.py new file mode 100644 index 00000000000..b86924f1b72 --- /dev/null +++ b/sale_kit_management/models/product_subproduct_line.py @@ -0,0 +1,49 @@ +from odoo import api, fields, models + + +class ProductSubproductLine(models.Model): + _name = 'product.subproduct.line' + _description = "Subproducts" + + product_tmpl_id = fields.Many2one('product.template', ondelete='cascade') + product_id = fields.Many2one('product.product', string="Product", required=True) + product_uom_qty = fields.Float(related='product_id.qty_available', string="Quantity on Hand") + kit_unit_qty = fields.Float(string="Kit Unit Qty", default=1.0) + price_unit = fields.Float(string="Unit Price", compute='_compute_price_unit', store=True, readonly=False) + amount = fields.Float(string="Amount", compute='_compute_amount', store=True) + + @api.depends('kit_unit_qty', 'price_unit') + def _compute_amount(self): + for line in self: + line.amount = line.kit_unit_qty * line.price_unit + + @api.depends('product_id') + def _compute_price_unit(self): + for line in self: + if line.product_id: + line.price_unit = line.product_id.lst_price + else: + line.price_unit = 0.0 + + @api.model_create_multi + def create(self, vals_list): + lines = super().create(vals_list) + lines.mapped('product_tmpl_id')._recompute_kit_list_price() + return lines + + def write(self, vals): + res = super().write(vals) + if 'price_unit' in vals or 'kit_unit_qty' in vals or 'product_id' in vals: + self.mapped('product_tmpl_id')._recompute_kit_list_price() + return res + + @api.ondelete(at_uninstall=False) + def _ondelete_recompute_kit_price(self): + for tmpl in self.mapped('product_tmpl_id'): + if tmpl.is_kit: + remaining = tmpl.subproduct_ids - self + if remaining: + tmpl.list_price = sum( + sub.price_unit * sub.kit_unit_qty + for sub in remaining + ) diff --git a/sale_kit_management/models/product_template.py b/sale_kit_management/models/product_template.py new file mode 100644 index 00000000000..7804b92e147 --- /dev/null +++ b/sale_kit_management/models/product_template.py @@ -0,0 +1,24 @@ +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + is_kit = fields.Boolean() + subproduct_ids = fields.One2many('product.subproduct.line', 'product_tmpl_id') + + def _recompute_kit_list_price(self): + for tmpl in self: + if tmpl.is_kit and tmpl.subproduct_ids: + tmpl.list_price = sum( + sub.price_unit * sub.kit_unit_qty + for sub in tmpl.subproduct_ids + ) + + @api.onchange('subproduct_ids') + def _onchange_subproduct_ids(self): + if self.is_kit and self.subproduct_ids: + self.list_price = sum( + sub.price_unit * sub.kit_unit_qty + for sub in self.subproduct_ids + ) diff --git a/sale_kit_management/models/sale_order_kit_config_line.py b/sale_kit_management/models/sale_order_kit_config_line.py new file mode 100644 index 00000000000..9d7e74271df --- /dev/null +++ b/sale_kit_management/models/sale_order_kit_config_line.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class SaleOrderKitConfigLine(models.Model): + _name = 'sale.order.kit.config.line' + _description = "Kit Configuration Line" + + sale_order_line_id = fields.Many2one('sale.order.line', string="Kit Parent Line", ondelete='cascade', required=True, index=True) + product_id = fields.Many2one('product.product', string="Product", required=True) + kit_unit_qty = fields.Float(string="Kit Unit Qty", default=1.0) + price_unit = fields.Float(string="Unit Price") diff --git a/sale_kit_management/models/sale_order_line.py b/sale_kit_management/models/sale_order_line.py new file mode 100644 index 00000000000..4f0e74385ff --- /dev/null +++ b/sale_kit_management/models/sale_order_line.py @@ -0,0 +1,94 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + _KIT_SUBPRODUCT_SEQ_BASE = 200 + + is_kit = fields.Boolean(related='product_id.product_tmpl_id.is_kit') + is_kit_subproduct = fields.Boolean(string="Is Kit Subproduct", default=False) + kit_unit_qty = fields.Float(string="Kit Unit Qty") + kit_parent_line_id = fields.Many2one('sale.order.line', string="Kit Parent Line", ondelete='cascade', index=True) + has_kit_subproducts = fields.Boolean(compute='_compute_has_kit_subproducts') + kit_config_line_ids = fields.One2many('sale.order.kit.config.line', 'sale_order_line_id', string="Kit Configuration") + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('is_kit_subproduct') or vals.get('kit_parent_line_id'): + continue + seq = vals.get('sequence', 0) + if seq >= self._KIT_SUBPRODUCT_SEQ_BASE: + vals['sequence'] = self._KIT_SUBPRODUCT_SEQ_BASE - 1 + return super().create(vals_list) + + @api.depends('order_id.order_line.kit_parent_line_id') + def _compute_has_kit_subproducts(self): + for line in self: + if line.is_kit and line.id: + line.has_kit_subproducts = any( + l.kit_parent_line_id.id == line.id + for l in line.order_id.order_line + if l.is_kit_subproduct + ) + else: + line.has_kit_subproducts = False + + def _compute_kit_price(self): + for line in self: + if line.is_kit and line.kit_config_line_ids: + line.price_unit = sum( + cl.price_unit * cl.product_uom_qty + for cl in line.kit_config_line_ids + ) + + def write(self, vals): + result = super().write(vals) + if 'product_uom_qty' in vals: + for line in self.filtered(lambda l: l.is_kit and l.id): + child_lines = self.env['sale.order.line'].search([ + ('kit_parent_line_id', '=', line.id), + ('is_kit_subproduct', '=', True), + ]) + for child in child_lines: + if child.kit_unit_qty: + child.product_uom_qty = line.product_uom_qty * child.kit_unit_qty + child.price_unit = 0 + return result + + @api.ondelete(at_uninstall=False) + def _ondelete_handle_kit(self): + for line in self.filtered(lambda l: l.is_kit_subproduct): + if line.kit_parent_line_id in self: + continue + section_in_batch = any( + l.display_type == 'line_section' + and l.kit_parent_line_id.id == line.kit_parent_line_id.id + for l in self + ) + if section_in_batch: + continue + raise UserError(_( + "You cannot delete subproduct lines directly. " + "Delete the parent kit product or delete the entire subproducts section." + )) + + child_lines = self.env['sale.order.line'].search([ + ('kit_parent_line_id', 'in', self.ids), + ('id', 'not in', self.ids), + ]) + if child_lines: + child_lines.unlink() + + def action_open_kit_configurator(self): + self.ensure_one() + return { + 'name': _("Kit"), + 'type': 'ir.actions.act_window', + 'res_model': 'sale.order.kit', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_sale_order_line_id': self.id}, + } diff --git a/sale_kit_management/security/ir.model.access.csv b/sale_kit_management/security/ir.model.access.csv new file mode 100644 index 00000000000..9f4e0029266 --- /dev/null +++ b/sale_kit_management/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_subproduct_line,access_product_subproduct_line,model_product_subproduct_line,sales_team.group_sale_salesman,1,1,1,1 +access_sale_order_kit,access_sale_order_kit,model_sale_order_kit,sales_team.group_sale_salesman,1,1,1,1 +access_sale_order_kit_line,access_sale_order_kit_line,model_sale_order_kit_line,sales_team.group_sale_salesman,1,1,1,1 +access_sale_order_kit_config_line,access_sale_order_kit_config_line,model_sale_order_kit_config_line,sales_team.group_sale_salesman,1,1,1,1 + diff --git a/sale_kit_management/views/product_views.xml b/sale_kit_management/views/product_views.xml new file mode 100644 index 00000000000..1619169f9b2 --- /dev/null +++ b/sale_kit_management/views/product_views.xml @@ -0,0 +1,33 @@ + + + + product.template.form.kit + product.template + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sale_kit_management/views/sale_order_views.xml b/sale_kit_management/views/sale_order_views.xml new file mode 100644 index 00000000000..5c2d0ac5ccf --- /dev/null +++ b/sale_kit_management/views/sale_order_views.xml @@ -0,0 +1,68 @@ + + + + sale.order.form.kit + sale.order + + + + +