From 6c155e1713f70bd543570b0f2cb2e7c91d04203d Mon Sep 17 00:00:00 2001 From: Lucas Perais Date: Wed, 27 Jul 2022 14:07:17 +0000 Subject: [PATCH] [FIX] web: relationalModel: basic keys supported in record context Have an editable list view, displaying a many2many tags field. The m2m field has a context, mentioning `active_id`. (`context="{'default_field': active_id}"`) "basic context keys" is to be understood as the usual suspects: active_id, active_ids, current_company_id, active_model Type something in the m2m input, and selet create and edit, in order to create a new record in a dialog form view. Before this commit, there was a crash because active_id was not present in the evaluation context. After this commit, the new record opens in its form view, with the right value for the field that must have a default. closes odoo/odoo#96910 Signed-off-by: Lucas Perais (lpe) --- .../static/tests/timesheet_graph_tests.js | 2 +- .../web/static/src/views/relational_model.js | 9 +- .../tests/core/debug/debug_manager_tests.js | 3 + .../web/static/tests/helpers/mock_services.js | 19 +- .../views/fields/many2many_field_tests.js | 183 ++++++++++++++++++ addons/web/static/tests/views/helpers.js | 2 + .../view_dialogs/export_data_dialog_tests.js | 9 +- addons/web/static/tests/webclient/helpers.js | 8 +- 8 files changed, 219 insertions(+), 16 deletions(-) diff --git a/addons/hr_timesheet/static/tests/timesheet_graph_tests.js b/addons/hr_timesheet/static/tests/timesheet_graph_tests.js index 56a3dcf9275..972294c7715 100644 --- a/addons/hr_timesheet/static/tests/timesheet_graph_tests.js +++ b/addons/hr_timesheet/static/tests/timesheet_graph_tests.js @@ -36,7 +36,7 @@ QUnit.module('Views', function (hooks) { } } setupControlPanelServiceRegistry(); - serviceRegistry.add("company", companyService); + serviceRegistry.add("company", companyService, { force: true }); serviceRegistry.add("dialog", dialogService); }); diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index de72a14aad5..11cd7912650 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -457,6 +457,10 @@ export class Record extends DataPoint { return { // ... ...this.dataContext, + active_id: this.resId || false, + active_ids: this.resId ? [this.resId] : [], + active_model: this.resModel, + current_company_id: this.model.company.currentCompany.id, }; } @@ -3199,12 +3203,13 @@ export class StaticList extends DataPoint { StaticList.DEFAULT_LIMIT = 40; export class RelationalModel extends Model { - setup(params, { action, dialog, notification, rpc, user, view }) { + setup(params, { action, dialog, notification, rpc, user, view, company }) { this.action = action; this.dialogService = dialog; this.notificationService = notification; this.rpc = rpc; this.user = user; + this.company = company; this.viewService = view; this.orm = new RequestBatcherORM(rpc, user); this.keepLast = new KeepLast(); @@ -3342,7 +3347,7 @@ export class RelationalModel extends Model { } } -RelationalModel.services = ["action", "dialog", "notification", "rpc", "user", "view"]; +RelationalModel.services = ["action", "dialog", "notification", "rpc", "user", "view", "company"]; RelationalModel.Record = Record; RelationalModel.Group = Group; RelationalModel.DynamicRecordList = DynamicRecordList; diff --git a/addons/web/static/tests/core/debug/debug_manager_tests.js b/addons/web/static/tests/core/debug/debug_manager_tests.js index bba7f1992d2..096aa97f924 100644 --- a/addons/web/static/tests/core/debug/debug_manager_tests.js +++ b/addons/web/static/tests/core/debug/debug_manager_tests.js @@ -12,6 +12,7 @@ import { ActionDialog } from "@web/webclient/actions/action_dialog"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; import { makeTestEnv, utils } from "../../helpers/mock_env"; import { + fakeCompanyService, fakeCommandService, makeFakeDialogService, makeFakeLocalizationService, @@ -296,6 +297,7 @@ QUnit.module("DebugMenu", (hooks) => { } }; prepareRegistriesWithCleanup(); + registry.category("services").add("company", fakeCompanyService); patchWithCleanup(odoo, { debug: true, @@ -387,6 +389,7 @@ QUnit.module("DebugMenu", (hooks) => { } }; prepareRegistriesWithCleanup(); + registry.category("services").add("company", fakeCompanyService); patchWithCleanup(odoo, { debug: true, diff --git a/addons/web/static/tests/helpers/mock_services.js b/addons/web/static/tests/helpers/mock_services.js index c2f8aec04ce..1fa499ecc6c 100644 --- a/addons/web/static/tests/helpers/mock_services.js +++ b/addons/web/static/tests/helpers/mock_services.js @@ -10,7 +10,6 @@ import { effectService } from "@web/core/effects/effect_service"; import { objectToUrlEncodedString } from "@web/core/utils/urls"; import { registerCleanup } from "./cleanup"; import { patchWithCleanup } from "./utils"; -import { companyService } from "@web/webclient/company_service"; import { uiService } from "@web/core/ui/ui_service"; import { ConnectionAbortedError } from "../../src/core/network/rpc_service"; @@ -70,7 +69,8 @@ export function makeFakeRPCService(mockRPC) { .then(resolve) .catch(reject); }); - rpcProm.abort = () => rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); + rpcProm.abort = () => + rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); return rpcProm; }; }, @@ -79,7 +79,7 @@ export function makeFakeRPCService(mockRPC) { } export function makeMockXHR(response, sendCb, def) { - let MockXHR = function () { + const MockXHR = function () { return { _loadListener: null, url: "", @@ -269,6 +269,17 @@ export function makeFakeUserService(hasGroup = () => false) { }; } +export const fakeCompanyService = { + start() { + return { + availableCompanies: {}, + allowedCompanyIds: [], + currentCompany: {}, + setCompanies: () => {}, + }; + }, +}; + export function makeFakeHTTPService(getResponse, postResponse) { getResponse = getResponse || @@ -295,7 +306,7 @@ export function makeFakeHTTPService(getResponse, postResponse) { } export const mocks = { - company: () => companyService, + company: () => fakeCompanyService, command: () => fakeCommandService, cookie: () => fakeCookieService, effect: () => effectService, // BOI The real service ? Is this what we want ? diff --git a/addons/web/static/tests/views/fields/many2many_field_tests.js b/addons/web/static/tests/views/fields/many2many_field_tests.js index 8f56f04a870..90f7a1b18d1 100644 --- a/addons/web/static/tests/views/fields/many2many_field_tests.js +++ b/addons/web/static/tests/views/fields/many2many_field_tests.js @@ -16,6 +16,9 @@ import { import { editSearch, validateSearch } from "@web/../tests/search/helpers"; import { AutoComplete } from "@web/core/autocomplete/autocomplete"; import { Many2XAutocomplete } from "@web/views/fields/relational_utils"; +import { session } from "@web/session"; +import { companyService } from "@web/webclient/company_service"; +import { registry } from "@web/core/registry"; let target; let serverData; @@ -1869,4 +1872,184 @@ QUnit.module("Fields", (hooks) => { assert.containsOnce(target, ".modal"); assert.strictEqual(target.querySelector(".modal-title").textContent, "Search: pokemon"); }); + + QUnit.test("many2many basic keys in field evalcontext -- in list", async (assert) => { + assert.expect(6); + serverData.models.partner_type.fields.partner_id = { + string: "Partners", + type: "many2one", + relation: "partner", + }; + serverData.views = { + "partner_type,false,form": `
`, + }; + + patchWithCleanup(session, { + user_companies: { + allowed_companies: { + 3: { id: 3, name: "Hermit", sequence: 1 }, + 2: { id: 2, name: "Herman's", sequence: 2 }, + 1: { id: 1, name: "Heroes TM", sequence: 3 }, + }, + current_company: 3, + }, + }); + + registry.category("services").add("company", companyService, { force: true }); + + patchWithCleanup(AutoComplete, { + timeout: 0, + }); + + await makeView({ + type: "list", + resModel: "partner", + serverData, + arch: ` + + + `, + mockRPC(route, args) { + if (args.method === "onchange") { + assert.strictEqual(args.kwargs.context.default_partner_id, 1); + assert.strictEqual(args.kwargs.context.model, "partner"); + assert.deepEqual(args.kwargs.context.ids, [1]); + assert.strictEqual(args.kwargs.context.company_id, 3); + } + }, + }); + + await click(target.querySelector(".o_data_cell")); + await editInput(target, ".o_field_many2many_selection input", "indianapolis"); + await clickOpenedDropdownItem(target, "timmy", "Create and edit..."); + assert.containsOnce(target, ".modal .o_field_many2one"); + assert.strictEqual( + target.querySelector(".modal .o_field_many2one input").value, + "first record" + ); + }); + + QUnit.test("many2many basic keys in field evalcontext -- in form", async (assert) => { + assert.expect(6); + serverData.models.partner_type.fields.partner_id = { + string: "Partners", + type: "many2one", + relation: "partner", + }; + serverData.views = { + "partner_type,false,form": `
`, + }; + + patchWithCleanup(session, { + user_companies: { + allowed_companies: { + 3: { id: 3, name: "Hermit", sequence: 1 }, + 2: { id: 2, name: "Herman's", sequence: 2 }, + 1: { id: 1, name: "Heroes TM", sequence: 3 }, + }, + current_company: 3, + }, + }); + + registry.category("services").add("company", companyService, { force: true }); + + patchWithCleanup(AutoComplete, { + timeout: 0, + }); + + await makeView({ + type: "form", + resId: 1, + resModel: "partner", + serverData, + arch: ` +
+ + `, + mockRPC(route, args) { + if (args.method === "onchange") { + assert.strictEqual(args.kwargs.context.default_partner_id, 1); + assert.strictEqual(args.kwargs.context.model, "partner"); + assert.deepEqual(args.kwargs.context.ids, [1]); + assert.strictEqual(args.kwargs.context.company_id, 3); + } + }, + }); + + await clickEdit(target); + await editInput(target, ".o_field_many2many_selection input", "indianapolis"); + await clickOpenedDropdownItem(target, "timmy", "Create and edit..."); + assert.containsOnce(target, ".modal .o_field_many2one"); + assert.strictEqual( + target.querySelector(".modal .o_field_many2one input").value, + "first record" + ); + }); + + QUnit.test( + "many2many basic keys in field evalcontext -- in a x2many in form", + async (assert) => { + assert.expect(6); + serverData.models.partner_type.fields.partner_id = { + string: "Partners", + type: "many2one", + relation: "partner", + }; + serverData.views = { + "partner_type,false,form": `
`, + }; + + const rec = serverData.models.partner.records.find(({ id }) => id === 2); + rec.p = [1]; + + patchWithCleanup(session, { + user_companies: { + allowed_companies: { + 3: { id: 3, name: "Hermit", sequence: 1 }, + 2: { id: 2, name: "Herman's", sequence: 2 }, + 1: { id: 1, name: "Heroes TM", sequence: 3 }, + }, + current_company: 3, + }, + }); + registry.category("services").add("company", companyService, { force: true }); + + patchWithCleanup(AutoComplete, { + timeout: 0, + }); + + await makeView({ + type: "form", + resId: 2, + resModel: "partner", + serverData, + arch: ` +
+ + + + + +
`, + mockRPC(route, args) { + if (args.method === "onchange") { + assert.strictEqual(args.kwargs.context.default_partner_id, 1); + assert.strictEqual(args.kwargs.context.model, "partner"); + assert.deepEqual(args.kwargs.context.ids, [1]); + assert.strictEqual(args.kwargs.context.company_id, 3); + } + }, + }); + + await clickEdit(target); + await click(target, ".o_data_cell"); + await editInput(target, ".o_field_many2many_selection input", "indianapolis"); + await clickOpenedDropdownItem(target, "timmy", "Create and edit..."); + assert.containsOnce(target, ".modal .o_field_many2one"); + assert.strictEqual( + target.querySelector(".modal .o_field_many2one input").value, + "first record" + ); + } + ); }); diff --git a/addons/web/static/tests/views/helpers.js b/addons/web/static/tests/views/helpers.js index a78f7b1d8b0..4a7efbcf4f6 100644 --- a/addons/web/static/tests/views/helpers.js +++ b/addons/web/static/tests/views/helpers.js @@ -11,6 +11,7 @@ import { } from "../search/helpers"; import { addLegacyMockEnvironment } from "../webclient/helpers"; import { + fakeCompanyService, makeFakeLocalizationService, makeFakeRouterService, makeFakeUserService, @@ -105,4 +106,5 @@ export function setupViewRegistries() { serviceRegistry.add("localization", makeFakeLocalizationService()), { force: true }; serviceRegistry.add("dialog", dialogService), { force: true }; serviceRegistry.add("popover", popoverService), { force: true }; + serviceRegistry.add("company", fakeCompanyService); } diff --git a/addons/web/static/tests/views/view_dialogs/export_data_dialog_tests.js b/addons/web/static/tests/views/view_dialogs/export_data_dialog_tests.js index 8f00b3c45fa..836d605b3be 100644 --- a/addons/web/static/tests/views/view_dialogs/export_data_dialog_tests.js +++ b/addons/web/static/tests/views/view_dialogs/export_data_dialog_tests.js @@ -12,10 +12,8 @@ import { triggerEvent, mockTimeout, } from "@web/../tests/helpers/utils"; -import { makeView } from "@web/../tests/views/helpers"; -import { dialogService } from "@web/core/dialog/dialog_service"; +import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { registry } from "@web/core/registry"; -import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers"; import { makeFakeUserService } from "../../helpers/mock_services"; const serviceRegistry = registry.category("services"); @@ -97,13 +95,12 @@ QUnit.module("ViewDialogs", (hooks) => { }, }; target = getFixture(); - setupControlPanelServiceRegistry(); + setupViewRegistries(); function hasGroup(group) { return group === "base.group_allow_export"; } serviceRegistry.add("user", makeFakeUserService(hasGroup), { force: true }); - serviceRegistry.add("dialog", dialogService); fetchedFields = { root: [ @@ -595,7 +592,7 @@ QUnit.module("ViewDialogs", (hooks) => { QUnit.test("Export dialog: display on small screen after resize", async function (assert) { const { execRegisteredTimeouts } = mockTimeout(); - let ui = { + const ui = { activateElement: () => {}, deactivateElement: () => {}, size: 4, diff --git a/addons/web/static/tests/webclient/helpers.js b/addons/web/static/tests/webclient/helpers.js index 7451b1a7360..326322b7027 100644 --- a/addons/web/static/tests/webclient/helpers.js +++ b/addons/web/static/tests/webclient/helpers.js @@ -33,6 +33,7 @@ import { registerCleanup } from "../helpers/cleanup"; import { makeTestEnv } from "../helpers/mock_env"; import { fakeTitleService, + fakeCompanyService, makeFakeLocalizationService, makeFakeRouterService, makeFakeHTTPService, @@ -74,7 +75,7 @@ export function setupWebClientRegistries() { options: { sequence: 0 }, }, }; - for (let [key, { value, options }] of Object.entries(favoriveMenuItems)) { + for (const [key, { value, options }] of Object.entries(favoriveMenuItems)) { if (!favoriteMenuRegistry.contains(key)) { favoriteMenuRegistry.add(key, value, options); } @@ -97,8 +98,9 @@ export function setupWebClientRegistries() { ui: () => uiService, user: () => makeFakeUserService(), view: () => viewService, + company: () => fakeCompanyService, }; - for (let serviceName in services) { + for (const serviceName in services) { if (!serviceRegistry.contains(serviceName)) { serviceRegistry.add(serviceName, services[serviceName]()); } @@ -195,7 +197,7 @@ export async function addLegacyMockEnvironment(env, legacyParams = {}) { serviceRegistry.add("legacy_action_manager", legacyActionManagerService); serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv)); // deploy wowl services into the legacy env. - const wowlToLegacyServiceMappers = registry.category('wowlToLegacyServiceMappers').getEntries(); + const wowlToLegacyServiceMappers = registry.category("wowlToLegacyServiceMappers").getEntries(); for (const [legacyServiceName, wowlToLegacyServiceMapper] of wowlToLegacyServiceMappers) { serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv)); }