[FIX] web: reload on usable view when switch company does AccessError

Be logged in multiple companies (A and B).
Be on a form view of some record which is visible only on company B via ir.rules.

With the company switcher, unlog from company B.

Before this commit, the user received an AccessError and arrived on a blank webclient.
To say the least, it was rather inelegant.

After this commit, the user ends up on the multi-record view of that model, provided that it is available for use.

Task-3029616

closes odoo/odoo#141796

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Lucas Perais
2023-11-17 16:11:50 +00:00
parent 374aa015d6
commit e08ddc21a6
10 changed files with 170 additions and 16 deletions
@@ -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
},
@@ -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]);
@@ -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"]);
});
});
@@ -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"]);
});
});
+1 -1
View File
@@ -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.
+2 -1
View File
@@ -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',
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_model_multicompany access_model_multicompany model_test_model_multicompany base.group_user 1 0 0 0
@@ -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")
@@ -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,
},
],
});
@@ -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):