diff --git a/product_pricelist_alternative/README.rst b/product_pricelist_alternative/README.rst new file mode 100644 index 00000000000..197b4f44711 --- /dev/null +++ b/product_pricelist_alternative/README.rst @@ -0,0 +1,80 @@ +============================= +Product Pricelist Alternative +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:86e2eb1f76f677e2d3704362fdf38f1f275251b62d7ac5273c956935e824eade + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/16.0/product_pricelist_alternative + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_pricelist_alternative + :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/product-attribute&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +It allows you to define alternative price lists to a reference price list. +As a general rule, the price of a given product is obtained from the minimum between its reference price list and the alternative price lists. + +However, if the product's reference price has been calculated on the basis of a price rule in which the "Alternative Pricelist Policy = Ignore" field is selected, the alternative price lists will not be taken into account. + +**Table of contents** + +.. contents:: + :local: + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Telmo Santos +* Akim Juillerat + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_pricelist_alternative/__init__.py b/product_pricelist_alternative/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/product_pricelist_alternative/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_pricelist_alternative/__manifest__.py b/product_pricelist_alternative/__manifest__.py new file mode 100644 index 00000000000..07bc701fbca --- /dev/null +++ b/product_pricelist_alternative/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2024 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Product Pricelist Alternative", + "version": "16.0.1.0.0", + "development_status": "Beta", + "category": "Product", + "summary": "Calculate product price based on alternative pricelists", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "license": "AGPL-3", + "depends": [ + "product", + ], + "data": [ + "views/product_pricelist_item_view.xml", + "views/product_pricelist_view.xml", + ], + "installable": True, + "auto_install": False, +} diff --git a/product_pricelist_alternative/i18n/fr.po b/product_pricelist_alternative/i18n/fr.po new file mode 100644 index 00000000000..e536a68109e --- /dev/null +++ b/product_pricelist_alternative/i18n/fr.po @@ -0,0 +1,46 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_pricelist_alternative +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-19 09:06+0000\n" +"PO-Revision-Date: 2024-03-19 09:06+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_pricelist_alternative +#: model:ir.model.fields,field_description:product_pricelist_alternative.field_product_pricelist_item__alternative_pricelist_policy +msgid "Alternative Pricelist Policy" +msgstr "Politique de prix alternatifs" + +#. module: product_pricelist_alternative +#: model_terms:ir.ui.view,arch_db:product_pricelist_alternative.product_pricelist_view +msgid "Alternative Pricelists" +msgstr "Listes de prix alternatives" + +#. module: product_pricelist_alternative +#: model:ir.model.fields,field_description:product_pricelist_alternative.field_product_pricelist__alternative_pricelist_ids +msgid "Alternative pricelists" +msgstr "Listes de prix alternatives" + +#. module: product_pricelist_alternative +#: model_terms:ir.ui.view,arch_db:product_pricelist_alternative.product_pricelist_view +msgid "Alternative to Pricelist" +msgstr "Alternative à la liste de prix" + +#. module: product_pricelist_alternative +#: model:ir.model.fields.selection,name:product_pricelist_alternative.selection__product_pricelist_item__alternative_pricelist_policy__ignore +msgid "Ignore alternatives" +msgstr "Ignorer les alternatives" + +#. module: product_pricelist_alternative +#: model:ir.model.fields.selection,name:product_pricelist_alternative.selection__product_pricelist_item__alternative_pricelist_policy__use_lower_price +msgid "Use lower price" +msgstr "Utiliser le prix le plus bas" diff --git a/product_pricelist_alternative/i18n/product_pricelist_alternative.pot b/product_pricelist_alternative/i18n/product_pricelist_alternative.pot new file mode 100644 index 00000000000..de9f165959b --- /dev/null +++ b/product_pricelist_alternative/i18n/product_pricelist_alternative.pot @@ -0,0 +1,73 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_pricelist_alternative +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-19 09:06+0000\n" +"PO-Revision-Date: 2024-03-19 09:06+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_pricelist_alternative +#: model:ir.model.fields,field_description:product_pricelist_alternative.field_product_pricelist_item__alternative_pricelist_policy +msgid "Alternative Pricelist Policy" +msgstr "" + +#. module: product_pricelist_alternative +#: model_terms:ir.ui.view,arch_db:product_pricelist_alternative.product_pricelist_view +msgid "Alternative Pricelists" +msgstr "" + +#. module: product_pricelist_alternative +#: model:ir.model.fields,field_description:product_pricelist_alternative.field_product_pricelist__alternative_pricelist_ids +msgid "Alternative pricelists" +msgstr "" + +#. module: product_pricelist_alternative +#: model_terms:ir.ui.view,arch_db:product_pricelist_alternative.product_pricelist_view +msgid "Alternative to Pricelist" +msgstr "" + +#. module: product_pricelist_alternative +#: model:ir.model.fields.selection,name:product_pricelist_alternative.selection__product_pricelist_item__alternative_pricelist_policy__ignore +msgid "Ignore alternatives" +msgstr "" + +#. module: product_pricelist_alternative +#: model:ir.model.fields,field_description:product_pricelist_alternative.field_product_pricelist__is_alternative_to_pricelist_count +msgid "Is Alternative To Pricelist Count" +msgstr "" + +#. module: product_pricelist_alternative +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist.py:0 +#, python-format +msgid "Is Alternative to Pricelist" +msgstr "" + +#. module: product_pricelist_alternative +#: model:ir.model.fields,field_description:product_pricelist_alternative.field_product_pricelist__is_alternative_to_pricelist_ids +msgid "Is alternative to pricelists" +msgstr "" + +#. module: product_pricelist_alternative +#: model:ir.model,name:product_pricelist_alternative.model_product_pricelist +msgid "Pricelist" +msgstr "" + +#. module: product_pricelist_alternative +#: model:ir.model,name:product_pricelist_alternative.model_product_pricelist_item +msgid "Pricelist Rule" +msgstr "" + +#. module: product_pricelist_alternative +#: model:ir.model.fields.selection,name:product_pricelist_alternative.selection__product_pricelist_item__alternative_pricelist_policy__use_lower_price +msgid "Use lower price" +msgstr "" diff --git a/product_pricelist_alternative/models/__init__.py b/product_pricelist_alternative/models/__init__.py new file mode 100644 index 00000000000..cb6c4e8fef5 --- /dev/null +++ b/product_pricelist_alternative/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_pricelist +from . import product_pricelist_item diff --git a/product_pricelist_alternative/models/product_pricelist.py b/product_pricelist_alternative/models/product_pricelist.py new file mode 100644 index 00000000000..4c96b17c7ba --- /dev/null +++ b/product_pricelist_alternative/models/product_pricelist.py @@ -0,0 +1,76 @@ +# Copyright 2024 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models + + +class Pricelist(models.Model): + _inherit = "product.pricelist" + + alternative_pricelist_ids = fields.Many2many( + comodel_name="product.pricelist", + string="Alternative pricelists", + relation="product_pricelist_alternative_rel", + column1="origin_id", + column2="alternative_id", + domain="[('id', '!=', id)]", + ) + is_alternative_to_pricelist_ids = fields.Many2many( + comodel_name="product.pricelist", + string="Is alternative to pricelists", + relation="product_pricelist_alternative_rel", + column1="alternative_id", + column2="origin_id", + ) + is_alternative_to_pricelist_count = fields.Integer( + compute="_compute_is_alternative_to_pricelist_count" + ) + + @api.depends("is_alternative_to_pricelist_ids") + def _compute_is_alternative_to_pricelist_count(self): + groups = self.read_group( + [("alternative_pricelist_ids", "in", self.ids)], + ["alternative_pricelist_ids"], + "alternative_pricelist_ids", + lazy=False, + ) + data = { + group["alternative_pricelist_ids"][0]: group["__count"] for group in groups + } + for pricelist in self: + pricelist.is_alternative_to_pricelist_count = data.get(pricelist.id, 0) + + def action_view_is_alternative_to_pricelist(self): + self.ensure_one() + action = { + "type": "ir.actions.act_window", + "name": _("Is Alternative to Pricelist"), + "res_model": "product.pricelist", + "view_mode": "tree,form", + "domain": [("id", "in", self.is_alternative_to_pricelist_ids.ids)], + "context": dict(self.env.context, create=False), + } + if self.is_alternative_to_pricelist_count == 1: + action.update( + {"view_mode": "form", "res_id": self.is_alternative_to_pricelist_ids.id} + ) + return action + + def _compute_price_rule(self, products, qty, uom=None, date=False, **kwargs): + res = super()._compute_price_rule(products, qty, uom=uom, date=date, **kwargs) + for product in products: + reference_pricelist_item = self.env["product.pricelist.item"].browse( + res[product.id][1] + ) + if ( + reference_pricelist_item.alternative_pricelist_policy + == "use_lower_price" + ): + for alternative_pricelist in self.alternative_pricelist_ids: + alternative_price_rule = alternative_pricelist._compute_price_rule( + product, qty, uom=uom, date=date, **kwargs + ) + # use alternative price if lower + if alternative_price_rule[product.id][0] < res[product.id][0]: + res[product.id] = alternative_price_rule[product.id] + return res diff --git a/product_pricelist_alternative/models/product_pricelist_item.py b/product_pricelist_alternative/models/product_pricelist_item.py new file mode 100644 index 00000000000..55c69d34ca7 --- /dev/null +++ b/product_pricelist_alternative/models/product_pricelist_item.py @@ -0,0 +1,17 @@ +# Copyright 2024 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class PricelistItem(models.Model): + _inherit = "product.pricelist.item" + + alternative_pricelist_policy = fields.Selection( + selection=[ + ("use_lower_price", "Use lower price"), + ("ignore", "Ignore alternatives"), + ], + default="use_lower_price", + required=True, + ) diff --git a/product_pricelist_alternative/readme/CONTRIBUTORS.rst b/product_pricelist_alternative/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..2052422f4dc --- /dev/null +++ b/product_pricelist_alternative/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Telmo Santos +* Akim Juillerat diff --git a/product_pricelist_alternative/readme/DESCRIPTION.rst b/product_pricelist_alternative/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..a535b85646a --- /dev/null +++ b/product_pricelist_alternative/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +It allows you to define alternative price lists to a reference price list. +As a general rule, the price of a given product is obtained from the minimum between its reference price list and the alternative price lists. + +However, if the product's reference price has been calculated on the basis of a price rule in which the "Alternative Pricelist Policy = Ignore" field is selected, the alternative price lists will not be taken into account. diff --git a/product_pricelist_alternative/static/description/index.html b/product_pricelist_alternative/static/description/index.html new file mode 100644 index 00000000000..899ce051196 --- /dev/null +++ b/product_pricelist_alternative/static/description/index.html @@ -0,0 +1,424 @@ + + + + + + +Product Pricelist Alternative + + + +
+

Product Pricelist Alternative

+ + +

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

It allows you to define alternative price lists to a reference price list. +As a general rule, the price of a given product is obtained from the minimum between its reference price list and the alternative price lists.

+

However, if the product’s reference price has been calculated on the basis of a price rule in which the “Alternative Pricelist Policy = Ignore” field is selected, the alternative price lists will not be taken into account.

+

Table of contents

+ +
+

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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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

+

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

+
+
+
+ + diff --git a/product_pricelist_alternative/tests/__init__.py b/product_pricelist_alternative/tests/__init__.py new file mode 100644 index 00000000000..8cc19ce9ce3 --- /dev/null +++ b/product_pricelist_alternative/tests/__init__.py @@ -0,0 +1 @@ +from . import test_pricelist_alternative diff --git a/product_pricelist_alternative/tests/test_pricelist_alternative.py b/product_pricelist_alternative/tests/test_pricelist_alternative.py new file mode 100644 index 00000000000..4206c0c7454 --- /dev/null +++ b/product_pricelist_alternative/tests/test_pricelist_alternative.py @@ -0,0 +1,176 @@ +# Copyright 2024 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.fields import Command +from odoo.tests import common, tagged + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +@tagged("post_install", "-at_install") +class TestPricelistAlternative(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env["base"].with_context(**DISABLED_MAIL_CONTEXT).env + cls.datacard = cls.env["product.product"].create( + {"name": "Data card", "list_price": 100} + ) + cls.usb_adapter = cls.env["product.product"].create( + {"name": "Usb adapter", "list_price": 100} + ) + + cls.alternative_pricelist_01 = cls.env["product.pricelist"].create( + { + "name": "Alternative pricelist 01", + "item_ids": [ + Command.create( + { + "compute_price": "fixed", + "product_id": cls.usb_adapter.id, + "applied_on": "0_product_variant", + "fixed_price": 70, + } + ), + ], + } + ) + cls.alternative_pricelist_02 = cls.env["product.pricelist"].create( + { + "name": "Alternative pricelist 02", + "item_ids": [ + Command.create( + { + "compute_price": "fixed", + "product_id": cls.datacard.id, + "applied_on": "0_product_variant", + "fixed_price": 80, + } + ), + ], + } + ) + + cls.pricelist01 = cls.env["product.pricelist"].create( + { + "name": "Sale pricelist", + "item_ids": [ + Command.create( + { + "compute_price": "fixed", + "product_id": cls.usb_adapter.id, + "applied_on": "0_product_variant", + "fixed_price": 95, + } + ), + Command.create( + { + "compute_price": "fixed", + "product_id": cls.datacard.id, + "applied_on": "0_product_variant", + "fixed_price": 70, + } + ), + ], + "alternative_pricelist_ids": [ + (4, cls.alternative_pricelist_01.id), + (4, cls.alternative_pricelist_02.id), + ], + } + ) + + cls.product_categ01 = cls.env["product.category"].create( + {"name": "Category 01"} + ) + cls.usb_adapter.categ_id = cls.product_categ01 + + cls.pricelist02 = cls.env["product.pricelist"].create( + { + "name": "Sale pricelist", + "item_ids": [ + Command.create( + { + "compute_price": "percentage", + "applied_on": "2_product_category", + "categ_id": cls.product_categ01.id, + "percent_price": 40, + } + ), + ], + "alternative_pricelist_ids": [ + (4, cls.alternative_pricelist_01.id), + ], + } + ) + + def test_is_alternative_to_pricelist_count(self): + """Test that the is_alternative_to_pricelist_count is correctly computed""" + + self.assertEqual( + self.alternative_pricelist_01.is_alternative_to_pricelist_count, 2 + ) + self.assertEqual( + self.alternative_pricelist_02.is_alternative_to_pricelist_count, 1 + ) + + def test_action_view_is_alternative_to_pricelist(self): + action = self.alternative_pricelist_01.action_view_is_alternative_to_pricelist() + self.assertEqual(action["view_mode"], "tree,form") + self.assertEqual( + action["domain"][0][2], + self.alternative_pricelist_01.is_alternative_to_pricelist_ids.ids, + ) + + action = self.alternative_pricelist_02.action_view_is_alternative_to_pricelist() + self.assertEqual(action["view_mode"], "form") + self.assertEqual( + action["domain"][0][2], + self.alternative_pricelist_02.is_alternative_to_pricelist_ids.ids, + ) + + def test_product_price_considering_alternative_pricelist_with_lower_price(self): + """Test that the product price is computed considering the alternative + pricelist with the lower price""" + + # Best price on alternative pricelist01 + result = self.pricelist01._compute_price_rule( + self.usb_adapter, 1.0, self.usb_adapter.uom_id + ) + self.assertEqual(result[self.usb_adapter.id][0], 70.0) + self.assertEqual( + result[self.usb_adapter.id][1], self.alternative_pricelist_01.item_ids[0].id + ) + # Best price on pricelist02 + result = self.pricelist02._compute_price_rule( + self.usb_adapter, 1.0, self.usb_adapter.uom_id + ) + self.assertEqual(result[self.usb_adapter.id][0], 60.0) + self.assertEqual( + result[self.usb_adapter.id][1], self.pricelist02.item_ids[0].id + ) + + def test_product_price_ignore_alternative_pricelist(self): + """Test that the product price ignore alternative pricelist""" + + # Set the pricelist items policy to ignore alternative pricelist + self.pricelist01.item_ids.write({"alternative_pricelist_policy": "ignore"}) + self.pricelist02.item_ids.write({"alternative_pricelist_policy": "ignore"}) + + # We won't consider the alternative pricelist + self.assertEqual(self.pricelist01._get_product_price(self.usb_adapter, 1.0), 95) + self.assertEqual(self.pricelist02._get_product_price(self.usb_adapter, 1.0), 60) + + result = self.pricelist01._compute_price_rule( + self.usb_adapter, 1.0, self.usb_adapter.uom_id + ) + self.assertEqual(result[self.usb_adapter.id][0], 95.0) + self.assertEqual( + result[self.usb_adapter.id][1], self.pricelist01.item_ids[1].id + ) + result = self.pricelist02._compute_price_rule( + self.usb_adapter, 1.0, self.usb_adapter.uom_id + ) + self.assertEqual(result[self.usb_adapter.id][0], 60.0) + self.assertEqual( + result[self.usb_adapter.id][1], self.pricelist02.item_ids[0].id + ) diff --git a/product_pricelist_alternative/views/product_pricelist_item_view.xml b/product_pricelist_alternative/views/product_pricelist_item_view.xml new file mode 100644 index 00000000000..501c4c2ef58 --- /dev/null +++ b/product_pricelist_alternative/views/product_pricelist_item_view.xml @@ -0,0 +1,38 @@ + + + + product.pricelist.item.form + product.pricelist.item + + + + + + + + + + product.pricelist.item.tree + product.pricelist.item + + + + + + + + + + product.pricelist.item.tree + product.pricelist.item + + + + + + + + diff --git a/product_pricelist_alternative/views/product_pricelist_view.xml b/product_pricelist_alternative/views/product_pricelist_view.xml new file mode 100644 index 00000000000..eb04039b56c --- /dev/null +++ b/product_pricelist_alternative/views/product_pricelist_view.xml @@ -0,0 +1,50 @@ + + + + product.pricelist.form + product.pricelist + + + +
+ +
+
+ + + + + + + + + + + + +
+
+
diff --git a/setup/product_pricelist_alternative/odoo/addons/product_pricelist_alternative b/setup/product_pricelist_alternative/odoo/addons/product_pricelist_alternative new file mode 120000 index 00000000000..5702d4336ef --- /dev/null +++ b/setup/product_pricelist_alternative/odoo/addons/product_pricelist_alternative @@ -0,0 +1 @@ +../../../../product_pricelist_alternative \ No newline at end of file diff --git a/setup/product_pricelist_alternative/setup.py b/setup/product_pricelist_alternative/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/product_pricelist_alternative/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)