diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js index 590d5e62fc6a4759a033fff072bb67fcdc508aed..1cf1150ab764d535ef2d42cf7c9fedaed21e6374 100644 --- a/addons/web/static/src/js/core/py_utils.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -254,14 +254,29 @@ function tz_offset() { function pycontext() { + const d = new Date(); + const today = `${ + String(d.getFullYear()).padStart(4, "0")}-${ + String(d.getMonth() + 1).padStart(2, "0")}-${ + String(d.getDate()).padStart(2, "0")}`; + const now = `${ + String(d.getUTCFullYear()).padStart(4, "0")}-${ + String(d.getUTCMonth() + 1).padStart(2, "0")}-${ + String(d.getUTCDate()).padStart(2, "0")} ${ + String(d.getUTCHours()).padStart(2, "0")}:${ + String(d.getUTCMinutes()).padStart(2, "0")}:${ + String(d.getUTCSeconds()).padStart(2, "0")}`; + + const { datetime, relativedelta, time } = py.extras; return { - datetime: py.extras.datetime, - context_today: context_today, - time: py.extras.time, - relativedelta: py.extras.relativedelta, - current_date: py.PY_call( - py.extras.time.strftime, [py.str.fromJSON('%Y-%m-%d')]), - tz_offset: tz_offset, + current_date: today, + datetime, + time, + now, + today, + relativedelta, + context_today, + tz_offset, }; } diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index adefa997e9985fd1afd68a0d6d688dbaa4231988..8878de55a3ed0ef18d525e28bc6a2b8d1b033c9e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -88,6 +88,7 @@ var concurrency = require('web.concurrency'); var Context = require('web.Context'); var core = require('web.core'); var Domain = require('web.Domain'); +const pyUtils = require('web.py_utils'); var session = require('web.session'); var utils = require('web.utils'); var viewUtils = require('web.viewUtils'); @@ -3696,19 +3697,27 @@ var BasicModel = AbstractModel.extend({ } // Uses "current_company_id" because "company_id" would conflict with all the company_id fields // in general, the actual "company_id" field of the form should be used for m2o domains, not this fallback + let current_company_id; if (session.user_context.allowed_company_ids) { - var current_company = session.user_context.allowed_company_ids[0]; + current_company_id = session.user_context.allowed_company_ids[0]; } else { - var current_company = session.user_companies ? session.user_companies.current_company[0] : false; - } - return _.extend({ - active_id: evalContext.id || false, - active_ids: evalContext.id ? [evalContext.id] : [], - active_model: element.model, - current_date: moment().format('YYYY-MM-DD'), - id: evalContext.id || false, - current_company_id: current_company, - }, session.user_context, element.context, evalContext); + current_company_id = session.user_companies ? + session.user_companies.current_company[0] : + false; + } + return Object.assign( + { + active_id: evalContext.id || false, + active_ids: evalContext.id ? [evalContext.id] : [], + active_model: element.model, + current_company_id, + id: evalContext.id || false, + }, + pyUtils.context(), + session.user_context, + element.context, + evalContext, + ); }, /** * Returns the list of field names of the given element according to its diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js index f57478e12522c8d548ec9b78974faf386d95d766..b533c6587e94ef638541e035c0e3d035e7d1ff08 100644 --- a/addons/web/static/tests/views/basic_model_tests.js +++ b/addons/web/static/tests/views/basic_model_tests.js @@ -2037,8 +2037,9 @@ odoo.define('web.basic_model_tests', function (require) { }); QUnit.test('has a proper evaluation context', async function (assert) { - assert.expect(1); + assert.expect(6); + const unpatchDate = testUtils.mock.patchDate(1997, 0, 9, 12, 0, 0); this.params.fieldNames = Object.keys(this.data.partner.fields); this.params.res_id = 1; @@ -2048,8 +2049,24 @@ odoo.define('web.basic_model_tests', function (require) { }); var resultID = await model.load(this.params); - var record = model.get(resultID); - assert.deepEqual(record.evalContext, { + const { evalContext } = model.get(resultID); + assert.strictEqual(typeof evalContext.datetime, "object"); + assert.strictEqual(typeof evalContext.relativedelta, "object"); + assert.strictEqual(typeof evalContext.time, "object"); + assert.strictEqual(typeof evalContext.context_today, "function"); + assert.strictEqual(typeof evalContext.tz_offset, "function"); + const blackListedKeys = [ + "time", + "datetime", + "relativedelta", + "context_today", + "tz_offset", + ]; + // Remove uncomparable values from the evaluation context + for (const key of blackListedKeys) { + delete evalContext[key]; + } + assert.deepEqual(evalContext, { active: true, active_id: 1, active_ids: [1], @@ -2058,6 +2075,8 @@ odoo.define('web.basic_model_tests', function (require) { category: [12], current_company_id: false, current_date: moment().format('YYYY-MM-DD'), + today: moment().format('YYYY-MM-DD'), + now: moment().utc().format('YYYY-MM-DD HH:mm:ss'), date: "2017-01-25", display_name: "first partner", foo: "blip", @@ -2070,6 +2089,7 @@ odoo.define('web.basic_model_tests', function (require) { x_active: true, }, "should use the proper eval context"); model.destroy(); + unpatchDate(); }); QUnit.test('x2manys in contexts and domains are correctly evaluated', async function (assert) { diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index e467d39d97bc8905e27e8133b01eb1f4d9f011d5..7ecc9093b9a88ab5970e1acb3f8ecddac82b83e1 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -10320,6 +10320,95 @@ QUnit.module('Views', { list.destroy(); }); + + QUnit.test("Date in evaluation context works with date field", async function (assert) { + assert.expect(11); + + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + const unpatchDate = testUtils.mock.patchDate(1997, 0, 9, 12, 0, 0); + testUtils.mock.patch(BasicModel, { + _getEvalContext() { + const evalContext = this._super(...arguments); + assert.ok(dateRegex.test(evalContext.today)); + assert.strictEqual(evalContext.current_date, evalContext.today); + return evalContext; + }, + }); + + this.data.foo.fields.birthday = { string: "Birthday", type: 'date' }; + this.data.foo.records[0].birthday = "1997-01-08"; + this.data.foo.records[1].birthday = "1997-01-09"; + this.data.foo.records[2].birthday = "1997-01-10"; + + const list = await createView({ + arch: ` + <tree> + <field name="birthday" decoration-danger="birthday > today"/> + </tree>`, + data: this.data, + model: 'foo', + View: ListView, + }); + + assert.containsOnce(list, ".o_data_row .text-danger"); + + list.destroy(); + unpatchDate(); + testUtils.mock.unpatch(BasicModel); + }); + + QUnit.test("Datetime in evaluation context works with datetime field", async function (assert) { + assert.expect(6); + + const datetimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; + const unpatchDate = testUtils.mock.patchDate(1997, 0, 9, 12, 0, 0); + testUtils.mock.patch(BasicModel, { + _getEvalContext() { + const evalContext = this._super(...arguments); + assert.ok(datetimeRegex.test(evalContext.now)); + return evalContext; + }, + }); + + /** + * Returns "1997-01-DD HH:MM:00" with D, H and M holding current UTC values + * from patched date + (deltaMinutes) minutes. + * This is done to allow testing from any timezone since UTC values are + * calculated with the offset of the current browser. + */ + function dateStringDelta(deltaMinutes) { + const d = new Date(Date.now() + 1000 * 60 * deltaMinutes); + return `1997-01-${ + String(d.getUTCDate()).padStart(2, '0') + } ${ + String(d.getUTCHours()).padStart(2, '0') + }:${ + String(d.getUTCMinutes()).padStart(2, '0') + }:00`; + } + + // "datetime" field may collide with "datetime" object in context + this.data.foo.fields.birthday = { string: "Birthday", type: 'datetime' }; + this.data.foo.records[0].birthday = dateStringDelta(-30); + this.data.foo.records[1].birthday = dateStringDelta(0); + this.data.foo.records[2].birthday = dateStringDelta(+30); + + const list = await createView({ + arch: ` + <tree> + <field name="birthday" decoration-danger="birthday > now"/> + </tree>`, + data: this.data, + model: 'foo', + View: ListView, + }); + + assert.containsOnce(list, ".o_data_row .text-danger"); + + list.destroy(); + unpatchDate(); + testUtils.mock.unpatch(BasicModel); + }); }); }); diff --git a/odoo/tools/view_validation.py b/odoo/tools/view_validation.py index 6a20044f939a475fb3041243ceedb0b39ab22ff1..61202fb6b610df30e752766b6f8458414ff80b15 100644 --- a/odoo/tools/view_validation.py +++ b/odoo/tools/view_validation.py @@ -36,6 +36,8 @@ def _get_attrs_symbols(): 'datetime', 'relativedelta', 'current_date', + 'today', + 'now', 'abs', 'len', 'bool',