Skip to content

Commit 8a71205

Browse files
Update applying price by update _is_applicable_for function in product_pricelist
1 parent ecf5679 commit 8a71205

File tree

6 files changed

+54
-127
lines changed

6 files changed

+54
-127
lines changed

sale_pricelist_global_rule/README.rst

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Sale pricelist global rule
33
==========================
44

5-
..
5+
..
66
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
77
!! This file is generated by oca-gen-addon-readme !!
88
!! changes will be overwritten. !!
@@ -64,21 +64,7 @@ Configuration
6464
- Create a new Pricelist and add at least one line with the Apply On
6565
option set to Global - Product template or Global - Product category
6666
- Choose the specific product template or category for the rule.
67-
- Set the computation mode and save.
68-
69-
**Global by Product Ancestor Category**
70-
71-
- In the pricelist line, set **Apply On** to *Global - Ancestor Product Category*.
72-
- Select the **Ancestor Product Category**; the rule will apply to any product
73-
whose category is a descendant of the selected ancestor.
74-
- Optionally set **Minimum Quantity**; this value is checked against the
75-
**cumulative quantities** of all products in the sales order that belong to
76-
any descendant category of the selected ancestor.
77-
- When multiple ancestor-category rules are eligible, the system will select
78-
the one with the **highest discount** (*Percent Price*) after considering
79-
the rule sequence (lowest sequence first).
80-
- Other fields (e.g., *Product*, *Product Category*) are not required and are
81-
cleared when this option is chosen.
67+
- Set the computation mode and save
8268

8369
Usage
8470
=====
@@ -90,17 +76,6 @@ Usage
9076
- Click the **Recompute pricelist global** button to update prices
9177
according to the specified pricelist rules.
9278

93-
**Global by Product Ancestor Category**
94-
95-
- Create or open a Sales Order and add lines for products from different
96-
subcategories under the same ancestor category.
97-
- Click **Compute pricelist global rule** on the order.
98-
- The module will evaluate applicable ancestor-category rules using the
99-
**cumulative quantity across descendant categories**. If the cumulative total
100-
reaches the *Minimum Quantity*, the rule is applied.
101-
- When multiple rules qualify, the system selects based on **sequence**
102-
(ascending) and then chooses the one with the **highest discount** (*Percent Price*).
103-
10479
Known issues / Roadmap
10580
======================
10681

@@ -133,11 +108,6 @@ Contributors
133108

134109
- Pedro M. Baeza
135110
- Carlos López
136-
- `Komit Company Limited <https://komit-consulting.com/>`__
137-
138-
- Jean-Charles Drubay
139-
- Truong Dinh Minh Duc
140-
- Vo Minh Bao Hieu
141111

142112
Maintainers
143113
-----------

sale_pricelist_global_rule/models/product_pricelist.py

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ 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-
(
13-
"3_3_global_product_ancestor_category",
14-
"Global - Ancestor Product Category",
15-
),
12+
("3_3_global_product_ancestor_category", "Global - Ancestor Product Category"),
1613
],
1714
ondelete={
1815
"3_1_global_product_template": "set default",
@@ -69,12 +66,10 @@ def _check_product_consistency(self):
6966
item.applied_on == "3_3_global_product_ancestor_category"
7067
and not item.ancestor_product_category_id
7168
):
72-
raise ValidationError(
73-
_(
74-
"Please specify the product ancestor category for which this "
75-
"global rule should be applied"
76-
)
77-
)
69+
raise ValidationError(_(
70+
"Please specify the product ancestor category for which this global"
71+
" rule should be applied"
72+
))
7873
return res
7974

8075
@api.depends(
@@ -113,8 +108,7 @@ def _compute_name_and_price(self):
113108
and item.applied_on == "3_3_global_product_ancestor_category"
114109
):
115110
item.name = _("Ancestor product category: %s") % (
116-
item.ancestor_product_category_id.display_name
117-
)
111+
item.ancestor_product_category_id.display_name)
118112
return res
119113

120114
@api.model_create_multi
@@ -144,15 +138,13 @@ def create(self, vals_list):
144138
}
145139
)
146140
elif applied_on == "3_3_global_product_ancestor_category":
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-
)
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+
})
156148
return super().create(vals_list)
157149

158150
def write(self, values):
@@ -204,36 +196,57 @@ def _is_applicable_for(self, product, qty_in_product_uom):
204196
:rtype: bool
205197
"""
206198
self.ensure_one()
207-
qty_data = self.env.context.get("pricelist_global_cummulative_quantity", {})
208-
if not qty_data or self.applied_on not in [
199+
qty_data = self.env.context.get("pricelist_global_cummulative_quantity", {}) or {}
200+
applied_on_vals = {
209201
"3_1_global_product_template",
210202
"3_2_global_product_category",
211203
"3_3_global_product_ancestor_category",
212-
]:
204+
}
205+
# If not one of the supported "global" applied_on values → fallback to super()
206+
if not qty_data or self.applied_on not in applied_on_vals:
213207
return super()._is_applicable_for(product, qty_in_product_uom)
214208

215209
is_applicable = True
210+
# Global Product Template
216211
if self.applied_on == "3_1_global_product_template":
217-
total_qty = qty_data["by_template"].get(product.product_tmpl_id, 0.0)
212+
total_qty = qty_data.get("by_template", {}).get(product.product_tmpl_id, 0.0)
218213
if self.min_quantity and total_qty < self.min_quantity:
219214
is_applicable = False
220215
elif self.global_product_tmpl_id != product.product_tmpl_id:
221216
is_applicable = False
217+
# Global Product Category
222218
elif self.applied_on == "3_2_global_product_category":
223-
total_qty = qty_data["by_categ"].get(product.categ_id, 0.0)
219+
total_qty = qty_data.get("by_categ", {}).get(product.categ_id, 0.0)
224220
if self.min_quantity and total_qty < self.min_quantity:
225221
is_applicable = False
226222
elif not product.categ_id.parent_path.startswith(
227223
self.global_categ_id.parent_path
228224
):
229225
is_applicable = False
230-
226+
# Global Product Ancestor Category
231227
elif self.applied_on == "3_3_global_product_ancestor_category":
232-
total_qty = qty_data.get("by_categ", {}).get(product.categ_id, 0.0)
228+
ancestor_categ = self.ancestor_product_category_id
229+
if not ancestor_categ:
230+
return False
231+
232+
# collect all ids in ancestor branch using child_of
233+
child_ids = self.env['product.category'].search([
234+
('id', 'child_of', ancestor_categ.id)
235+
])
236+
# product must belong to the branch
237+
prod_categ = product.categ_id
238+
if not prod_categ or prod_categ not in child_ids:
239+
return False
240+
241+
# compute total qty across all categories in this branch
242+
total_qty = 0.0
243+
by_categ = qty_data.get("by_categ", {})
244+
for categ_id, qty in by_categ.items():
245+
c_id = categ_id.id if hasattr(categ_id, "id") else int(categ_id)
246+
if c_id in child_ids:
247+
total_qty += qty
248+
233249
if self.min_quantity and total_qty < self.min_quantity:
234250
is_applicable = False
235-
elif not product.categ_id.parent_path.startswith(
236-
self.ancestor_product_category_id.parent_path
237-
):
238-
is_applicable = False
251+
239252
return is_applicable
Lines changed: 5 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,22 @@
1-
from odoo import api, models
1+
from odoo import models
22

33

44
class SaleOrderLine(models.Model):
55
_inherit = "sale.order.line"
66

7-
@api.depends("product_id", "product_uom", "product_uom_qty")
87
def _compute_pricelist_item_id(self):
9-
# Compute the applicable pricelist item for each sale order line.
10-
# For rules using "Global - Ancestor Product Category":
11-
# - Compute cumulative product quantities per category in the order.
12-
# - Evaluate whether total quantity of descendant products meets min_quantity.
13-
# - Select the valid rule based on sequence (ASC) and discount (DESC).
14-
# Sale order data is cached per order to avoid redundant calculations.
8+
# Compute the cumulative quantity of products in the sale order
9+
# for each line to ensure quantities are not mixed between different orders.
10+
# Store the data in a dictionary to avoid redundant computations
11+
# for the same order multiple times.
1512
sale_data = {}
1613
res = None
17-
product_categ_env = self.env["product.category"]
1814
for line in self:
1915
if line.order_id not in sale_data:
2016
sale_data[line.order_id] = line.order_id._get_cummulative_quantity()
2117
qty_data = sale_data[line.order_id]
22-
# First run the normal pricelist logic
2318
res = super(
2419
SaleOrderLine,
2520
line.with_context(pricelist_global_cummulative_quantity=qty_data),
2621
)._compute_pricelist_item_id()
27-
28-
# Check if there are any global-ancestor-category rules applicable
29-
valid_items = []
30-
pricelist_items = line.order_id.pricelist_id.item_ids.filtered(
31-
lambda rule: rule.applied_on == "3_3_global_product_ancestor_category"
32-
and rule.ancestor_product_category_id
33-
)
34-
35-
for rule in pricelist_items:
36-
ancestor_product_categ = rule.ancestor_product_category_id
37-
categories = set(
38-
product_categ_env.search(
39-
[("id", "child_of", ancestor_product_categ.id)]
40-
).ids
41-
)
42-
43-
if line.product_id.categ_id.id not in categories:
44-
continue
45-
46-
total_qty = sum(
47-
qty
48-
for categ, qty in qty_data["by_categ"].items()
49-
if categ.id in categories
50-
)
51-
52-
if total_qty >= rule.min_quantity:
53-
valid_items.append(rule)
54-
55-
# If there's any valid global-ancestor rule, compare it with the current one
56-
if valid_items:
57-
valid_items.sort(key=lambda r: -r.percent_price)
58-
best_rule = valid_items[0]
59-
# Overwrite only if it's a better match than the current rule has best
60-
# discount.
61-
if (
62-
not line.pricelist_item_id
63-
or line.pricelist_item_id.applied_on
64-
== "3_3_global_product_ancestor_category"
65-
or (
66-
best_rule.percent_price > line.pricelist_item_id.percent_price
67-
and best_rule.applied_on
68-
== "3_3_global_product_ancestor_category"
69-
)
70-
):
71-
line.pricelist_item_id = best_rule
7222
return res
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
- [Tecnativa](https://www.tecnativa.com)
22
- Pedro M. Baeza
33
- Carlos López
4-
- [Komit Company Limited](https://komit-consulting.com/)
5-
- Jean-Charles Drubay
6-
- Truong Dinh Minh Duc
7-
- Vo Minh Bao Hieu

sale_pricelist_global_rule/views/product_pricelist_item_views.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<odoo>
3-
43
<record id="view_product_pricelist_item_form" model="ir.ui.view">
54
<field name="name">view.product.pricelist.item.form</field>
65
<field name="model">product.pricelist.item</field>
@@ -25,5 +24,4 @@
2524
</xpath>
2625
</field>
2726
</record>
28-
2927
</odoo>

sale_pricelist_global_rule/views/sale_order_views.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
string="Recompute pricelist global"
1212
name="button_compute_pricelist_global_rule"
1313
type="object"
14-
attrs="{'invisible': [('state', 'not in', ['draft', 'sent']), ('has_pricelist_global', '=', False), ('need_recompute_pricelist_global', '=', False)]}"
14+
attrs="{'invisible': ['|', '|', ('state', 'not in', ['draft', 'sent']), ('has_pricelist_global', '=', False), ('need_recompute_pricelist_global', '=', False)]}"
1515
class="oe_highlight"
1616
/>
1717
<button
1818
string="Recompute pricelist global"
1919
name="button_compute_pricelist_global_rule"
2020
type="object"
21-
attrs="{'invisible': [('state', 'not in', ['draft', 'sent']), ('has_pricelist_global', '=', False), ('need_recompute_pricelist_global', '=', True)]}"
21+
attrs="{'invisible': ['|', '|', ('state', 'not in', ['draft', 'sent']), ('has_pricelist_global', '=', False), ('need_recompute_pricelist_global', '=', True)]}"
2222
/>
2323
<field name="has_pricelist_global" invisible="1" />
2424
<field name="need_recompute_pricelist_global" invisible="1" />

0 commit comments

Comments
 (0)