Skip to content

Commit bf30cba

Browse files
author
bosd
committed
[FIX] sale_restricted_qty: fix logic bugs and maximize test coverage
- Fix: Ensure float fields (min/max/multiple) reset to 0.0 instead of False. - Fix: Correct context key in inverse methods for inheritance. - Test: Add exhaustive deep coverage suite for all mixin paths. - Test: Fix test logic for model overrides and onchanges.
1 parent 699c246 commit bf30cba

File tree

4 files changed

+328
-98
lines changed

4 files changed

+328
-98
lines changed

sale_restricted_qty/models/product_restricted_qty_mixin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def _compute_sale_restrict_min_qty(self):
307307
)
308308

309309
def _inverse_sale_restrict_min_qty(self):
310-
for rec in self.with_context(sale_own_restrict_min_qty=True):
310+
for rec in self.with_context(skip_sale_own_restrict_min_qty=True):
311311
rec.is_sale_own_restrict_min_qty_set = True
312312
rec.sale_own_restrict_min_qty = rec.sale_restrict_min_qty
313313

@@ -357,7 +357,7 @@ def _inverse_sale_max_qty(self):
357357
rec.sale_own_max_qty = rec.sale_max_qty
358358
rec.is_sale_own_max_qty_set = True
359359
else:
360-
rec.sale_own_max_qty = False
360+
rec.sale_own_max_qty = 0.0
361361
rec.is_sale_own_max_qty_set = False
362362

363363
@api.onchange("is_sale_own_restrict_max_qty_set")
@@ -484,7 +484,7 @@ def _inverse_sale_multiple_of_qty(self):
484484
rec.sale_own_multiple_of_qty = rec.sale_multiple_of_qty
485485
rec.is_sale_own_multiple_of_qty_set = True
486486
else:
487-
rec.sale_own_multiple_of_qty = False
487+
rec.sale_own_multiple_of_qty = 0.0
488488
rec.is_sale_own_multiple_of_qty_set = False
489489

490490
@api.onchange("is_sale_own_restrict_multiple_of_qty_set")

sale_restricted_qty/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
test_product_product,
44
test_product_template,
55
test_sale_order_line,
6+
test_coverage_deep,
67
)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Copyright 2024 CorporateHub
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
4+
from odoo.tests import common, tagged
5+
6+
7+
@tagged("post_install", "-at_install")
8+
class TestCoverageDeep(common.TransactionCase):
9+
@classmethod
10+
def setUpClass(cls):
11+
super().setUpClass()
12+
cls.ProductCategory = cls.env["product.category"]
13+
cls.ProductTemplate = cls.env["product.template"]
14+
cls.Product = cls.env["product.product"]
15+
16+
def test_exhaustive_mixin_paths(self):
17+
"""Hit all branches in the mixin using a template."""
18+
template = self.ProductTemplate.create({"name": "Test Template"})
19+
20+
for field_prefix in ["min_qty", "max_qty", "multiple_of_qty"]:
21+
# 1. Test Value logic
22+
val_field = f"sale_{field_prefix}"
23+
own_val_field = f"sale_own_{field_prefix}"
24+
own_set_field = f"is_sale_own_{field_prefix}_set"
25+
onchange_val = f"_onchange_is_sale_{field_prefix}_set"
26+
27+
# Inverse: Set value
28+
setattr(template, val_field, 10.0)
29+
self.assertTrue(getattr(template, own_set_field))
30+
self.assertEqual(getattr(template, own_val_field), 10.0)
31+
32+
# Inverse: Reset to 0 (or inherited) -> unsets
33+
setattr(template, val_field, 0.0)
34+
self.assertFalse(getattr(template, own_set_field))
35+
self.assertEqual(getattr(template, own_val_field), 0.0)
36+
37+
# Onchange: Set
38+
setattr(template, own_set_field, True)
39+
getattr(template, onchange_val)()
40+
# Onchange: Unset
41+
setattr(template, own_set_field, False)
42+
getattr(template, onchange_val)()
43+
44+
# 2. Test Restrict logic
45+
restrict_field = f"sale_restrict_{field_prefix}"
46+
own_restrict_field = f"sale_own_restrict_{field_prefix}"
47+
own_restrict_set_field = f"is_sale_own_restrict_{field_prefix}_set"
48+
onchange_restrict = f"_onchange_is_sale_restrict_{field_prefix}_set"
49+
inverse_restrict_set = f"_inverse_is_sale_own_restrict_{field_prefix}_set"
50+
51+
# Inverse: Set restriction
52+
setattr(template, restrict_field, "1")
53+
self.assertTrue(getattr(template, own_restrict_set_field))
54+
self.assertEqual(getattr(template, own_restrict_field), "1")
55+
56+
# Test the boolean flag inverse explicitly
57+
setattr(template, own_restrict_set_field, True)
58+
getattr(template, inverse_restrict_set)()
59+
60+
setattr(template, own_restrict_set_field, False)
61+
getattr(template, inverse_restrict_set)()
62+
63+
# Onchange: Set
64+
setattr(template, own_restrict_set_field, True)
65+
getattr(template, onchange_restrict)()
66+
# Onchange: Unset
67+
setattr(template, own_restrict_set_field, False)
68+
getattr(template, onchange_restrict)()
69+
70+
def test_model_overrides_coverage(self):
71+
"""Hit the 12 compute methods in each model by changing hierarchy."""
72+
# 1. Category hierarchy
73+
parent = self.ProductCategory.create({"name": "Parent"})
74+
child = self.ProductCategory.create({"name": "Child", "parent_id": parent.id})
75+
76+
# Trigger all 12 computes on child by modifying parent
77+
parent.write(
78+
{
79+
"sale_min_qty": 1.0,
80+
"sale_restrict_min_qty": "1",
81+
"sale_max_qty": 2.0,
82+
"sale_restrict_max_qty": "1",
83+
"sale_multiple_of_qty": 3.0,
84+
"sale_restrict_multiple_of_qty": "1",
85+
}
86+
)
87+
self.assertEqual(child.sale_min_qty, 1.0)
88+
self.assertEqual(child.sale_max_qty, 2.0)
89+
self.assertEqual(child.sale_multiple_of_qty, 3.0)
90+
91+
# Test the "not parent_id" branch for all types (hits super())
92+
for pf in ["min", "max", "multiple_of"]:
93+
self.assertFalse(getattr(parent, f"is_sale_inherited_{pf}_qty_set"))
94+
self.assertEqual(getattr(parent, f"sale_inherited_{pf}_qty"), 0.0)
95+
96+
# 2. Template / Product variants
97+
template = self.ProductTemplate.create(
98+
{
99+
"name": "Template",
100+
"categ_id": child.id,
101+
}
102+
)
103+
product = template.product_variant_id
104+
105+
# Trigger computes by changing template
106+
template.write({"sale_min_qty": 5.0})
107+
self.assertEqual(product.sale_min_qty, 5.0)
108+
109+
# Clear parent values to stop inheritance (all fields)
110+
parent.write(
111+
{
112+
"sale_min_qty": 0.0,
113+
"sale_restrict_min_qty": "0",
114+
"sale_max_qty": 0.0,
115+
"sale_restrict_max_qty": "0",
116+
"sale_multiple_of_qty": 0.0,
117+
"sale_restrict_multiple_of_qty": "0",
118+
}
119+
)
120+
# Also clear the template's own value, otherwise product
121+
# still inherits from template
122+
template.write({"sale_min_qty": 0.0})
123+
template.invalidate_recordset()
124+
125+
# Test "no parent" cases for Template and Product for all types
126+
for pf in ["min", "max", "multiple_of"]:
127+
self.assertFalse(getattr(template, f"is_sale_inherited_{pf}_qty_set"))
128+
self.assertFalse(getattr(product, f"is_sale_inherited_{pf}_qty_set"))
129+
130+
# 3. Test edge case: no product_tmpl_id
131+
# (should not normally happen, but for coverage)
132+
product.product_tmpl_id = False
133+
for pf in ["min", "max", "multiple_of"]:
134+
self.assertFalse(getattr(product, f"is_sale_inherited_{pf}_qty_set"))
135+
self.assertEqual(getattr(product, f"sale_inherited_{pf}_qty"), 0.0)
136+
137+
def test_sale_order_line_onchanges_deep(self):
138+
"""Cover all branches of SO line onchanges."""
139+
product = self.Product.create(
140+
{
141+
"name": "Product",
142+
"sale_min_qty": 10.0,
143+
"sale_restrict_min_qty": "1",
144+
}
145+
)
146+
line = self.env["sale.order.line"].new({"product_id": product.id})
147+
148+
# Hits the "1.0" branch
149+
line.product_uom_qty = 1.0
150+
line._onchange_product_id_set_min_qty()
151+
self.assertEqual(line.product_uom_qty, 10.0)
152+
153+
# Hits the "0.0" branch
154+
line.product_uom_qty = 0.0
155+
line._onchange_product_id_set_min_qty()
156+
self.assertEqual(line.product_uom_qty, 10.0)
157+
158+
# Hits the "already set" branch (no overwrite)
159+
line.product_uom_qty = 5.0
160+
line._onchange_product_id_set_min_qty()
161+
self.assertEqual(line.product_uom_qty, 5.0)
162+
163+
# Hits the "not enforced" branch
164+
product.sale_restrict_min_qty = "0"
165+
166+
# New line to pick up the change
167+
line2 = self.env["sale.order.line"].new({"product_id": product.id})
168+
# Force recompute of line fields from product
169+
line2._compute_restricted_qty_from_product()
170+
171+
line2.product_uom_qty = 1.0
172+
line2._onchange_product_id_set_min_qty()
173+
self.assertEqual(line2.product_uom_qty, 1.0)
174+
175+
def test_onchange_no_product(self):
176+
"""Test onchange with no product set (coverage edge case)."""
177+
# Initialize with 0.0 to ensure it doesn't default to 1.0 (Odoo default)
178+
line = self.env["sale.order.line"].new({"product_uom_qty": 0.0})
179+
line._onchange_product_id_set_min_qty()
180+
# Should not crash and do nothing
181+
self.assertFalse(line.product_uom_qty)

0 commit comments

Comments
 (0)