diff --git a/sales_products_kit/__init__.py b/sales_products_kit/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/sales_products_kit/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sales_products_kit/__manifest__.py b/sales_products_kit/__manifest__.py new file mode 100644 index 00000000000..62826bbda9c --- /dev/null +++ b/sales_products_kit/__manifest__.py @@ -0,0 +1,21 @@ +{ + "name": "Sales - Product_Kit", + "version": "1.0", + "description": """ + This custom module adds a function to Odoo to sell products as a Kit, but not using a BOM or the Manufacturing Module. + """, + "category": "Sales/Sales", + "depends": ["sale_management"], + "data": [ + "security/ir.model.access.csv", + "wizard/sub_product_wizard.xml", + "views/product_view.xml", + "views/sale_order_view.xml", + "report/sale_order_report.xml", + "views/sale_portal_templates.xml", + "report/invoice_report.xml", + ], + "installable": True, + "auto_install": False, + "license": "LGPL-3", +} diff --git a/sales_products_kit/models/__init__.py b/sales_products_kit/models/__init__.py new file mode 100644 index 00000000000..8f2f8c0cbc1 --- /dev/null +++ b/sales_products_kit/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import sale_order_line +from . import sale_order diff --git a/sales_products_kit/models/product_template.py b/sales_products_kit/models/product_template.py new file mode 100644 index 00000000000..3051e74b5b8 --- /dev/null +++ b/sales_products_kit/models/product_template.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_kit = fields.Boolean() + sub_product_ids = fields.Many2many("product.product", string="Sub Product") diff --git a/sales_products_kit/models/sale_order.py b/sales_products_kit/models/sale_order.py new file mode 100644 index 00000000000..191177cae3c --- /dev/null +++ b/sales_products_kit/models/sale_order.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + print_in_report = fields.Boolean(string="Print in Report ?") diff --git a/sales_products_kit/models/sale_order_line.py b/sales_products_kit/models/sale_order_line.py new file mode 100644 index 00000000000..fd41a542790 --- /dev/null +++ b/sales_products_kit/models/sale_order_line.py @@ -0,0 +1,51 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + is_kit = fields.Boolean(related="product_template_id.is_kit") + parent_line_id = fields.Many2one("sale.order.line", ondelete="cascade") + + @api.ondelete(at_uninstall=False) + def _ondelete_sale_order_line(self): + if not self.parent_line_id: + for line in self: + sub_product_lines = self.env["sale.order.line"].search( + [("parent_line_id", "=", line.id)] + ) + sub_product_lines.with_context(allow_child_unlink=True).unlink() + else: + if not self.env.context.get("allow_child_unlink"): + raise UserError( + "You cannot delete a child line directly. Please delete the parent line instead." + ) + + def write(self, vals): + if "product_uom_qty" in vals and not self.parent_line_id: + for line in self: + sub_product_lines = self.env["sale.order.line"].search( + [("parent_line_id", "=", line.id)] + ) + old_qty = line.product_uom_qty + for sub_line in sub_product_lines: + if sub_line: + if old_qty != 0: + qty = sub_line.product_uom_qty / old_qty + new_qty = vals["product_uom_qty"] * qty + sub_line.update({"product_uom_qty": new_qty}) + else: + new_qty = vals["product_uom_qty"] * sub_line.product_uom_qty + sub_line.update({"product_uom_qty": new_qty}) + return super().write(vals) + + def action_subproduct(self): + return { + "type": "ir.actions.act_window", + "name": f"Product: {self.product_id.name}", + "res_model": "sub.product.wizard", + "view_mode": "form", + "target": "new", + "context": {"default_sale_order_line_id": self.id}, + } diff --git a/sales_products_kit/report/invoice_report.xml b/sales_products_kit/report/invoice_report.xml new file mode 100644 index 00000000000..60b58cc6ec5 --- /dev/null +++ b/sales_products_kit/report/invoice_report.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/sales_products_kit/report/sale_order_report.xml b/sales_products_kit/report/sale_order_report.xml new file mode 100644 index 00000000000..a3f242f5350 --- /dev/null +++ b/sales_products_kit/report/sale_order_report.xml @@ -0,0 +1,8 @@ + + + + diff --git a/sales_products_kit/security/ir.model.access.csv b/sales_products_kit/security/ir.model.access.csv new file mode 100644 index 00000000000..c57a11b5dbd --- /dev/null +++ b/sales_products_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_sub_product_wizard,access_sub_product_wizard,model_sub_product_wizard,base.group_user,1,1,1,1 +access_sub_product_line,access_sub_product_line,model_sub_product_line,base.group_user,1,1,1,1 diff --git a/sales_products_kit/tests/__init__.py b/sales_products_kit/tests/__init__.py new file mode 100644 index 00000000000..416fc1a5206 --- /dev/null +++ b/sales_products_kit/tests/__init__.py @@ -0,0 +1 @@ +from . import test_subproduct diff --git a/sales_products_kit/tests/test_subproduct.py b/sales_products_kit/tests/test_subproduct.py new file mode 100644 index 00000000000..bc3d0a8b600 --- /dev/null +++ b/sales_products_kit/tests/test_subproduct.py @@ -0,0 +1,78 @@ +from odoo.tests import TransactionCase + + +class TestSubProdcut(TransactionCase): + def setUp(self): + childproduct1 = self.env["product.template"].create({"name": "Child1"}) + childproduct2 = self.env["product.template"].create({"name": "Child2"}) + self.product1 = self.env["product.product"].search( + [("product_tmpl_id", "=", childproduct1.id)] + ) + self.product2 = self.env["product.product"].search( + [("product_tmpl_id", "=", childproduct2.id)] + ) + + parentproduct = self.env["product.template"].create( + { + "name": "parent", + "is_kit": True, + "sub_product_ids": [self.product1.id, self.product2.id], + } + ) + self.parent_product = self.env["product.product"].search( + [("product_tmpl_id", "=", parentproduct.id)] + ) + + sale_order = self.env["sale.order"].create( + { + "partner_id": self.env.ref("base.partner_demo").id, + } + ) + self.parentline = self.env["sale.order.line"].create( + { + "product_id": self.parent_product.id, + "order_id": sale_order.id, + "product_uom_qty": 1, + "name": self.parent_product.name, + } + ) + self.childline1 = self.env["sale.order.line"].create( + { + "product_template_id": self.product1.id, + "order_id": sale_order.id, + "product_uom_qty": 3, + "parent_line_id": self.parentline.id, + "name": self.product1.name, + } + ) + self.childline2 = self.env["sale.order.line"].create( + { + "product_template_id": self.product2.id, + "order_id": sale_order.id, + "product_uom_qty": 2, + "parent_line_id": self.parentline.id, + "name": self.product2.name, + } + ) + + def test_parent_quantity_update_child(self): + old_parent_qty = self.parentline.product_uom_qty + old_child_qty1 = self.childline1.product_uom_qty + old_child_qty2 = self.childline2.product_uom_qty + + new_parent_qty = 4 + self.parentline.write({"product_uom_qty": new_parent_qty}) + + new_child_qty1 = (old_child_qty1 / old_parent_qty) * new_parent_qty + new_child_qty2 = (old_child_qty2 / old_parent_qty) * new_parent_qty + + self.assertEqual( + self.childline1.product_uom_qty, + new_child_qty1, + f"expected {new_child_qty1}, but got {self.childline1.product_uom_qty}", + ) + self.assertEqual( + self.childline2.product_uom_qty, + new_child_qty2, + f"expected {new_child_qty2}, but got {self.childline2.product_uom_qty}", + ) diff --git a/sales_products_kit/views/product_view.xml b/sales_products_kit/views/product_view.xml new file mode 100644 index 00000000000..9addc0aa6ad --- /dev/null +++ b/sales_products_kit/views/product_view.xml @@ -0,0 +1,15 @@ + + + + + product.template.form.view.inherit.sales_products_kit + product.template + + + + + + + + + diff --git a/sales_products_kit/views/sale_order_view.xml b/sales_products_kit/views/sale_order_view.xml new file mode 100644 index 00000000000..548dfa53d1d --- /dev/null +++ b/sales_products_kit/views/sale_order_view.xml @@ -0,0 +1,45 @@ + + + + + sale.order.form.inherit + sale.order + + + + + + +