diff --git a/price_security_planned_price/README.rst b/price_security_planned_price/README.rst index 0fd779c6f..533c41ceb 100644 --- a/price_security_planned_price/README.rst +++ b/price_security_planned_price/README.rst @@ -29,8 +29,9 @@ Configuration To configure this module, you need to: -#. Set 'Restrict Prices' for users you want to restrict. -#. For the same users, configure discounts range on "Discounts Permissions" users tab. +#. On Settings > Users, give the user one of the groups of the "Price + restriction" privilege that Price Security adds: "Only view: sale and + purchase price" or "Only view: sale price". Usage @@ -38,14 +39,15 @@ Usage To use this module, you need to: -For users with price restriction, it restricts: +Open a product. On both the product template form and the product variant +form, the planned price fields that Product Planned Price adds are restricted +by group: -* on sales orders: change payment term or pricelist -* on sales order lines: change unit price and set limits on discount (limits configured on user) -* on partners: change payment term or pricelist -* on invoices: change unit price -* on invoice lines: change unit price and set limits on discount (limits configured on user) -* on product: change price +* "Only view: sale and purchase price" leaves the planned price fields + readonly: based on, margin, surcharge, price in another currency, its + currency, and the manual planned price +* "Only view: sale price" (which implies the group above) also hides the whole + Planned Price block .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot diff --git a/price_security_planned_price/__manifest__.py b/price_security_planned_price/__manifest__.py index 01424151f..c5855b202 100644 --- a/price_security_planned_price/__manifest__.py +++ b/price_security_planned_price/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Price Security with planned price", - "version": "19.0.1.0.0", + "version": "20.0.1.0.0", "category": "Sales Management", "author": "ADHOC SA", "website": "http://www.adhoc.com.ar/", @@ -28,6 +28,6 @@ "price_security", "product_planned_price", ], - "installable": False, + "installable": True, "auto_install": True, } diff --git a/price_security_planned_price/i18n/price_security_planned_price.pot b/price_security_planned_price/i18n/price_security_planned_price.pot index a01bbc751..2fd47fb0f 100644 --- a/price_security_planned_price/i18n/price_security_planned_price.pot +++ b/price_security_planned_price/i18n/price_security_planned_price.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 19.0+e\n" +"Project-Id-Version: Odoo Server 20.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-05 14:14+0000\n" -"PO-Revision-Date: 2025-12-05 14:14+0000\n" +"POT-Creation-Date: 2026-10-22 23:01+0000\n" +"PO-Revision-Date: 2026-10-22 23:01+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -15,6 +15,93 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: price_security_planned_price +#: model:ir.module.module,description:base.module_price_security_planned_price +msgid "" +".. |company| replace:: ADHOC SA\n" +"\n" +".. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png\n" +" :alt: ADHOC SA\n" +" :target: https://www.adhoc.com.ar\n" +"\n" +".. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png\n" +"\n" +".. image:: https://img.shields.io/badge/license-AGPL--3-blue.png\n" +" :target: https://www.gnu.org/licenses/agpl\n" +" :alt: License: AGPL-3\n" +"\n" +"=================================\n" +"Price Security with planned price\n" +"=================================\n" +"\n" +"This module is an integration of Price Security with Product Planned Price.\n" +"\n" +"Installation\n" +"============\n" +"\n" +"To install this module, you need to:\n" +"\n" +"#. Just install this module.\n" +"\n" +"\n" +"Configuration\n" +"=============\n" +"\n" +"To configure this module, you need to:\n" +"\n" +"#. On Settings > Users, give the user one of the groups of the \"Price\n" +" restriction\" privilege that Price Security adds: \"Only view: sale and\n" +" purchase price\" or \"Only view: sale price\".\n" +"\n" +"\n" +"Usage\n" +"=====\n" +"\n" +"To use this module, you need to:\n" +"\n" +"Open a product. On both the product template form and the product variant\n" +"form, the planned price fields that Product Planned Price adds are restricted\n" +"by group:\n" +"\n" +"* \"Only view: sale and purchase price\" leaves the planned price fields\n" +" readonly: based on, margin, surcharge, price in another currency, its\n" +" currency, and the manual planned price\n" +"* \"Only view: sale price\" (which implies the group above) also hides the whole\n" +" Planned Price block\n" +"\n" +".. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas\n" +" :alt: Try me on Runbot\n" +" :target: http://runbot.adhoc.com.ar/\n" +"\n" +"Bug Tracker\n" +"===========\n" +"\n" +"Bugs are tracked on `GitHub Issues\n" +"`_. In case of trouble, please\n" +"check there if your issue has already been reported. If you spotted it first,\n" +"help us smashing it by providing a detailed and welcomed feedback.\n" +"\n" +"Credits\n" +"=======\n" +"\n" +"Images\n" +"------\n" +"\n" +"* |company| |icon|\n" +"\n" +"Contributors\n" +"------------\n" +"\n" +"Maintainer\n" +"----------\n" +"\n" +"|company_logo|\n" +"\n" +"This module is maintained by the |company|.\n" +"\n" +"To contribute to this module, please visit https://www.adhoc.com.ar.\n" +msgstr "" + #. module: price_security_planned_price #: model:ir.model.fields,field_description:price_security_planned_price.field_product_product__display_name #: model:ir.model.fields,field_description:price_security_planned_price.field_product_template__display_name @@ -27,6 +114,11 @@ msgstr "" msgid "ID" msgstr "" +#. module: price_security_planned_price +#: model:ir.module.module,shortdesc:base.module_price_security_planned_price +msgid "Price Security with planned price" +msgstr "" + #. module: price_security_planned_price #: model:ir.model,name:price_security_planned_price.model_product_template msgid "Product" diff --git a/price_security_planned_price/tests/__init__.py b/price_security_planned_price/tests/__init__.py new file mode 100644 index 000000000..65e91c7ec --- /dev/null +++ b/price_security_planned_price/tests/__init__.py @@ -0,0 +1 @@ +from . import test_planned_price_visibility diff --git a/price_security_planned_price/tests/test_planned_price_visibility.py b/price_security_planned_price/tests/test_planned_price_visibility.py new file mode 100644 index 000000000..9279d6dd5 --- /dev/null +++ b/price_security_planned_price/tests/test_planned_price_visibility.py @@ -0,0 +1,85 @@ +from lxml import etree +from odoo.tests import TransactionCase, new_test_user, tagged + +# the fields product_planned_price adds to the product form, which this module +# locks for users that may only look at prices +PLANNED_PRICE_FIELDS = ( + "list_price_type", + "computed_list_price_manual", + "other_currency_list_price", + "other_currency_id", + "sale_margin", + "sale_surcharge", +) +PRODUCT_MODELS = ("product.template", "product.product") + + +@tagged("post_install", "-at_install") +class TestPlannedPriceVisibility(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + base_groups = "base.group_user,sales_team.group_sale_manager" + cls.plain_user = new_test_user(cls.env, login="psp_plain", groups=base_groups) + cls.only_view_user = new_test_user( + cls.env, + login="psp_only_view", + groups=base_groups + ",price_security.group_only_view", + ) + # group_only_view_sale_price implies group_only_view, so this user gets both behaviours + cls.only_sale_price_user = new_test_user( + cls.env, + login="psp_only_sale_price", + groups=base_groups + ",price_security.group_only_view_sale_price", + ) + + def _form_arch(self, model, user): + arch = self.env[model].with_user(user).get_view(view_type="form")["arch"] + return etree.fromstring(arch) + + def _assert_readonly(self, arch, expected, model): + for field_name in PLANNED_PRICE_FIELDS: + nodes = arch.xpath("//field[@name='%s']" % field_name) + self.assertTrue(nodes, "%s is expected on the %s form" % (field_name, model)) + for node in nodes: + self.assertEqual( + node.get("readonly"), + expected, + "wrong readonly on %s of the %s form" % (field_name, model), + ) + + def _planned_price_group(self, arch, model): + nodes = arch.xpath("//group[@name='planned_price']") + self.assertTrue(nodes, "the planned price group is expected on the %s form" % model) + return nodes + + def test_plain_user_keeps_the_planned_price_fields_editable(self): + for model in PRODUCT_MODELS: + with self.subTest(model=model): + arch = self._form_arch(model, self.plain_user) + self._assert_readonly(arch, None, model) + for node in self._planned_price_group(arch, model): + self.assertIsNone(node.get("invisible")) + + def test_only_view_locks_the_planned_price_fields(self): + for model in PRODUCT_MODELS: + with self.subTest(model=model): + arch = self._form_arch(model, self.only_view_user) + self._assert_readonly(arch, "1", model) + + def test_only_view_keeps_the_planned_price_group_visible(self): + """Seeing the sale price is a stricter group: only_view still shows the block""" + for model in PRODUCT_MODELS: + with self.subTest(model=model): + arch = self._form_arch(model, self.only_view_user) + for node in self._planned_price_group(arch, model): + self.assertIsNone(node.get("invisible")) + + def test_only_sale_price_hides_the_planned_price_group(self): + for model in PRODUCT_MODELS: + with self.subTest(model=model): + arch = self._form_arch(model, self.only_sale_price_user) + # the implied group still locks the fields + self._assert_readonly(arch, "1", model) + for node in self._planned_price_group(arch, model): + self.assertEqual(node.get("invisible"), "1")