diff --git a/sale_product_pack/models/__init__.py b/sale_product_pack/models/__init__.py index cd9997169..ce88d17aa 100644 --- a/sale_product_pack/models/__init__.py +++ b/sale_product_pack/models/__init__.py @@ -1,5 +1,6 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models from . import product_pack_line from . import sale_order_line from . import sale_order diff --git a/sale_product_pack/models/models.py b/sale_product_pack/models/models.py new file mode 100644 index 000000000..48e54fd64 --- /dev/null +++ b/sale_product_pack/models/models.py @@ -0,0 +1,32 @@ +# Copyright 2023 Foodles (http://www.foodles.co). +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from collections import defaultdict + +from odoo import models + + +class BaseModel(models.AbstractModel): + _inherit = "base" + + def group_recordset_by(self, key): + """Return a collection of pairs ``(key, recordset)`` from ``self``. The + ``key`` is a function computing a key value for each element. This + function is similar to ``itertools.groupby``, but aggregates all + elements under the same key, not only consecutive elements. + + it's also similar to ``òdoo.tools.misc.groupby`` but return a recordset + of sale.order.line instead list + + this let write some code likes this:: + + my_recordset.filtered( + lambda record: record.to_use + ).group_recordset_by( + lambda record: record.group_key + ) + """ + groups = defaultdict(self.env[self._name].browse) + for elem in self: + groups[key(elem)] |= elem + return groups.items() diff --git a/sale_product_pack/models/sale_order.py b/sale_product_pack/models/sale_order.py index a37d1dda0..a0c6b437b 100644 --- a/sale_product_pack/models/sale_order.py +++ b/sale_product_pack/models/sale_order.py @@ -37,23 +37,3 @@ def check_pack_line_unlink(self): " delete the pack itself" ) ) - - def write(self, vals): - if "order_line" in vals: - to_delete_ids = [e[1] for e in vals["order_line"] if e[0] == 2] - subpacks_to_delete_ids = ( - self.env["sale.order.line"] - .search( - [("id", "child_of", to_delete_ids), ("id", "not in", to_delete_ids)] - ) - .ids - ) - if subpacks_to_delete_ids: - for cmd in vals["order_line"]: - if cmd[1] in subpacks_to_delete_ids: - if cmd[0] != 2: - cmd[0] = 2 - subpacks_to_delete_ids.remove(cmd[1]) - for to_delete_id in subpacks_to_delete_ids: - vals["order_line"].append([2, to_delete_id, False]) - return super().write(vals) diff --git a/sale_product_pack/models/sale_order_line.py b/sale_product_pack/models/sale_order_line.py index b6a6b0728..99c9b4761 100644 --- a/sale_product_pack/models/sale_order_line.py +++ b/sale_product_pack/models/sale_order_line.py @@ -1,5 +1,6 @@ # Copyright 2019 Tecnativa - Ernesto Tejeda # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.fields import first @@ -83,9 +84,23 @@ def create(self, vals): record.expand_pack_line() return record + @api.model + def _pack_fields_trigger_expand_pack_line_on_write(self): + """A set of fields that will trigger expand pack line + + To propagate information over sale order line add your + field in this list and overload the following method: + `ProductPack.get_sale_order_line_vals` + + Be aware if pack line are "modifiable" user input can + be overwrite once save if one of this field as been + changed on the pack line... + """ + return {"product_id", "product_uom_qty"} + def write(self, vals): res = super().write(vals) - if "product_id" in vals or "product_uom_qty" in vals: + if self._pack_fields_trigger_expand_pack_line_on_write() & set(vals.keys()): for record in self: record.expand_pack_line(write=True) return res @@ -121,3 +136,15 @@ def action_open_parent_pack_product_view(self): "view_mode": "tree,form", "domain": domain, } + + def unlink(self): + for order, lines in self.group_recordset_by(lambda sol: sol.order_id): + pack_component_to_delete = self.search( + [ + ("id", "child_of", lines.ids), + ("id", "not in", lines.ids), + ("order_id", "=", order.id), + ] + ) + pack_component_to_delete.unlink() + return super().unlink() diff --git a/sale_product_pack/tests/test_sale_product_pack.py b/sale_product_pack/tests/test_sale_product_pack.py index 21440838f..2d62da29d 100644 --- a/sale_product_pack/tests/test_sale_product_pack.py +++ b/sale_product_pack/tests/test_sale_product_pack.py @@ -1,7 +1,8 @@ # Copyright 2019 Tecnativa - Ernesto Tejeda # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo.tests import SavepointCase +from odoo.exceptions import UserError +from odoo.tests import Form, SavepointCase class TestSaleProductPack(SavepointCase): @@ -155,6 +156,7 @@ def qty_in_order(): } ) total_qty_init = qty_in_order() + # change qty of main sol main_sol.product_uom_qty = 2 * main_sol.product_uom_qty total_qty_updated = qty_in_order() @@ -169,6 +171,105 @@ def qty_in_order(): total_qty_confirmed = qty_in_order() self.assertAlmostEqual(total_qty_updated * 2, total_qty_confirmed) + def test_update_qty_do_not_expand(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + main_sol = self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + main_sol.with_context(update_prices=True).product_uom_qty = 2 + self.assertTrue( + all( + self.sale_order.order_line.filtered( + lambda sol: sol.pack_parent_line_id == main_sol + ).mapped(lambda sol: sol.product_uom_qty == 1) + ), + ) + + def test_update_pack_qty_with_new_component(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + main_sol = self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + + self.assertEqual( + sum( + self.sale_order.order_line.filtered( + lambda sol: sol.pack_parent_line_id == main_sol + ).mapped("product_uom_qty") + ), + 3, + "Expected 3 lines with quantity 1 while setup this test", + ) + + product_cp.pack_line_ids |= self.env["product.pack.line"].create( + { + "parent_product_id": product_cp.id, + "product_id": self.env.ref("product.product_product_12").id, + "quantity": 2, + } + ) + + main_sol.product_uom_qty = 2 + self.assertEqual( + sum( + self.sale_order.order_line.filtered( + lambda sol: sol.pack_parent_line_id == main_sol + ).mapped("product_uom_qty") + ), + 10, + "Expected 3 lines with quantity 2 and new component line with quantity 4", + ) + + def test_update_pack_qty_with_new_component_do_not_expand(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + main_sol = self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + + self.assertEqual( + sum( + self.sale_order.order_line.filtered( + lambda sol: sol.pack_parent_line_id == main_sol + ).mapped("product_uom_qty") + ), + 3, + "Expected 3 lines with quantity 1 while setup this test", + ) + + product_cp.pack_line_ids |= self.env["product.pack.line"].create( + { + "parent_product_id": product_cp.id, + "product_id": self.env.ref("product.product_product_12").id, + "quantity": 2, + } + ) + + main_sol.with_context(update_prices=True).product_uom_qty = 2 + self.assertEqual( + sum( + self.sale_order.order_line.filtered( + lambda sol: sol.pack_parent_line_id == main_sol + ).mapped("product_uom_qty") + ), + 3, + "Expected 3 lines with quantity 2 and no new component line", + ) + def test_do_not_expand(self): product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") pack_line = self.env["sale.order.line"].create( @@ -221,3 +322,91 @@ def test_create_several_lines(self): self.assertEqual(sequence_tp, self.sale_order.order_line[5].sequence) self.assertEqual(sequence_tp, self.sale_order.order_line[6].sequence) self.assertEqual(sequence_tp, self.sale_order.order_line[7].sequence) + + def test_copy_sale_order_with_detailed_product_pack(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + copied_order = self.sale_order.copy() + copied_order_component_lines_pack_line = copied_order.order_line.filtered( + lambda line: line.product_id.pack_ok + ) + copied_order_component_lines = copied_order.order_line.filtered( + lambda line: line.pack_parent_line_id + ) + self.assertEqual( + copied_order_component_lines.pack_parent_line_id, + copied_order_component_lines_pack_line, + ) + + def test_check_pack_line_unlink(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + with Form(self.sale_order) as so_form: + with self.assertRaisesRegex( + UserError, + "You cannot delete this line because is part of a pack in this " + "sale order. In order to delete this line you need to delete the " + "pack itself", + ): + so_form.order_line.remove(len(self.sale_order.order_line) - 1) + + def test_unlink_pack_form_proxy(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + with Form(self.sale_order) as so_form: + so_form.order_line.remove(0) + so_form.save() + self.assertEqual(len(self.sale_order.order_line), 0) + + def test_unlink_pack_record_unlink(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + pack_line = self.sale_order.order_line.filtered( + lambda line: line.product_id.pack_ok + ) + pack_line.unlink() + self.assertEqual(len(self.sale_order.order_line), 0) + + def test_unlink_pack_old_style_like_ui(self): + product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components") + self.env["sale.order.line"].create( + { + "order_id": self.sale_order.id, + "name": product_cp.name, + "product_id": product_cp.id, + "product_uom_qty": 1, + } + ) + pack_line = self.sale_order.order_line.filtered( + lambda line: line.product_id.pack_ok + ) + self.sale_order.write({"order_line": [(2, pack_line.id)]}) + self.assertEqual(len(self.sale_order.order_line), 0)