Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][MIG] sale_pricelist_global_rule #3533

Open
wants to merge 10 commits into
base: 16.0
Choose a base branch
from
10 changes: 5 additions & 5 deletions sale_pricelist_global_rule/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ Sale pricelist global rule
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/sale-workflow/tree/15.0/sale_pricelist_global_rule
:target: https://github.com/OCA/sale-workflow/tree/16.0/sale_pricelist_global_rule
:alt: OCA/sale-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/sale-workflow-15-0/sale-workflow-15-0-sale_pricelist_global_rule
:target: https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_pricelist_global_rule
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=15.0
:target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|
Expand Down Expand Up @@ -80,7 +80,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_pricelist_global_rule%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_pricelist_global_rule%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Expand Down Expand Up @@ -113,6 +113,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/15.0/sale_pricelist_global_rule>`_ project on GitHub.
This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/16.0/sale_pricelist_global_rule>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 1 addition & 1 deletion sale_pricelist_global_rule/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Sale pricelist global rule",
"version": "15.0.1.0.3",
"version": "16.0.1.0.0",
"summary": "Apply a global rule to all sale order",
"author": "Tecnativa, Odoo Community Association (OCA)",
"category": "Sales Management",
Expand Down
86 changes: 66 additions & 20 deletions sale_pricelist_global_rule/models/product_pricelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,51 @@
from odoo.exceptions import ValidationError


def get_family_category(category):
subcategories = category.child_id
all_subcategories = subcategories
for subcategory in subcategories:
all_subcategories |= get_family_category(subcategory)

Check warning on line 9 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L9

Added line #L9 was not covered by tests
return all_subcategories


class ProductPricelist(models.Model):
_inherit = "product.pricelist"

def _compute_price_rule_get_items(
self, products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids
):
items = super()._compute_price_rule_get_items(
products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids
self.ensure_one()

Check warning on line 19 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L19

Added line #L19 was not covered by tests
# Load all rules
self.env["product.pricelist.item"].flush(

Check warning on line 21 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L21

Added line #L21 was not covered by tests
["price", "currency_id", "company_id", "active"]
)
# ignore new global rules on Odoo standard
self.env.cr.execute(

Check warning on line 24 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L24

Added line #L24 was not covered by tests
"""
SELECT
item.id
FROM
product_pricelist_item AS item
LEFT JOIN product_category AS categ ON item.categ_id = categ.id
WHERE
(item.product_tmpl_id IS NULL OR item.product_tmpl_id = any(%s))
AND (item.product_id IS NULL OR item.product_id = any(%s))
AND (item.categ_id IS NULL OR item.categ_id = any(%s))
AND (item.pricelist_id = %s)
AND (item.date_start IS NULL OR item.date_start<=%s)
AND (item.date_end IS NULL OR item.date_end>=%s)
AND (item.active = TRUE)
ORDER BY
item.applied_on, item.min_quantity desc, categ.complete_name desc, item.id desc
""",
(prod_tmpl_ids, prod_ids, categ_ids, self.id, date, date),
)
# NOTE: if you change `order by` on that query, make sure it matches
# _order from model to avoid inconstencies and undeterministic issues.

item_ids = [x[0] for x in self.env.cr.fetchall()]
items = self.env["product.pricelist.item"].browse(item_ids)
Comment on lines +24 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you re implementing the whole thing, and ignoring the recursion, or am I missing something?

return items.filtered(

Check warning on line 49 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L47-L49

Added lines #L47 - L49 were not covered by tests
lambda item: item.applied_on
not in ["4_global_product_template", "5_global_product_category"]
)
Expand Down Expand Up @@ -82,6 +116,7 @@
"by_template": {},
"by_categ": {},
}

for line in sale.order_line.filtered(lambda x: not x.display_type):
qty_in_product_uom = line.product_uom_qty
# Final unit price is computed according to `qty` in the default `uom_id`.
Expand Down Expand Up @@ -111,10 +146,10 @@
# need to call price_compute.
price = product.price_compute("list_price")[product.id]

price_uom = product.uom_id
for rule in items:
if not rule._is_applicable_for_sale(product.product_tmpl_id, qty_data):
if not rule._is_applicable_for_sale(product, qty_data):
continue

if rule.base == "pricelist" and rule.base_pricelist_id:
# first, try compute the price for global rule
# otherwise, fallback to regular computation
Expand All @@ -125,9 +160,7 @@
) = rule.base_pricelist_id._compute_price_rule_global(sale)[line.id]
if not rule_applied:
price = rule.base_pricelist_id._compute_price_rule(
[(product, line.product_uom_qty, sale.partner_id)],
date,
line.product_uom.id,
product, line.product_uom_qty
)[product.id][0]
src_currency = rule.base_pricelist_id.currency_id
else:
Expand All @@ -135,17 +168,22 @@
# price_compute returns the price in the context UoM, i.e. qty_uom_id
price = product.price_compute(rule.base)[product.id]
if rule.base == "standard_price":
src_currency = product.cost_currency_id

Check warning on line 171 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L171

Added line #L171 was not covered by tests
else:
src_currency = product.currency_id

if src_currency != self.currency_id:
price = src_currency._convert(

Check warning on line 176 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L176

Added line #L176 was not covered by tests
price, self.currency_id, self.env.company, date, round=False
)

if price is not False:
price = rule._compute_price(price, price_uom, product)
price = rule._compute_price(
product,
line.product_uom_qty,
product.uom_id,
date,
self.currency_id,
)
suitable_rule = rule
break

Expand Down Expand Up @@ -199,7 +237,7 @@
item.applied_on == "5_global_product_category"
and not item.global_categ_id
):
raise ValidationError(

Check warning on line 240 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L240

Added line #L240 was not covered by tests
_(
"Please specify the category "
"for which this global rule should be applied"
Expand All @@ -209,7 +247,7 @@
item.applied_on == "4_global_product_template"
and not item.global_product_tmpl_id
):
raise ValidationError(

Check warning on line 250 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L250

Added line #L250 was not covered by tests
_(
"Please specify the product "
"for which this global rule should be applied"
Expand All @@ -231,18 +269,18 @@
"price_discount",
"price_surcharge",
)
def _get_pricelist_item_name_price(self):
res = super()._get_pricelist_item_name_price()
def _compute_name_and_price(self):
res = super()._compute_name_and_price()
for item in self:
if item.global_categ_id and item.applied_on == "5_global_product_category":
item.name = _("Global category: %s") % (

Check warning on line 276 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L276

Added line #L276 was not covered by tests
item.global_categ_id.display_name
)
elif (
item.global_product_tmpl_id
and item.applied_on == "4_global_product_template"
):
item.name = _("Global product: %s") % (

Check warning on line 283 in sale_pricelist_global_rule/models/product_pricelist.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/product_pricelist.py#L283

Added line #L283 was not covered by tests
item.global_product_tmpl_id.display_name
)
return res
Expand Down Expand Up @@ -297,7 +335,7 @@
)
return super().write(values)

def _is_applicable_for_sale(self, product_template, qty_data):
def _is_applicable_for_sale(self, product, qty_data):
"""Check whether the current rule is valid
for the given sale order and cummulated quantity.
:param product_template: browse_record(product.template)
Expand All @@ -312,17 +350,25 @@
self.ensure_one()
is_applicable = True
if self.applied_on == "4_global_product_template":
total_qty = qty_data["by_template"].get(product_template, 0.0)
total_qty = qty_data["by_template"].get(product.product_tmpl_id, 0.0)
if self.min_quantity and total_qty < self.min_quantity:
is_applicable = False
elif self.global_product_tmpl_id != product_template:
elif self.global_product_tmpl_id != product.product_tmpl_id:
is_applicable = False
elif self.applied_on == "5_global_product_category":
total_qty = qty_data["by_categ"].get(product_template.categ_id, 0.0)
if self.min_quantity and total_qty < self.min_quantity:
is_applicable = False
elif not product_template.categ_id.parent_path.startswith(
self.global_categ_id.parent_path
rule_family_categories = get_family_category(self.global_categ_id).ids
rule_family_categories.append(self.global_categ_id.id)

total_qty = 0
for categ in qty_data["by_categ"]:
if categ.id in rule_family_categories:
total_qty += qty_data["by_categ"].get(categ, 0.0)

if (
self.min_quantity
and total_qty < self.min_quantity
or product.categ_id.id not in rule_family_categories
):
is_applicable = False

return is_applicable
16 changes: 8 additions & 8 deletions sale_pricelist_global_rule/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import math

from odoo import api, fields, models
from odoo.tools import float_compare

Expand Down Expand Up @@ -47,6 +49,7 @@
fiscal_position=self.env.context.get("fiscal_position"),
)
price, suitable_rule = prices_data[line.id]
price = math.trunc(price * 100) / 100
if is_discount_visible:
product_context = dict(
self.env.context,
Expand All @@ -55,26 +58,22 @@
uom=line.product_uom.id,
)

base_price, currency = line.with_context(
currency = self.pricelist_id.currency_id
base_price = line.with_context(
**product_context
)._get_real_price_currency(
product,
suitable_rule,
line.product_uom_qty,
line.product_uom,
self.pricelist_id.id,
)
).product_id.uom_id._compute_price(product.lst_price, line.product_uom)
if base_price != 0:
if self.pricelist_id.currency_id != currency:
# we need new_list_price in the same currency as price,
# which is in the SO's pricelist's currency
base_price = currency._convert(

Check warning on line 69 in sale_pricelist_global_rule/models/sale_order.py

View check run for this annotation

Codecov / codecov/patch

sale_pricelist_global_rule/models/sale_order.py#L69

Added line #L69 was not covered by tests
base_price,
self.pricelist_id.currency_id,
self.company_id or self.env.company,
self.date_order or fields.Date.context_today(self),
)
discount = (base_price - price) / base_price * 100

if (discount > 0 and base_price > 0) or (
discount < 0 and base_price < 0
):
Expand All @@ -84,5 +83,6 @@
if float_compare(price, line.price_unit, precision_digits=digits) != 0:
vals_to_write["price_unit"] = price
if vals_to_write:

line.write(vals_to_write)
self.need_recompute_pricelist_global = False
6 changes: 3 additions & 3 deletions sale_pricelist_global_rule/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ <h1 class="title">Sale pricelist global rule</h1>
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:b93829a100ce8864f9f399ba2bf2c998544d9a7b00d3555405045eaebe3f4cd4
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/15.0/sale_pricelist_global_rule"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-15-0/sale-workflow-15-0-sale_pricelist_global_rule"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&amp;target_branch=15.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/16.0/sale_pricelist_global_rule"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_pricelist_global_rule"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows configured pricelists to be applied to a sales order by considering cumulative quantities across all lines.</p>
<p><strong>Global by Product Template</strong></p>
<p>If a pricelist rule has a <cite>min_quantity = 15</cite>, and a sales order contains:</p>
Expand Down Expand Up @@ -430,7 +430,7 @@ <h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-workflow/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_pricelist_global_rule%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_pricelist_global_rule%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
Expand Down Expand Up @@ -460,7 +460,7 @@ <h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/15.0/sale_pricelist_global_rule">OCA/sale-workflow</a> project on GitHub.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/16.0/sale_pricelist_global_rule">OCA/sale-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
Expand Down
16 changes: 8 additions & 8 deletions sale_pricelist_global_rule/tests/test_pricelist_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,14 +683,14 @@ def test_pricelist_visible_discount(self):
- Based on other pricelist
- Global pricelist discount policy: without_discount
- base pricelist discount_policy: with_discount
- product_m_red: price=80, discount=10
- product_m_black: price=80, discount=10
- product_m_red: price=100, discount=28
- product_m_black: price=100, discount=28
Case 5:
- Based on other pricelist
- Global pricelist discount policy: with_discount
- base pricelist discount_policy: without_discount
- product_m_red: price=80, discount=10
- product_m_black: price=80, discount=10
- product_m_red: price=72, discount=0
- product_m_black: price=72, discount=0
Case 6:
- Based on other pricelist
- Global pricelist discount policy: without_discount
Expand Down Expand Up @@ -744,10 +744,10 @@ def test_pricelist_visible_discount(self):
self.pricelist_global.write({"discount_policy": "without_discount"})
self.pricelist_base.write({"discount_policy": "with_discount"})
self.sale_order1.button_compute_pricelist_global_rule()
self.assertEqual(self.sale_line_m_red.price_unit, 80)
self.assertEqual(self.sale_line_m_red.discount, 10)
self.assertEqual(self.sale_line_m_black.price_unit, 80)
self.assertEqual(self.sale_line_m_black.discount, 10)
self.assertEqual(self.sale_line_m_red.price_unit, 100)
self.assertEqual(self.sale_line_m_red.discount, 28)
self.assertEqual(self.sale_line_m_black.price_unit, 100)
self.assertEqual(self.sale_line_m_black.discount, 28)
self.assertEqual(self.sale_line_2.price_unit, 200)
self.assertEqual(self.sale_line_2.discount, 0)
self.assertEqual(self.sale_line_3.price_unit, 300)
Expand Down
6 changes: 6 additions & 0 deletions setup/sale_pricelist_global_rule/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
Loading