diff --git a/addons/web/static/src/core/context.js b/addons/web/static/src/core/context.js index aba96fc5897..8be6cc9f308 100644 --- a/addons/web/static/src/core/context.js +++ b/addons/web/static/src/core/context.js @@ -1,6 +1,7 @@ /** @odoo-module **/ -import { evaluateExpr } from "./py_js/py"; +import { evaluateExpr, parseExpr } from "./py_js/py"; +import { evaluate } from "./py_js/py_interpreter"; /** * @typedef {{[key: string]: any}} Context @@ -27,3 +28,25 @@ export function makeContext(contexts, initialEvaluationContext) { } return context; } + +/** + * Allow to evaluate a context with an incomplete evaluation context. The evaluated context only + * contains keys whose values are static or can be evaluated with the given evaluation context. + * + * @param {string} context + * @param {Object} [evaluationContext={}] + * @returns {Context} + */ +export function evalPartialContext(_context, evaluationContext = {}) { + const ast = parseExpr(_context); + const context = {}; + for (const key in ast.value) { + const value = ast.value[key]; + try { + context[key] = evaluate(value, evaluationContext); + } catch { + // ignore this key as we can't evaluate its value + } + } + return context; +} diff --git a/addons/web/static/src/model/relational_model/utils.js b/addons/web/static/src/model/relational_model/utils.js index 1115737ebeb..554c0d1b23b 100644 --- a/addons/web/static/src/model/relational_model/utils.js +++ b/addons/web/static/src/model/relational_model/utils.js @@ -1,7 +1,7 @@ /* @odoo-module */ import { markup, onWillDestroy, onWillStart, onWillUpdateProps, useComponent } from "@odoo/owl"; -import { makeContext } from "@web/core/context"; +import { evalPartialContext, makeContext } from "@web/core/context"; import { deserializeDate, deserializeDateTime } from "@web/core/l10n/dates"; import { x2ManyCommands } from "@web/core/orm_service"; import { Deferred } from "@web/core/utils/concurrency"; @@ -204,49 +204,6 @@ export function getFieldContext( }; } -const SENTINEL = Symbol("sentinel"); -function _getFieldContextSpec( - fieldName, - activeFields, - fields, - evalContext, - parentActiveFields = null -) { - const rawContext = activeFields[fieldName].context; - if (!rawContext || rawContext === "{}") { - return fields[fieldName].context; - } - - evalContext = { - ids: SENTINEL, - active_id: SENTINEL, - active_ids: SENTINEL, - active_model: SENTINEL, - ...evalContext, - id: SENTINEL, - }; - for (const fieldName in activeFields) { - evalContext[fieldName] = SENTINEL; - } - if (parentActiveFields) { - evalContext.parent = {}; - for (const fieldName in parentActiveFields) { - evalContext.parent[fieldName] = SENTINEL; - } - } - const evaluatedContext = makeContext([fields[fieldName].context, rawContext], evalContext); - for (const key in evaluatedContext) { - if (evaluatedContext[key] === SENTINEL || key.startsWith("default_")) { - // FIXME: this isn't perfect, a value might be evaluted to something else - // than the symbol because of the symbol - delete evaluatedContext[key]; - } - } - if (Object.keys(evaluatedContext).length > 0) { - return evaluatedContext; - } -} - export function getFieldsSpec( activeFields, fields, @@ -283,14 +240,13 @@ export function getFieldsSpec( fieldsSpec[fieldName].fields = { display_name: {} }; } if (["many2one", "one2many", "many2many"].includes(fields[fieldName].type)) { - const context = _getFieldContextSpec( - fieldName, - activeFields, - fields, - evalContext, - parentActiveFields - ); - if (context) { + let context = activeFields[fieldName].context; + if (!context || context === "{}") { + context = fields[fieldName].context || {}; + } else { + context = evalPartialContext(context, evalContext); + } + if (Object.keys(context).length > 0) { fieldsSpec[fieldName].context = context; } } diff --git a/addons/web/static/tests/core/context_tests.js b/addons/web/static/tests/core/context_tests.js index b09a4f8cd3a..75fe0ca6982 100644 --- a/addons/web/static/tests/core/context_tests.js +++ b/addons/web/static/tests/core/context_tests.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { makeContext } from "@web/core/context"; +import { evalPartialContext, makeContext } from "@web/core/context"; -QUnit.module("utils", {}, () => { +QUnit.module("Context", {}, () => { QUnit.module("makeContext"); QUnit.test("return empty context", (assert) => { @@ -42,4 +42,38 @@ QUnit.module("utils", {}, () => { assert.deepEqual(makeContext(["{'a': a + 1}"], { a: 1 }), { a: 2 }); assert.deepEqual(makeContext(["{'b': a + 1}"], { a: 1 }), { b: 2 }); }); + + QUnit.module("evalPartialContext"); + + QUnit.test("static contexts", (assert) => { + assert.deepEqual(evalPartialContext("{}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': 1}", {}), { a: 1 }); + assert.deepEqual(evalPartialContext("{'a': 'b'}", {}), { a: "b" }); + assert.deepEqual(evalPartialContext("{'a': true}", {}), { a: true }); + assert.deepEqual(evalPartialContext("{'a': None}", {}), { a: null }); + }); + + QUnit.test("complete dynamic contexts", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a, 'b': 1}", { a: 2 }), { a: 2, b: 1 }); + }); + + QUnit.test("partial dynamic contexts", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a, 'b': 1}", {}), { b: 1 }); + assert.deepEqual(evalPartialContext("{'a': a, 'b': b}", { a: 2 }), { a: 2 }); + }); + + QUnit.test("value of type obj (15)", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a.b.c}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a.b.c}", { a: {} }), {}); + assert.deepEqual(evalPartialContext("{'a': a.b.c}", { a: { b: { c: 2 } } }), { a: 2 }); + }); + + QUnit.test("value of type op (14)", (assert) => { + assert.deepEqual(evalPartialContext("{'a': a + 1}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a + b}", {}), {}); + assert.deepEqual(evalPartialContext("{'a': a + b}", { a: 2 }), {}); + assert.deepEqual(evalPartialContext("{'a': a + 1}", { a: 2 }), { a: 3 }); + assert.deepEqual(evalPartialContext("{'a': a + b}", { a: 2, b: 3 }), { a: 5 }); + }); }); diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index eb27925d1c6..9987c3aeea3 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -3833,6 +3833,52 @@ QUnit.module("Views", (hooks) => { await click(target.querySelector(".o_field_x2many_list_row_add a")); }); + QUnit.test("form with one2many with dynamic context", async (assert) => { + assert.expect(2); + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + + + + `, + resId: 1, + mockRPC(route, args) { + if (args.method === "web_read") { + assert.deepEqual(args.kwargs.specification, { + display_name: {}, + int_field: {}, + p: { + fields: { + foo: {}, + }, + context: { static: 4 }, + limit: 40, + order: "", + }, + }); + } + if (args.method === "onchange2") { + assert.deepEqual(args.kwargs.context, { + dynamic: 20, + lang: "en", + static: 4, + tz: "taht", + uid: 7, + }); + } + }, + }); + + await addRow(target); + }); + QUnit.test("reference field in one2many list", async function (assert) { serverData.models.partner.records[0].reference = "partner,2"; serverData.views = {