Skip to content

Commit 0f69a8b

Browse files
Fix pre-commit
1 parent bb386f4 commit 0f69a8b

6 files changed

Lines changed: 134 additions & 66 deletions

File tree

sale_pricelist_global_rule/README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ Contributors
140140
- Pedro M. Baeza
141141
- Carlos López
142142

143+
- `Komit-consulting <https://komit-consulting.com/>`__
144+
145+
- Truong Duc
146+
143147
Maintainers
144148
-----------
145149

sale_pricelist_global_rule/__manifest__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"name": "Sale pricelist global rule",
33
"version": "16.0.1.0.0",
44
"summary": "Apply a global rule to all sale order",
5-
"author": "Tecnativa, Odoo Community Association (OCA)",
5+
"author": "Tecnativa, Odoo Community Association (OCA),"
6+
"Truong Duc (Komit-consulting)",
67
"category": "Sales Management",
78
"website": "https://github.com/OCA/sale-workflow",
89
"depends": [

sale_pricelist_global_rule/models/product_pricelist.py

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ class ProductPricelistItem(models.Model):
99
selection_add=[
1010
("3_1_global_product_template", "Global - Product template"),
1111
("3_2_global_product_category", "Global - Product category"),
12-
("3_3_global_product_ancestor_category", "Global - Ancestor Product Category"),
12+
(
13+
"3_3_global_product_ancestor_category",
14+
"Global - Ancestor Product Category",
15+
),
1316
],
1417
ondelete={
1518
"3_1_global_product_template": "set default",
@@ -66,10 +69,12 @@ def _check_product_consistency(self):
6669
item.applied_on == "3_3_global_product_ancestor_category"
6770
and not item.ancestor_product_category_id
6871
):
69-
raise ValidationError(_(
70-
"Please specify the product ancestor category for which this global"
71-
" rule should be applied"
72-
))
72+
raise ValidationError(
73+
_(
74+
"Please specify the product ancestor category for which this global"
75+
" rule should be applied"
76+
)
77+
)
7378
return res
7479

7580
@api.depends(
@@ -108,7 +113,8 @@ def _compute_name_and_price(self):
108113
and item.applied_on == "3_3_global_product_ancestor_category"
109114
):
110115
item.name = _("Ancestor product category: %s") % (
111-
item.ancestor_product_category_id.display_name)
116+
item.ancestor_product_category_id.display_name
117+
)
112118
return res
113119

114120
@api.model_create_multi
@@ -138,13 +144,15 @@ def create(self, vals_list):
138144
}
139145
)
140146
elif applied_on == "3_3_global_product_ancestor_category":
141-
values.update({
142-
"product_id": None,
143-
"product_tmpl_id": None,
144-
"categ_id": None,
145-
"global_categ_id": None,
146-
"global_product_tmpl_id": None,
147-
})
147+
values.update(
148+
{
149+
"product_id": None,
150+
"product_tmpl_id": None,
151+
"categ_id": None,
152+
"global_categ_id": None,
153+
"global_product_tmpl_id": None,
154+
}
155+
)
148156
return super().create(vals_list)
149157

150158
def write(self, values):
@@ -196,7 +204,9 @@ def _is_applicable_for(self, product, qty_in_product_uom):
196204
:rtype: bool
197205
"""
198206
self.ensure_one()
199-
qty_data = self.env.context.get("pricelist_global_cummulative_quantity", {}) or {}
207+
qty_data = (
208+
self.env.context.get("pricelist_global_cummulative_quantity", {}) or {}
209+
)
200210
supported = {
201211
"3_1_global_product_template",
202212
"3_2_global_product_category",
@@ -222,6 +232,7 @@ def _is_applicable_for(self, product, qty_in_product_uom):
222232
self.global_categ_id.parent_path
223233
):
224234
is_applicable = False
235+
# Global Product Category
225236
elif self.applied_on == "3_3_global_product_ancestor_category":
226237
ancestor_categ = self.ancestor_product_category_id
227238
if not ancestor_categ:
@@ -262,38 +273,44 @@ def _is_applicable_for(self, product, qty_in_product_uom):
262273
# because it comes first. Therefore, we need the code below to check if the
263274
# current price item is already higher discount.
264275
# --------------------------------------------------------------
276+
if self._has_better_percentage_rule(product, qty_in_product_uom):
277+
is_applicable = False
278+
return is_applicable
279+
280+
def _has_better_percentage_rule(self, product, qty_in_product_uom):
281+
"""
282+
Check if there is another percentage rule that should take precedence.
283+
This prevents a weaker discount (lower percent_price) from overriding
284+
a stronger one when multiple percentage rules apply.
285+
"""
265286
if (
266-
self.compute_price == "percentage"
267-
and self.percent_price
268-
and not self.env.context.get("skip_best_percent_check")
287+
self.compute_price != "percentage"
288+
or not self.percent_price
289+
or self.env.context.get("skip_best_percent_check")
269290
):
270-
ctx = dict(self.env.context or {})
271-
# The context flag `skip_best_percent_check` is used to avoid
272-
# infinite recursion when we call `_is_applicable_for` on rivals.
273-
ctx["skip_best_percent_check"] = True
274-
# Check all percentage rules in the same pricelist
275-
items = self.pricelist_id.item_ids.with_context(ctx).filtered(
276-
lambda it: it.id != self.id
277-
and it.compute_price == "percentage"
278-
and it.percent_price not in (False, None)
279-
)
291+
return False
280292

281-
for it in items:
282-
# Ask each rival if it applies to THIS product & qty
283-
if not it._is_applicable_for(product, qty_in_product_uom):
284-
continue
293+
# Fetch all other percentage rules from the same pricelist
294+
# context skip_best_percent_check to avoid run forever
295+
items = self.pricelist_id.item_ids.with_context(
296+
skip_best_percent_check=True
297+
).filtered(
298+
lambda it: it.id != self.id
299+
and it.compute_price == "percentage"
300+
and it.percent_price not in (False, None)
301+
)
302+
for it in items:
303+
# Check if rival rule also applies to this product & quantity
304+
if not it._is_applicable_for(product, qty_in_product_uom):
305+
continue
285306

286-
# Among percentage rules that are all applicable:
287-
# 1. Higher percent_price is always preferred.
288-
# 2. If tied, the rule created earlier (smaller create_date)
289-
# wins.
290-
if (
291-
it.percent_price > self.percent_price
292-
or (
293-
it.percent_price == self.percent_price
294-
and it.create_date < self.create_date
295-
)
296-
):
297-
is_applicable = False
298-
break
299-
return is_applicable
307+
# Among applicable percentage rules:
308+
# 1. Prefer higher percent_price
309+
# 2. If tied, prefer older rule (smaller create_date)
310+
if it.percent_price > self.percent_price or (
311+
it.percent_price == self.percent_price
312+
and it.create_date < self.create_date
313+
):
314+
return True
315+
316+
return False
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
- [Tecnativa](https://www.tecnativa.com)
22
- Pedro M. Baeza
33
- Carlos López
4+
- Truong Duc (Komit-consulting)

sale_pricelist_global_rule/tests/test_pricelist_ancestor.py

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from odoo.addons.base.tests.common import TransactionCase
2-
from odoo import fields
32

43

54
class TestPricelistAncestor(TransactionCase):
@@ -56,8 +55,14 @@ def _create_pricelist(name):
5655
)
5756

5857
def _create_pricelist_item(
59-
pricelist, applied_on, compute_price, percent_price, min_quantity, name,
60-
ancestor_product_category_id=None, product_id=None
58+
pricelist,
59+
applied_on,
60+
compute_price,
61+
percent_price,
62+
min_quantity,
63+
name,
64+
ancestor_product_category_id=None,
65+
product_id=None,
6166
):
6267
vals = {
6368
"pricelist_id": pricelist.id,
@@ -77,52 +82,89 @@ def _create_pricelist_item(
7782
cls.pl_case1 = _create_pricelist("Price List 1")
7883
# PI0: Variant Prod A -> 30%
7984
cls.pi0 = _create_pricelist_item(
80-
cls.pl_case1, "0_product_variant", "percentage",
81-
30.0, 1.0, "PI0", product_id=cls.prod_a
85+
cls.pl_case1,
86+
"0_product_variant",
87+
"percentage",
88+
30.0,
89+
1.0,
90+
"PI0",
91+
product_id=cls.prod_a,
8292
)
8393
# PI1: Ancestor Cat C -> 20% (min 5)
8494
cls.pi1 = _create_pricelist_item(
85-
cls.pl_case1, "3_3_global_product_ancestor_category",
86-
"percentage", 20.0,5.0, "PI1", ancestor_product_category_id=cls.cat_c
95+
cls.pl_case1,
96+
"3_3_global_product_ancestor_category",
97+
"percentage",
98+
20.0,
99+
5.0,
100+
"PI1",
101+
ancestor_product_category_id=cls.cat_c,
87102
)
88103
# PI2: Ancestor Cat A -> 10% (min 5)
89104
cls.pi2 = _create_pricelist_item(
90-
cls.pl_case1, "3_3_global_product_ancestor_category", "percentage", 10.0,
91-
5.0, "PI2", ancestor_product_category_id=cls.cat_a
105+
cls.pl_case1,
106+
"3_3_global_product_ancestor_category",
107+
"percentage",
108+
10.0,
109+
5.0,
110+
"PI2",
111+
ancestor_product_category_id=cls.cat_a,
92112
)
93113

94114
# --- PRICELIST for Test Case 2
95115
cls.pl_case2 = _create_pricelist("Price List 2")
96116
# PI0: Ancestor Cat C -> 20%, seq 0
97117
cls.pi0p = _create_pricelist_item(
98-
cls.pl_case2, "3_3_global_product_ancestor_category",
99-
"percentage",20.0, 5.0, "PI0'", ancestor_product_category_id=cls.cat_c
118+
cls.pl_case2,
119+
"3_3_global_product_ancestor_category",
120+
"percentage",
121+
20.0,
122+
5.0,
123+
"PI0'",
124+
ancestor_product_category_id=cls.cat_c,
100125
)
101126
# PI1: Ancestor Cat A -> 10%, seq 1
102127
cls.pi1p = _create_pricelist_item(
103-
cls.pl_case2, "3_3_global_product_ancestor_category",
104-
"percentage",10.0, 5.0, "PI1'", ancestor_product_category_id=cls.cat_a
128+
cls.pl_case2,
129+
"3_3_global_product_ancestor_category",
130+
"percentage",
131+
10.0,
132+
5.0,
133+
"PI1'",
134+
ancestor_product_category_id=cls.cat_a,
105135
)
106136
# --- PRICELIST for Case 3 (tie on % and sequence; break by create_date)
107137
cls.pl_case3 = _create_pricelist("Price List 3")
108138
# Two rules on Cat A, both 15%, same sequence
109139
cls.pi_tie_old = _create_pricelist_item(
110-
cls.pl_case3, "3_3_global_product_ancestor_category",
111-
"percentage",15.0, 1.0, "Tie older", ancestor_product_category_id=cls.cat_a
140+
cls.pl_case3,
141+
"3_3_global_product_ancestor_category",
142+
"percentage",
143+
15.0,
144+
1.0,
145+
"Tie older",
146+
ancestor_product_category_id=cls.cat_a,
112147
)
113148
cls.pi_tie_new = _create_pricelist_item(
114-
cls.pl_case3, "3_3_global_product_ancestor_category",
115-
"percentage",15.0, 1.0, "Tie newer", ancestor_product_category_id=cls.cat_a
149+
cls.pl_case3,
150+
"3_3_global_product_ancestor_category",
151+
"percentage",
152+
15.0,
153+
1.0,
154+
"Tie newer",
155+
ancestor_product_category_id=cls.cat_a,
116156
)
117157
# Force create_date so that pi_tie_old is older than pi_tie_new
118158
# Note: write to create_date via SQL for deterministic ordering
119159
cr = env.cr
120160
cr.execute(
121-
"UPDATE product_pricelist_item SET create_date = NOW() - interval '2 days' WHERE id = %s",
161+
"UPDATE product_pricelist_item SET create_date = NOW() - interval '2 days' "
162+
"WHERE id = %s",
122163
(cls.pi_tie_old.id,),
123164
)
124165
cr.execute(
125-
"UPDATE product_pricelist_item SET create_date = NOW() - interval '1 day' WHERE id = %s",
166+
"UPDATE product_pricelist_item SET create_date = NOW() - interval '1 day' "
167+
"WHERE id = %s",
126168
(cls.pi_tie_new.id,),
127169
)
128170

sale_pricelist_global_rule/views/sale_order_views.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
<field name="has_pricelist_global" invisible="1" />
2424
<field name="need_recompute_pricelist_global" invisible="1" />
2525
</xpath>
26-
<xpath expr="//field[@name='order_line']/tree/field[@name='qty_invoiced']" position="after">
26+
<xpath
27+
expr="//field[@name='order_line']/tree/field[@name='qty_invoiced']"
28+
position="after"
29+
>
2730
<field name="pricelist_item_id" optional="hide" />
2831
</xpath>
2932
</field>

0 commit comments

Comments
 (0)