diff --git a/product_pricelist_alternative/README.rst b/product_pricelist_alternative/README.rst new file mode 100644 index 00000000000..e5bb59394d6 --- /dev/null +++ b/product_pricelist_alternative/README.rst @@ -0,0 +1,85 @@ +============================= +Product Pricelist Alternative +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d0c65497950562e7e7984650f3fd3c19fe1edb10d0bf08eb62523523436aa7f1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.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-18-0/product-attribute-18-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=18.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..047ddef6d4e --- /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": "18.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..b02592ac26f --- /dev/null +++ b/product_pricelist_alternative/i18n/fr.po @@ -0,0 +1,98 @@ +# 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" +"Language: \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 +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist.py:0 +#, python-format +msgid "" +"Formulas based on another pricelist are not allowed on alternative " +"pricelists." +msgstr "" +"Les formules basées sur un autre liste de prix ne sont pas autorisées sur " +"les listes de prix alternatives alternatifs." + +#. 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,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 +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist_item.py:0 +#, python-format +msgid "" +"It is not possible to encode this price rule. Formulas based on another " +"pricelist are not allowed on alternative pricelists. Please change to " +"another type of price computation." +msgstr "" +"Il n'est pas possible d'encoder cette règle de prix. Les formules basées sur " +"une autre ne sont pas autorisées dans les listes de prix alternatives. " +"Veuillez passer à un autre type de calcul de prix." + +#. 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 "Utiliser le prix le plus bas" diff --git a/product_pricelist_alternative/i18n/it.po b/product_pricelist_alternative/i18n/it.po new file mode 100644 index 00000000000..bb06afeb354 --- /dev/null +++ b/product_pricelist_alternative/i18n/it.po @@ -0,0 +1,98 @@ +# 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\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-10-13 18:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\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 "Politica dei listini alternativi" + +#. module: product_pricelist_alternative +#: model_terms:ir.ui.view,arch_db:product_pricelist_alternative.product_pricelist_view +msgid "Alternative Pricelists" +msgstr "Listini alternativi" + +#. module: product_pricelist_alternative +#: model:ir.model.fields,field_description:product_pricelist_alternative.field_product_pricelist__alternative_pricelist_ids +msgid "Alternative pricelists" +msgstr "Listini alternativi" + +#. module: product_pricelist_alternative +#: model_terms:ir.ui.view,arch_db:product_pricelist_alternative.product_pricelist_view +msgid "Alternative to Pricelist" +msgstr "Alternativa al listino" + +#. module: product_pricelist_alternative +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist.py:0 +#, python-format +msgid "" +"Formulas based on another pricelist are not allowed on alternative " +"pricelists." +msgstr "" +"Le formule che si basano su un altro listino non sono consentite nei listini " +"alternativi." + +#. 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 "Ignora alternative" + +#. 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 "Conteggio alternative ai listino" + +#. module: product_pricelist_alternative +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist.py:0 +#, python-format +msgid "Is Alternative to Pricelist" +msgstr "È alternativa al listino" + +#. 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 "È alternativa ai listini" + +#. module: product_pricelist_alternative +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist_item.py:0 +#, python-format +msgid "" +"It is not possible to encode this price rule. Formulas based on another " +"pricelist are not allowed on alternative pricelists. Please change to " +"another type of price computation." +msgstr "" +"Non è possibile codificare questa regola di prezzo. Le formule che si basano " +"su un altro listino non sono consentite nei listini alternativi. Scegliere " +"un altro tipo di calcolo del prezzo." + +#. module: product_pricelist_alternative +#: model:ir.model,name:product_pricelist_alternative.model_product_pricelist +msgid "Pricelist" +msgstr "Listino prezzi" + +#. module: product_pricelist_alternative +#: model:ir.model,name:product_pricelist_alternative.model_product_pricelist_item +msgid "Pricelist Rule" +msgstr "Regola listino prezzi" + +#. 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 "Utilizzare prezzo minimo" 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..5ced616f942 --- /dev/null +++ b/product_pricelist_alternative/i18n/product_pricelist_alternative.pot @@ -0,0 +1,90 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_pricelist_alternative +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0+e\n" +"Report-Msgid-Bugs-To: \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 +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist.py:0 +#, python-format +msgid "" +"Formulas based on another pricelist are not allowed on alternative " +"pricelists." +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 +#. odoo-python +#: code:addons/product_pricelist_alternative/models/product_pricelist_item.py:0 +#, python-format +msgid "" +"It is not possible to encode this price rule. Formulas based on another " +"pricelist are not allowed on alternative pricelists. Please change to " +"another type of price computation." +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..f4378f3a2e1 --- /dev/null +++ b/product_pricelist_alternative/models/product_pricelist.py @@ -0,0 +1,126 @@ +# Copyright 2024 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +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": self.env._("Is Alternative to Pricelist"), + "res_model": "product.pricelist", + "view_mode": "list,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, + quantity, + currency=None, + uom=None, + date=False, + compute_price=True, + **kwargs, + ): + # This context key is used in `sale.order::_recompute_prices()`, + # triggered by `action_update_prices()` button that recomputes + # the unit price of all products based on the new pricelist. + if self.env.context.get("force_price_recomputation"): + compute_price = True + + res = super()._compute_price_rule( + products, + quantity, + currency=currency, + uom=uom, + date=date, + compute_price=compute_price, + **kwargs, + ) + # In some contexts we want to ignore alternative pricelists + # and return the original price + if self.env.context.get("skip_alternative_pricelist", False): + return res + + 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, + quantity, + currency=currency, + uom=uom, + date=date, + compute_price=compute_price, + **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 + + @api.constrains("alternative_pricelist_ids") + def _check_pricelist_alternative_items_based_on_other_pricelist(self): + """Alternative pricelists can not contain items based on other pricelist""" + for pricelist in self: + if pricelist.alternative_pricelist_ids.item_ids.filtered( + lambda item: item.compute_price == "formula" + and item.base == "pricelist" + ): + raise ValidationError( + self.env._( + "Formulas based on another pricelist are not allowed " + "on alternative pricelists." + ) + ) 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..f2a869a93f1 --- /dev/null +++ b/product_pricelist_alternative/models/product_pricelist_item.py @@ -0,0 +1,36 @@ +# Copyright 2024 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +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, + ) + + @api.constrains("base") + def _check_pricelist_alternative_items_based_on_other_pricelist(self): + """Alternative pricelists can not contain items based on other pricelist""" + for item in self: + if ( + item.compute_price == "formula" + and item.base == "pricelist" + and item.pricelist_id.is_alternative_to_pricelist_count + ): + raise ValidationError( + self.env._( + "It is not possible to encode this price rule. " + "Formulas based on another pricelist " + "are not allowed on alternative pricelists. " + "Please change to another type of price computation." + ) + ) diff --git a/product_pricelist_alternative/pyproject.toml b/product_pricelist_alternative/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/product_pricelist_alternative/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_pricelist_alternative/readme/CONTRIBUTORS.md b/product_pricelist_alternative/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..f5fcdc50207 --- /dev/null +++ b/product_pricelist_alternative/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Telmo Santos \<\> +- Akim Juillerat \<\> diff --git a/product_pricelist_alternative/readme/DESCRIPTION.md b/product_pricelist_alternative/readme/DESCRIPTION.md new file mode 100644 index 00000000000..6fde5f4ce61 --- /dev/null +++ b/product_pricelist_alternative/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +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/icon.png b/product_pricelist_alternative/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/product_pricelist_alternative/static/description/icon.png differ diff --git a/product_pricelist_alternative/static/description/index.html b/product_pricelist_alternative/static/description/index.html new file mode 100644 index 00000000000..8d826836907 --- /dev/null +++ b/product_pricelist_alternative/static/description/index.html @@ -0,0 +1,431 @@ + + + + + +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/common.py b/product_pricelist_alternative/tests/common.py new file mode 100644 index 00000000000..56fc6ae6797 --- /dev/null +++ b/product_pricelist_alternative/tests/common.py @@ -0,0 +1,103 @@ +# 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 tagged + +from odoo.addons.base.tests.common import BaseCommon + + +@tagged("post_install", "-at_install") +class CommonProductPricelistAlternative(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + 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), + ], + } + ) 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..c19837e1db9 --- /dev/null +++ b/product_pricelist_alternative/tests/test_pricelist_alternative.py @@ -0,0 +1,107 @@ +# Copyright 2024 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.exceptions import ValidationError +from odoo.tests import tagged + +from .common import CommonProductPricelistAlternative + + +@tagged("post_install", "-at_install") +class TestPricelistAlternative(CommonProductPricelistAlternative): + 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"], "list,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 + ) + + def test_skip_alternative_pricelist(self): + """Test product price computation with skip alternative pricelist""" + self.assertEqual(self.pricelist01._get_product_price(self.usb_adapter, 1.0), 70) + # Set the context to skip alternative pricelist + self.assertEqual( + self.pricelist01.with_context( + skip_alternative_pricelist=True + )._get_product_price(self.usb_adapter, 1.0), + 95, + ) + + def test_check_pricelist_alternative_items_based_on_other_pricelist(self): + with self.assertRaises(ValidationError) as e: + self.alternative_pricelist_01.item_ids.write( + { + "compute_price": "formula", + "base": "pricelist", + "base_pricelist_id": self.alternative_pricelist_02.id, + } + ) + msg = ( + "Formulas based on another pricelist " + "are not allowed on alternative pricelists." + ) + self.assertIn(msg, e.exception.args[0]) 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..affae72e624 --- /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.list + product.pricelist.item + + + + + + + + + + product.pricelist.item.list + 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..28b76df5bb8 --- /dev/null +++ b/product_pricelist_alternative/views/product_pricelist_view.xml @@ -0,0 +1,44 @@ + + + + product.pricelist.form + product.pricelist + + + +
+ +
+
+ + + + + + + + + +
+
+