Skip to content

Commit 17520f9

Browse files
committed
IMP from #159
1 parent 4eaf1d1 commit 17520f9

File tree

5 files changed

+248
-21
lines changed

5 files changed

+248
-21
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
22

3+
from . import models
34
from . import product_pack_line
45
from . import sale_order_line
56
from . import sale_order

sale_product_pack/models/models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2023 Foodles (http://www.foodles.co).
2+
# @author Pierre Verkest <pierreverkest84@gmail.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from collections import defaultdict
5+
6+
from odoo import models
7+
8+
9+
class BaseModel(models.AbstractModel):
10+
_inherit = "base"
11+
12+
def group_recordset_by(self, key):
13+
"""Return a collection of pairs ``(key, recordset)`` from ``self``. The
14+
``key`` is a function computing a key value for each element. This
15+
function is similar to ``itertools.groupby``, but aggregates all
16+
elements under the same key, not only consecutive elements.
17+
it's also similar to ``òdoo.tools.misc.groupby`` but return a recordset
18+
of sale.order.line instead list
19+
this let write some code likes this::
20+
my_recordset.filtered(
21+
lambda record: record.to_use
22+
).group_recordset_by(
23+
lambda record: record.group_key
24+
)
25+
"""
26+
groups = defaultdict(self.env[self._name].browse)
27+
for elem in self:
28+
groups[key(elem)] |= elem
29+
return groups.items()

sale_product_pack/models/sale_order.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,6 @@ def check_pack_line_unlink(self):
3838
)
3939
)
4040

41-
def write(self, vals):
42-
if "order_line" in vals:
43-
to_delete_ids = [e[1] for e in vals["order_line"] if e[0] == 2]
44-
subpacks_to_delete_ids = (
45-
self.env["sale.order.line"]
46-
.search(
47-
[("id", "child_of", to_delete_ids), ("id", "not in", to_delete_ids)]
48-
)
49-
.ids
50-
)
51-
if subpacks_to_delete_ids:
52-
for cmd in vals["order_line"]:
53-
if cmd[1] in subpacks_to_delete_ids:
54-
if cmd[0] != 2:
55-
cmd[0] = 2
56-
subpacks_to_delete_ids.remove(cmd[1])
57-
for to_delete_id in subpacks_to_delete_ids:
58-
vals["order_line"].append([2, to_delete_id, False])
59-
return super().write(vals)
60-
6141
def _get_update_prices_lines(self):
6242
res = super()._get_update_prices_lines()
6343
return res.filtered(

sale_product_pack/models/sale_order_line.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright 2019 Tecnativa - Ernesto Tejeda
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
34
from odoo import _, api, fields, models
45
from odoo.exceptions import UserError
56
from odoo.fields import first
@@ -93,9 +94,21 @@ def create(self, vals_list):
9394
else:
9495
return super().create(vals_list)
9596

97+
@api.model
98+
def _pack_fields_trigger_expand_pack_line_on_write(self):
99+
"""A set of fields that will trigger expand pack line
100+
To propagate information over sale order line add your
101+
field in this list and overload the following method:
102+
`ProductPack.get_sale_order_line_vals`
103+
Be aware if pack line are "modifiable" user input can
104+
be overwrite once save if one of this field as been
105+
changed on the pack line...
106+
"""
107+
return {"product_id", "product_uom_qty"}
108+
96109
def write(self, vals):
97110
res = super().write(vals)
98-
if "product_id" in vals or "product_uom_qty" in vals:
111+
if self._pack_fields_trigger_expand_pack_line_on_write() & set(vals.keys()):
99112
for record in self:
100113
record.expand_pack_line(write=True)
101114
return res
@@ -167,3 +180,16 @@ def _compute_discount(self):
167180
for pack_line in self.filtered("pack_parent_line_id"):
168181
pack_line.discount = pack_line._get_pack_line_discount()
169182
return res
183+
184+
185+
def unlink(self):
186+
for order, lines in self.group_recordset_by(lambda sol: sol.order_id):
187+
pack_component_to_delete = self.search(
188+
[
189+
("id", "child_of", lines.ids),
190+
("id", "not in", lines.ids),
191+
("order_id", "=", order.id),
192+
]
193+
)
194+
pack_component_to_delete.unlink()
195+
return super().unlink()

sale_product_pack/tests/test_sale_product_pack.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
33

44
from odoo.addons.base.tests.common import BaseCommon
5+
from odoo.exceptions import UserError
6+
from odoo.tests import Form
57

68

79
class TestSaleProductPack(BaseCommon):
@@ -221,6 +223,106 @@ def qty_in_order():
221223
total_qty_confirmed = qty_in_order()
222224
self.assertAlmostEqual(total_qty_updated * 2, total_qty_confirmed)
223225

226+
227+
def test_update_qty_do_not_expand(self):
228+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
229+
main_sol = self.env["sale.order.line"].create(
230+
{
231+
"order_id": self.sale_order.id,
232+
"name": product_cp.name,
233+
"product_id": product_cp.id,
234+
"product_uom_qty": 1,
235+
}
236+
)
237+
main_sol.with_context(update_prices=True).product_uom_qty = 2
238+
self.assertTrue(
239+
all(
240+
self.sale_order.order_line.filtered(
241+
lambda sol: sol.pack_parent_line_id == main_sol
242+
).mapped(lambda sol: sol.product_uom_qty == 1)
243+
),
244+
)
245+
246+
def test_update_pack_qty_with_new_component(self):
247+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
248+
main_sol = self.env["sale.order.line"].create(
249+
{
250+
"order_id": self.sale_order.id,
251+
"name": product_cp.name,
252+
"product_id": product_cp.id,
253+
"product_uom_qty": 1,
254+
}
255+
)
256+
257+
self.assertEqual(
258+
sum(
259+
self.sale_order.order_line.filtered(
260+
lambda sol: sol.pack_parent_line_id == main_sol
261+
).mapped("product_uom_qty")
262+
),
263+
3,
264+
"Expected 3 lines with quantity 1 while setup this test",
265+
)
266+
267+
product_cp.pack_line_ids |= self.env["product.pack.line"].create(
268+
{
269+
"parent_product_id": product_cp.id,
270+
"product_id": self.env.ref("product.product_product_12").id,
271+
"quantity": 2,
272+
}
273+
)
274+
275+
main_sol.product_uom_qty = 2
276+
self.assertEqual(
277+
sum(
278+
self.sale_order.order_line.filtered(
279+
lambda sol: sol.pack_parent_line_id == main_sol
280+
).mapped("product_uom_qty")
281+
),
282+
10,
283+
"Expected 3 lines with quantity 2 and new component line with quantity 4",
284+
)
285+
286+
def test_update_pack_qty_with_new_component_do_not_expand(self):
287+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
288+
main_sol = self.env["sale.order.line"].create(
289+
{
290+
"order_id": self.sale_order.id,
291+
"name": product_cp.name,
292+
"product_id": product_cp.id,
293+
"product_uom_qty": 1,
294+
}
295+
)
296+
297+
self.assertEqual(
298+
sum(
299+
self.sale_order.order_line.filtered(
300+
lambda sol: sol.pack_parent_line_id == main_sol
301+
).mapped("product_uom_qty")
302+
),
303+
3,
304+
"Expected 3 lines with quantity 1 while setup this test",
305+
)
306+
307+
product_cp.pack_line_ids |= self.env["product.pack.line"].create(
308+
{
309+
"parent_product_id": product_cp.id,
310+
"product_id": self.env.ref("product.product_product_12").id,
311+
"quantity": 2,
312+
}
313+
)
314+
315+
main_sol.with_context(update_prices=True).product_uom_qty = 2
316+
self.assertEqual(
317+
sum(
318+
self.sale_order.order_line.filtered(
319+
lambda sol: sol.pack_parent_line_id == main_sol
320+
).mapped("product_uom_qty")
321+
),
322+
3,
323+
"Expected 3 lines with quantity 2 and no new component line",
324+
)
325+
224326
def test_do_not_expand(self):
225327
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
226328
pack_line = self.env["sale.order.line"].create(
@@ -370,3 +472,92 @@ def test_compute_discount_for_detailed_packs(self):
370472
[19, 19, 10],
371473
"Discounts for the pack lines are not calculated correctly.",
372474
)
475+
476+
def test_copy_sale_order_with_detailed_product_pack(self):
477+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
478+
self.env["sale.order.line"].create(
479+
{
480+
"order_id": self.sale_order.id,
481+
"name": product_cp.name,
482+
"product_id": product_cp.id,
483+
"product_uom_qty": 1,
484+
}
485+
)
486+
copied_order = self.sale_order.copy()
487+
copied_order_component_lines_pack_line = copied_order.order_line.filtered(
488+
lambda line: line.product_id.pack_ok
489+
)
490+
copied_order_component_lines = copied_order.order_line.filtered(
491+
lambda line: line.pack_parent_line_id
492+
)
493+
self.assertEqual(
494+
copied_order_component_lines.pack_parent_line_id,
495+
copied_order_component_lines_pack_line,
496+
)
497+
498+
def test_check_pack_line_unlink(self):
499+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
500+
self.env["sale.order.line"].create(
501+
{
502+
"order_id": self.sale_order.id,
503+
"name": product_cp.name,
504+
"product_id": product_cp.id,
505+
"product_uom_qty": 1,
506+
}
507+
)
508+
with Form(self.sale_order) as so_form:
509+
with self.assertRaisesRegex(
510+
UserError,
511+
"You cannot delete this line because is part of a pack in this "
512+
"sale order. In order to delete this line you need to delete the "
513+
"pack itself",
514+
):
515+
so_form.order_line.remove(len(self.sale_order.order_line) - 1)
516+
517+
def test_unlink_pack_form_proxy(self):
518+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
519+
self.env["sale.order.line"].create(
520+
{
521+
"order_id": self.sale_order.id,
522+
"name": product_cp.name,
523+
"product_id": product_cp.id,
524+
"product_uom_qty": 1,
525+
}
526+
)
527+
with Form(self.sale_order) as so_form:
528+
so_form.order_line.remove(0)
529+
so_form.save()
530+
self.assertEqual(len(self.sale_order.order_line), 0)
531+
532+
def test_unlink_pack_record_unlink(self):
533+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
534+
self.env["sale.order.line"].create(
535+
{
536+
"order_id": self.sale_order.id,
537+
"name": product_cp.name,
538+
"product_id": product_cp.id,
539+
"product_uom_qty": 1,
540+
}
541+
)
542+
pack_line = self.sale_order.order_line.filtered(
543+
lambda line: line.product_id.pack_ok
544+
)
545+
pack_line.unlink()
546+
self.assertEqual(len(self.sale_order.order_line), 0)
547+
548+
def test_unlink_pack_old_style_like_ui(self):
549+
product_cp = self.env.ref("product_pack.product_pack_cpu_detailed_components")
550+
self.env["sale.order.line"].create(
551+
{
552+
"order_id": self.sale_order.id,
553+
"name": product_cp.name,
554+
"product_id": product_cp.id,
555+
"product_uom_qty": 1,
556+
}
557+
)
558+
pack_line = self.sale_order.order_line.filtered(
559+
lambda line: line.product_id.pack_ok
560+
)
561+
self.sale_order.write({"order_line": [(2, pack_line.id)]})
562+
self.assertEqual(len(self.sale_order.order_line), 0)
563+

0 commit comments

Comments
 (0)