From ff1205c3d6aab208c8ff81354219a4f0eccd4080 Mon Sep 17 00:00:00 2001 From: alvaro-domatix Date: Wed, 27 May 2026 14:05:08 +0000 Subject: [PATCH] [IMP] subscription_oca: add multi-company security rules Without record rules, a salesman from company A could read or write subscriptions belonging to company B. This change adds record rules on sale.subscription and sale.subscription.line so that the salesman group only sees records whose company_id is in the user's allowed companies, matching the standard multi-company pattern used across OCA. The new security file is declared in the manifest before the ACL CSV so that rules are available as soon as access rights are created. --- subscription_oca/__manifest__.py | 1 + .../security/subscription_security.xml | 22 +++++ subscription_oca/tests/__init__.py | 1 + .../tests/test_subscription_security.py | 97 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 subscription_oca/security/subscription_security.xml create mode 100644 subscription_oca/tests/test_subscription_security.py diff --git a/subscription_oca/__manifest__.py b/subscription_oca/__manifest__.py index 58335fe6eb..ab29c0925d 100644 --- a/subscription_oca/__manifest__.py +++ b/subscription_oca/__manifest__.py @@ -22,6 +22,7 @@ "data/ir_cron.xml", "data/sale_subscription_data.xml", "wizard/close_subscription_wizard.xml", + "security/subscription_security.xml", "security/ir.model.access.csv", ], "installable": True, diff --git a/subscription_oca/security/subscription_security.xml b/subscription_oca/security/subscription_security.xml new file mode 100644 index 0000000000..6ded4f84cd --- /dev/null +++ b/subscription_oca/security/subscription_security.xml @@ -0,0 +1,22 @@ + + + + + Subscription: multi-company rule + + + + [('company_id', 'in', company_ids)] + + + + Subscription Line: multi-company rule + + + + [('sale_subscription_id.company_id', 'in', company_ids)] + + diff --git a/subscription_oca/tests/__init__.py b/subscription_oca/tests/__init__.py index f445239d7f..2137c3168a 100644 --- a/subscription_oca/tests/__init__.py +++ b/subscription_oca/tests/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import test_subscription_oca +from . import test_subscription_security diff --git a/subscription_oca/tests/test_subscription_security.py b/subscription_oca/tests/test_subscription_security.py new file mode 100644 index 0000000000..b7722143f2 --- /dev/null +++ b/subscription_oca/tests/test_subscription_security.py @@ -0,0 +1,97 @@ +# Copyright 2026 Domatix - Alvaro Domatix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import AccessError +from odoo.tests.common import new_test_user + +from odoo.addons.base.tests.common import BaseCommon + + +class TestSubscriptionSecurity(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + cls.company_a = cls.env["res.company"].create({"name": "Company A"}) + cls.company_b = cls.env["res.company"].create({"name": "Company B"}) + + cls.user_company_a = new_test_user( + cls.env, + login="subscription_oca_user_company_a", + groups="sales_team.group_sale_salesman", + company_id=cls.company_a.id, + company_ids=[(6, 0, [cls.company_a.id])], + ) + + cls.pricelist_a = cls.env["product.pricelist"].create( + {"name": "Pricelist A", "company_id": cls.company_a.id} + ) + cls.pricelist_b = cls.env["product.pricelist"].create( + {"name": "Pricelist B", "company_id": cls.company_b.id} + ) + cls.partner = cls.env["res.partner"].create({"name": "Multi-company partner"}) + cls.template = cls.env["sale.subscription.template"].create( + { + "name": "Multi-company template", + "code": "MC-TMPL", + } + ) + cls.product = cls.env["product.product"].create( + {"name": "Multi-company product", "subscribable": True} + ) + + cls.subscription_a = cls.env["sale.subscription"].create( + { + "company_id": cls.company_a.id, + "partner_id": cls.partner.id, + "template_id": cls.template.id, + "pricelist_id": cls.pricelist_a.id, + } + ) + cls.subscription_b = cls.env["sale.subscription"].create( + { + "company_id": cls.company_b.id, + "partner_id": cls.partner.id, + "template_id": cls.template.id, + "pricelist_id": cls.pricelist_b.id, + } + ) + cls.line_a = cls.env["sale.subscription.line"].create( + { + "sale_subscription_id": cls.subscription_a.id, + "product_id": cls.product.id, + } + ) + cls.line_b = cls.env["sale.subscription.line"].create( + { + "sale_subscription_id": cls.subscription_b.id, + "product_id": cls.product.id, + } + ) + + def test_user_can_read_own_company_subscription(self): + subscription = self.subscription_a.with_user(self.user_company_a) + subscription.read(["name"]) + + def test_user_cannot_read_other_company_subscription(self): + subscription = self.subscription_b.with_user(self.user_company_a) + with self.assertRaises(AccessError): + subscription.read(["name"]) + + def test_user_cannot_search_other_company_subscription(self): + found = ( + self.env["sale.subscription"] + .with_user(self.user_company_a) + .search([("id", "=", self.subscription_b.id)]) + ) + self.assertFalse(found) + + def test_user_cannot_read_other_company_subscription_line(self): + line = self.line_b.with_user(self.user_company_a) + with self.assertRaises(AccessError): + line.read(["name"]) + + def test_user_can_read_own_company_subscription_line(self): + line = self.line_a.with_user(self.user_company_a) + line.read(["name"])