diff --git a/addons/web/static/src/webclient/company_service.js b/addons/web/static/src/webclient/company_service.js index 8a80d47ab0f..f30c86bc792 100644 --- a/addons/web/static/src/webclient/company_service.js +++ b/addons/web/static/src/webclient/company_service.js @@ -51,9 +51,39 @@ function getCompanyIdsFromBrowser(hash) { return cids || []; } +const errorHandlerRegistry = registry.category("error_handlers"); +function accessErrorHandler(env, error, originalError) { + const router = env.services.router; + const hash = router.current.hash; + if (!hash._company_switching) { + return false; + } + if (originalError?.exceptionName === "odoo.exceptions.AccessError") { + const { model, id, view_type } = hash; + if (!model || !id || view_type !== "form") { + return false; + } + router.pushState({ view_type: undefined }); + + browser.setTimeout(() => { + // Force the WebClient to reload the state contained in the hash. + env.bus.trigger("ROUTE_CHANGE"); + }); + if (error.event) { + error.event.preventDefault(); + } + return true; + } + return false; +} + export const companyService = { dependencies: ["user", "router", "action"], start(env, { user, router, action }) { + // Push an error handler in the registry. It needs to be before "rpcErrorHandler", which + // has a sequence of 97. The default sequence of registry is 50. + errorHandlerRegistry.add("accessErrorHandlerCompanies", accessErrorHandler); + const allowedCompanies = session.user_companies.allowed_companies; const disallowedAncestorCompanies = session.user_companies.disallowed_ancestor_companies; const allowedCompaniesWithAncestors = { @@ -123,6 +153,7 @@ export const companyService = { const cidsHash = formatCompanyIds(newCompanyIds, CIDS_HASH_SEPARATOR); router.pushState({ cids: cidsHash }, { lock: true }); + router.pushState({ _company_switching: true }); cookie.set("cids", formatCompanyIds(newCompanyIds)); browser.setTimeout(() => browser.location.reload()); // history.pushState is a little async }, diff --git a/addons/web/static/tests/webclient/company_service_tests.js b/addons/web/static/tests/webclient/company_service_tests.js index 3a63cfb5278..e64306d29fc 100644 --- a/addons/web/static/tests/webclient/company_service_tests.js +++ b/addons/web/static/tests/webclient/company_service_tests.js @@ -86,6 +86,7 @@ QUnit.test("extract allowed company ids from url hash", async (assert) => { assert.strictEqual(env.services.company.currentCompany.id, 3); // backward compatibility + registry.category("error_handlers").remove("accessErrorHandlerCompanies"); Object.assign(browser.location, { hash: "cids=3%2C1" }); env = await makeTestEnv(); assert.deepEqual(env.services.company.activeCompanyIds, [3, 1]); diff --git a/addons/web/static/tests/webclient/mobile/mobile_switch_company_menu_tests.js b/addons/web/static/tests/webclient/mobile/mobile_switch_company_menu_tests.js index 33787778792..21e0c2b9bc7 100644 --- a/addons/web/static/tests/webclient/mobile/mobile_switch_company_menu_tests.js +++ b/addons/web/static/tests/webclient/mobile/mobile_switch_company_menu_tests.js @@ -119,7 +119,7 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => { assert.containsN(scMenuEl, "[data-company-id] .fa-check-square", 2); assert.containsN(scMenuEl, "[data-company-id] .fa-square-o", 1); await prom; - assert.verifySteps(["cids=1-2"]); + assert.verifySteps(["cids=1-2&_company_switching=1"]); }); QUnit.test("can toggle multiple companies at once", async (assert) => { @@ -157,7 +157,7 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => { assert.verifySteps([]); await prom; // await toggle promise - assert.verifySteps(["cids=2-3"]); + assert.verifySteps(["cids=2-3&_company_switching=1"]); }); QUnit.test("single company selected: toggling it off will keep it", async (assert) => { @@ -189,7 +189,10 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => { * [ ] Company 3 */ await click(scMenuEl.querySelectorAll(".toggle_company")[0]); - assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 1 }); + assert.deepEqual(scMenu.env.services.router.current.hash, { + cids: 1, + _company_switching: 1, + }); assert.deepEqual(scMenu.env.services.company.activeCompanyIds, [1]); assert.strictEqual(scMenu.env.services.company.currentCompany.id, 1); assert.containsN(scMenuEl, "[data-company-id] .fa-check-squarqe", 0); @@ -222,7 +225,7 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => { * [ ] Company 3 */ await click(scMenuEl.querySelectorAll(".log_into")[1]); - assert.verifySteps(["cids=2"]); + assert.verifySteps(["cids=2&_company_switching=1"]); }); QUnit.test("multi company mode: log into a non selected company", async (assert) => { @@ -252,7 +255,7 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => { * [x] **Company 3** */ await click(scMenuEl.querySelectorAll(".log_into")[1]); - assert.verifySteps(["cids=2"]); + assert.verifySteps(["cids=2&_company_switching=1"]); }); QUnit.test("multi company mode: log into an already selected company", async (assert) => { @@ -282,7 +285,7 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => { * [x] Company 3 -> log into */ await click(scMenuEl.querySelectorAll(".log_into")[2]); - assert.verifySteps(["cids=3"]); + assert.verifySteps(["cids=3&_company_switching=1"]); }); QUnit.test("companies can be logged in even if some toggled within delay", async (assert) => { @@ -313,6 +316,6 @@ QUnit.module("MobileSwitchCompanyMenu", (hooks) => { await click(scMenuEl.querySelectorAll(".toggle_company")[2]); await click(scMenuEl.querySelectorAll(".toggle_company")[0]); await click(scMenuEl.querySelectorAll(".log_into")[1]); - assert.verifySteps(["cids=2"]); + assert.verifySteps(["cids=2&_company_switching=1"]); }); }); diff --git a/addons/web/static/tests/webclient/switch_company_menu_tests.js b/addons/web/static/tests/webclient/switch_company_menu_tests.js index 858114652e5..23898cbcd9d 100644 --- a/addons/web/static/tests/webclient/switch_company_menu_tests.js +++ b/addons/web/static/tests/webclient/switch_company_menu_tests.js @@ -138,7 +138,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => { ["true", "false", "false", "false", "false"] ); await prom; - assert.verifySteps(["cids=3-2"]); + assert.verifySteps(["cids=3-2&_company_switching=1"]); }); QUnit.test("can toggle multiple companies at once", async (assert) => { @@ -181,7 +181,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => { assert.verifySteps([]); await prom; // await toggle promise - assert.verifySteps(["cids=2-1-4-5"]); + assert.verifySteps(["cids=2-1-4-5&_company_switching=1"]); }); QUnit.test("single company selected: toggling it off will keep it", async (assert) => { @@ -217,7 +217,10 @@ QUnit.module("SwitchCompanyMenu", (hooks) => { * [ ] Hulk */ await click(target.querySelectorAll(".toggle_company")[0]); - assert.deepEqual(scMenu.env.services.router.current.hash, { cids: 3 }); + assert.deepEqual(scMenu.env.services.router.current.hash, { + cids: 3, + _company_switching: 1, + }); assert.deepEqual(scMenu.env.services.company.activeCompanyIds, [3]); assert.strictEqual(scMenu.env.services.company.currentCompany.id, 3); assert.containsOnce(target, ".dropdown-menu", "dropdown is still opened"); @@ -256,7 +259,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => { */ await click(target.querySelectorAll(".log_into")[1]); assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed"); - assert.verifySteps(["cids=2"]); + assert.verifySteps(["cids=2&_company_switching=1"]); }); QUnit.test("multi company mode: log into a non selected company", async (assert) => { @@ -291,7 +294,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => { */ await click(target.querySelectorAll(".log_into")[1]); assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed"); - assert.verifySteps(["cids=2"]); + assert.verifySteps(["cids=2&_company_switching=1"]); }); QUnit.test("multi company mode: log into an already selected company", async (assert) => { @@ -326,7 +329,7 @@ QUnit.module("SwitchCompanyMenu", (hooks) => { */ await click(target.querySelectorAll(".log_into")[2]); assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed"); - assert.verifySteps(["cids=1-4-5"]); + assert.verifySteps(["cids=1-4-5&_company_switching=1"]); }); QUnit.test("companies can be logged in even if some toggled within delay", async (assert) => { @@ -362,6 +365,6 @@ QUnit.module("SwitchCompanyMenu", (hooks) => { await click(target.querySelectorAll(".toggle_company")[0]); await click(target.querySelectorAll(".log_into")[1]); assert.containsNone(target, ".dropdown-menu", "dropdown is directly closed"); - assert.verifySteps(["cids=2"]); + assert.verifySteps(["cids=2&_company_switching=1"]); }); }); diff --git a/odoo/addons/test_main_flows/__init__.py b/odoo/addons/test_main_flows/__init__.py index 8fd8d754270..073920d5e46 100644 --- a/odoo/addons/test_main_flows/__init__.py +++ b/odoo/addons/test_main_flows/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +from .models import model_multicompany # # Conditional installation of enterprise modules. diff --git a/odoo/addons/test_main_flows/__manifest__.py b/odoo/addons/test_main_flows/__manifest__.py index 0e54ae6371b..27c18420025 100644 --- a/odoo/addons/test_main_flows/__manifest__.py +++ b/odoo/addons/test_main_flows/__manifest__.py @@ -11,10 +11,11 @@ It will install some main apps and will try to execute the most important action 'depends': ['web_tour', 'crm', 'sale_timesheet', 'purchase_stock', 'mrp', 'account'], 'installable': True, 'post_init_hook': '_auto_install_enterprise_dependencies', + 'data': ['models/ir.model.access.csv'], 'assets': { 'web.assets_tests': [ # inside . - 'test_main_flows/static/tests/tours/main_flow.js', + 'test_main_flows/static/tests/tours/*.js', ], }, 'license': 'LGPL-3', diff --git a/odoo/addons/test_main_flows/models/ir.model.access.csv b/odoo/addons/test_main_flows/models/ir.model.access.csv new file mode 100644 index 00000000000..6fa5c80838b --- /dev/null +++ b/odoo/addons/test_main_flows/models/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_model_multicompany,access_model_multicompany,model_test_model_multicompany,base.group_user,1,0,0,0 diff --git a/odoo/addons/test_main_flows/models/model_multicompany.py b/odoo/addons/test_main_flows/models/model_multicompany.py new file mode 100644 index 00000000000..2a2827196fb --- /dev/null +++ b/odoo/addons/test_main_flows/models/model_multicompany.py @@ -0,0 +1,8 @@ +from odoo import models, fields + +class ModelMultiCompany(models.Model): + _name = "test.model_multicompany" + _description = "test multicompany model" + + name = fields.Char() + company_id = fields.Many2one("res.company") diff --git a/odoo/addons/test_main_flows/static/tests/tours/switch_company_access_error_tour.js b/odoo/addons/test_main_flows/static/tests/tours/switch_company_access_error_tour.js new file mode 100644 index 00000000000..fb65d9050c4 --- /dev/null +++ b/odoo/addons/test_main_flows/static/tests/tours/switch_company_access_error_tour.js @@ -0,0 +1,71 @@ +/** @odoo-module **/ +import { registry } from "@web/core/registry"; + +const _console = window.console; +function assertEqual(actual, expected, msg = "") { + if (actual !== expected) { + const description = msg ? ` ${msg}` : ""; + _console.error(`Assert failed: expected: ${expected} ; got: ${actual}.${description}`); + } +} + +registry.category("web_tour.tours").add("test_company_switch_access_error", { + test: true, + steps: () => [ + { + trigger: ".o_list_view", + run() { + assertEqual( + JSON.stringify( + Array.from(this.$anchor[0].querySelectorAll(".o_data_cell")).map( + (n) => n.innerText + ) + ), + JSON.stringify(["p1", "p2"]) + ); + }, + }, + { + trigger: ".o_list_view .o_data_cell:contains(p2)", + }, + { + trigger: ".o_form_view .o_last_breadcrumb_item:contains(p2)", + isCheck: true, + }, + { + trigger: ".o_switch_company_menu button", + }, + { + trigger: + ".o_switch_company_menu .dropdown-item:contains(second company) .toggle_company", + }, + { + trigger: ".o_view_controller.o_list_view", + }, + { + trigger: ".o_view_controller.o_list_view", + async run() { + assertEqual( + JSON.stringify( + Array.from(this.$anchor[0].querySelectorAll(".o_data_cell")).map( + (n) => n.innerText + ) + ), + JSON.stringify(["p1"]) + ); + assertEqual( + document.querySelector("header.o_navbar .o_menu_brand").innerText, + "model_multicompany_menu" + ); + const url = new URL(window.location); + const hash = new URLSearchParams(url.hash.slice(1)); + assertEqual(hash.get("model"), "test.model_multicompany"); + assertEqual(hash.has("action"), true); + assertEqual(hash.has("menu_id"), true); + assertEqual(hash.get("view_type"), "list"); + assertEqual(hash.has("_company_switching"), false); + }, + isCheck: true, + }, + ], +}); diff --git a/odoo/addons/test_main_flows/tests/test_flow.py b/odoo/addons/test_main_flows/tests/test_flow.py index 51fbb7b6e46..69a029808d4 100644 --- a/odoo/addons/test_main_flows/tests/test_flow.py +++ b/odoo/addons/test_main_flows/tests/test_flow.py @@ -1,4 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import Command +from odoo.tools import mute_logger import odoo.tests @@ -72,6 +74,38 @@ class TestUi(BaseTestUi): def test_01_main_flow_tour(self): self.main_flow_tour() + def test_company_switch_access_error(self): + company1 = self.env.company + company2 = self.env["res.company"].create({"name":"second company"}) + self.env["res.users"].browse(2).write({ + "company_ids": [Command.clear(), Command.link(company1.id), Command.link(company2.id)] + }) + + self.env["ir.rule"].create({ + "name": "multiCompany rule", + "domain_force": '["|", ("company_id", "=", False), ("company_id", "in", company_ids)]', + "model_id": self.env["ir.model"]._get("test.model_multicompany").id + }) + + self.env["test.model_multicompany"].create({"name": "p1"}) + self.env["test.model_multicompany"].create({"name": "p2", "company_id": company2.id}) + + act_window = self.env["ir.actions.act_window"].create({ + "name": "model_multicompany_action", + "res_model": "test.model_multicompany", + "view_ids": [Command.create({"view_mode": "tree"}), Command.create({"view_mode": "form"})] + }) + + self.env["ir.ui.menu"].create({ + "name": "model_multicompany_menu", + "action": f"ir.actions.act_window,{act_window.id}", + }) + + current_companies = "%s-%s" % (company1.id, company2.id) + with mute_logger("odoo.http"): + self.start_tour(f"/web#action={act_window.id}&cids={current_companies}", "test_company_switch_access_error", login="admin") + + @odoo.tests.tagged('post_install', '-at_install') class TestUiMobile(BaseTestUi):