diff --git a/sale_pricelist_global_rule/README.rst b/sale_pricelist_global_rule/README.rst index f84d72645c0d..d2db8f5fae0b 100644 --- a/sale_pricelist_global_rule/README.rst +++ b/sale_pricelist_global_rule/README.rst @@ -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| @@ -80,7 +80,7 @@ Bug Tracker Bugs are tracked on `GitHub 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -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 `_ project on GitHub. +This module is part of the `OCA/sale-workflow `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_pricelist_global_rule/__manifest__.py b/sale_pricelist_global_rule/__manifest__.py index 4335f479d53b..956abf01f546 100644 --- a/sale_pricelist_global_rule/__manifest__.py +++ b/sale_pricelist_global_rule/__manifest__.py @@ -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", diff --git a/sale_pricelist_global_rule/models/product_pricelist.py b/sale_pricelist_global_rule/models/product_pricelist.py index 3654bc9eb429..47cb39b58530 100644 --- a/sale_pricelist_global_rule/models/product_pricelist.py +++ b/sale_pricelist_global_rule/models/product_pricelist.py @@ -2,16 +2,50 @@ from odoo.exceptions import ValidationError +def get_family_category(category): + subcategories = set(category.child_id) + all_subcategories = subcategories.copy() + for subcategory in subcategories: + all_subcategories |= get_family_category(subcategory) + 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() + # Load all rules + self.env["product.pricelist.item"].flush( + ["price", "currency_id", "company_id", "active"] ) - # ignore new global rules on Odoo standard + self.env.cr.execute( + """ + 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) return items.filtered( lambda item: item.applied_on not in ["4_global_product_template", "5_global_product_category"] @@ -82,6 +116,7 @@ def _compute_price_rule_global(self, sale): "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`. @@ -111,10 +146,10 @@ def _compute_price_rule_global(self, sale): # 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): continue + if rule.base == "pricelist" and rule.base_pricelist_id: # first, try compute the price for global rule # otherwise, fallback to regular computation @@ -125,9 +160,7 @@ def _compute_price_rule_global(self, sale): ) = 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: @@ -143,9 +176,15 @@ def _compute_price_rule_global(self, sale): price = src_currency._convert( price, self.currency_id, self.env.company, date, round=False ) - + # import pdb; pdb.set_trace() 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 @@ -231,8 +270,8 @@ def _check_product_consistency(self): "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") % ( @@ -318,11 +357,17 @@ def _is_applicable_for_sale(self, product_template, qty_data): elif self.global_product_tmpl_id != product_template: is_applicable = False elif self.applied_on == "5_global_product_category": - total_qty = qty_data["by_categ"].get(product_template.categ_id, 0.0) + category = product_template.categ_id + total_qty = qty_data["by_categ"].get(category, 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 - ): - is_applicable = False + else: + rule_family_categories = get_family_category(self.global_categ_id) + rule_family_categories.add(self.global_categ_id) + + product_family_categories = get_family_category(category) + product_family_categories.add(category) + + if not rule_family_categories.intersection(product_family_categories): + is_applicable = False return is_applicable diff --git a/sale_pricelist_global_rule/models/sale_order.py b/sale_pricelist_global_rule/models/sale_order.py index d41605a9dcd2..2da79740f4b5 100644 --- a/sale_pricelist_global_rule/models/sale_order.py +++ b/sale_pricelist_global_rule/models/sale_order.py @@ -1,3 +1,5 @@ +import math + from odoo import api, fields, models from odoo.tools import float_compare @@ -47,6 +49,7 @@ def button_compute_pricelist_global_rule(self): 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, @@ -55,15 +58,10 @@ def button_compute_pricelist_global_rule(self): 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, @@ -75,6 +73,7 @@ def button_compute_pricelist_global_rule(self): 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 ): @@ -84,5 +83,6 @@ def button_compute_pricelist_global_rule(self): 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 diff --git a/sale_pricelist_global_rule/static/description/index.html b/sale_pricelist_global_rule/static/description/index.html index 6be684c49327..19a46179c63f 100644 --- a/sale_pricelist_global_rule/static/description/index.html +++ b/sale_pricelist_global_rule/static/description/index.html @@ -369,7 +369,7 @@

Sale pricelist global rule

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:b93829a100ce8864f9f399ba2bf2c998544d9a7b00d3555405045eaebe3f4cd4 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

This module allows configured pricelists to be applied to a sales order by considering cumulative quantities across all lines.

Global by Product Template

If a pricelist rule has a min_quantity = 15, and a sales order contains:

@@ -430,7 +430,7 @@

Bug Tracker

Bugs are tracked on GitHub 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.

+feedback.

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

@@ -460,7 +460,7 @@

Maintainers

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 project on GitHub.

+

This module is part of the OCA/sale-workflow project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/sale_pricelist_global_rule/tests/test_pricelist_global.py b/sale_pricelist_global_rule/tests/test_pricelist_global.py index 31a5c1adad52..dca251e170ab 100644 --- a/sale_pricelist_global_rule/tests/test_pricelist_global.py +++ b/sale_pricelist_global_rule/tests/test_pricelist_global.py @@ -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 @@ -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) diff --git a/setup/sale_pricelist_global_rule/odoo/addons/sale_pricelist_global_rule b/setup/sale_pricelist_global_rule/odoo/addons/sale_pricelist_global_rule new file mode 120000 index 000000000000..6dbba224015f --- /dev/null +++ b/setup/sale_pricelist_global_rule/odoo/addons/sale_pricelist_global_rule @@ -0,0 +1 @@ +../../../../sale_pricelist_global_rule \ No newline at end of file diff --git a/setup/sale_pricelist_global_rule/setup.py b/setup/sale_pricelist_global_rule/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/sale_pricelist_global_rule/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)