Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions price_security_planned_price/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,25 @@ 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
=====

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
Expand Down
4 changes: 2 additions & 2 deletions price_security_planned_price/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand All @@ -28,6 +28,6 @@
"price_security",
"product_planned_price",
],
"installable": False,
"installable": True,
"auto_install": True,
}
98 changes: 95 additions & 3 deletions price_security_planned_price/i18n/price_security_planned_price.pot
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,104 @@
#
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"
"Content-Type: text/plain; charset=UTF-8\n"
"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"
"<https://github.com/ingadhoc/product/issues>`_. 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
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions price_security_planned_price/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_planned_price_visibility
Original file line number Diff line number Diff line change
@@ -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")
Loading