From 9d1786c3613b44a59bfe2ffca54a62502e82142b Mon Sep 17 00:00:00 2001 From: Bruno Boi Date: Mon, 1 Aug 2022 14:28:39 +0000 Subject: [PATCH] [IMP] web: account for local timezone when formatting dates formatDate and formatDateTime had their default 'timezone' option set to false but this was changed to true in order to have the same behavior as the legacy formatDate and formatDateTime functions. Part-of: odoo/odoo#96946 --- .../static/src/models/tracking_value_item.js | 6 +- addons/web/static/src/core/l10n/dates.js | 29 ++-- addons/web/static/src/legacy/debug_manager.js | 8 +- .../src/views/basic_relational_model.js | 2 +- addons/web/static/src/views/debug_items.js | 8 +- .../views/fields/datetime/datetime_field.js | 2 +- .../web/static/src/views/relational_model.js | 10 +- .../web/static/tests/core/l10n/dates_tests.js | 133 +++++++++++++----- 8 files changed, 121 insertions(+), 77 deletions(-) diff --git a/addons/mail/static/src/models/tracking_value_item.js b/addons/mail/static/src/models/tracking_value_item.js index 58614331233..7fc320a15d3 100644 --- a/addons/mail/static/src/models/tracking_value_item.js +++ b/addons/mail/static/src/models/tracking_value_item.js @@ -44,8 +44,10 @@ registerModel({ return format.date(moment.utc(this.value)); } return format.date(this.value); - case 'datetime': - return formatters.get("datetime")(deserializeDateTime(this.value), { timezone: true }); + case 'datetime': { + const value = this.value ? deserializeDateTime(this.value) : this.value; + return formatters.get("datetime")(value, { timezone: true }); + } case 'float': return format.float(this.value); case 'integer': diff --git a/addons/web/static/src/core/l10n/dates.js b/addons/web/static/src/core/l10n/dates.js index 74adc65efbb..94c9719a5bd 100644 --- a/addons/web/static/src/core/l10n/dates.js +++ b/addons/web/static/src/core/l10n/dates.js @@ -9,6 +9,7 @@ const { DateTime, Settings } = luxon; const SERVER_DATE_FORMAT = "yyyy-MM-dd"; const SERVER_TIME_FORMAT = "HH:mm:ss"; +const SERVER_DATETIME_FORMAT = `${SERVER_DATE_FORMAT} ${SERVER_TIME_FORMAT}`; // ----------------------------------------------------------------------------- // Helpers @@ -173,11 +174,11 @@ export function formatDate(value, options = {}) { * * Default=the session localization format. * - * @param {boolean} [options.timezone] + * @param {boolean} [options.timezone=true] * - True = input will be set in local time before being formatted. * - False = input will be set in UTC time before being formatted. * - * Default=false. + * Default=true. * * @param {string} [options.numberingSystem] * Provided numbering system used to parse the input value. @@ -193,7 +194,7 @@ export function formatDateTime(value, options = {}) { } const format = options.format || localization.dateTimeFormat; const numberingSystem = options.numberingSystem || Settings.defaultNumberingSystem || "latn"; - const zone = options.timezone ? "default" : "utc"; + const zone = !("timezone" in options) || options.timezone ? "default" : "utc"; value = value.setZone(zone, { keepLocaltime: options.timezone }); return value.toFormat(format, { numberingSystem }); } @@ -236,7 +237,7 @@ export function parseDate(value, options = {}) { * * Default=the session localization format * - * @param {boolean} [options.timezone] + * @param {boolean} [options.timezone=false] * - True = input value is considered being in localtime. * - False = input value is considered being in utc time, and the returned * value will have the UTC zone. @@ -345,10 +346,7 @@ export function parseDateTime(value, options = {}) { * @returns {DateTime | false} */ export function deserializeDate(value) { - return parseDate(value, { - format: SERVER_DATE_FORMAT, - numberingSystem: "latn", - }); + return DateTime.fromSQL(value, { zone: "utc", numberingSystem: "latn" }); } /** @@ -357,10 +355,7 @@ export function deserializeDate(value) { * @returns {DateTime | false} */ export function deserializeDateTime(value) { - return parseDateTime(value, { - format: `${SERVER_DATE_FORMAT} ${SERVER_TIME_FORMAT}`, - numberingSystem: "latn", - }); + return DateTime.fromSQL(value, { zone: "utc", numberingSystem: "latn" }); } /** @@ -369,10 +364,7 @@ export function deserializeDateTime(value) { * @returns {string} */ export function serializeDate(value) { - return formatDate(value, { - format: SERVER_DATE_FORMAT, - numberingSystem: "latn", - }); + return value.setZone("utc").toFormat(SERVER_DATE_FORMAT, { numberingSystem: "latn" }); } /** @@ -381,8 +373,5 @@ export function serializeDate(value) { * @returns {string} */ export function serializeDateTime(value) { - return formatDateTime(value, { - format: `${SERVER_DATE_FORMAT} ${SERVER_TIME_FORMAT}`, - numberingSystem: "latn", - }); + return value.setZone("utc").toFormat(SERVER_DATETIME_FORMAT, { numberingSystem: "latn" }); } diff --git a/addons/web/static/src/legacy/debug_manager.js b/addons/web/static/src/legacy/debug_manager.js index 7492e3caaab..f227636939c 100644 --- a/addons/web/static/src/legacy/debug_manager.js +++ b/addons/web/static/src/legacy/debug_manager.js @@ -60,12 +60,8 @@ class GetMetadataDialog extends Component { this.state.creator = formatMany2one(metadata.create_uid); this.state.lastModifiedBy = formatMany2one(metadata.write_uid); this.state.noupdate = metadata.noupdate; - this.state.createDate = formatDateTime(parseDateTime(metadata.create_date), { - timezone: true, - }); - this.state.writeDate = formatDateTime(parseDateTime(metadata.write_date), { - timezone: true, - }); + this.state.createDate = formatDateTime(parseDateTime(metadata.create_date)); + this.state.writeDate = formatDateTime(parseDateTime(metadata.write_date)); } } GetMetadataDialog.template = "web.DebugMenu.GetMetadataDialog"; diff --git a/addons/web/static/src/views/basic_relational_model.js b/addons/web/static/src/views/basic_relational_model.js index c6c7d15dc73..75f174b8ae6 100644 --- a/addons/web/static/src/views/basic_relational_model.js +++ b/addons/web/static/src/views/basic_relational_model.js @@ -479,7 +479,7 @@ export class Record extends DataPoint { if (data[fieldName]) { const deserialize = fieldType === "date" ? deserializeDate : deserializeDateTime; - data[fieldName] = deserialize(JSON.stringify(data[fieldName])); + data[fieldName] = deserialize(data[fieldName].toJSON()); } break; } diff --git a/addons/web/static/src/views/debug_items.js b/addons/web/static/src/views/debug_items.js index 6f816acef2c..30275d3b1fc 100644 --- a/addons/web/static/src/views/debug_items.js +++ b/addons/web/static/src/views/debug_items.js @@ -158,12 +158,8 @@ class GetMetadataDialog extends Component { this.state.noupdate = metadata.noupdate; this.state.creator = formatMany2one(metadata.create_uid); this.state.lastModifiedBy = formatMany2one(metadata.write_uid); - this.state.createDate = formatDateTime(parseDateTime(metadata.create_date), { - timezone: true, - }); - this.state.writeDate = formatDateTime(parseDateTime(metadata.write_date), { - timezone: true, - }); + this.state.createDate = formatDateTime(parseDateTime(metadata.create_date)); + this.state.writeDate = formatDateTime(parseDateTime(metadata.write_date)); } } GetMetadataDialog.template = "web.DebugMenu.GetMetadataDialog"; diff --git a/addons/web/static/src/views/fields/datetime/datetime_field.js b/addons/web/static/src/views/fields/datetime/datetime_field.js index ffdc6fe7a06..46cd731e8b0 100644 --- a/addons/web/static/src/views/fields/datetime/datetime_field.js +++ b/addons/web/static/src/views/fields/datetime/datetime_field.js @@ -10,7 +10,7 @@ const { Component } = owl; export class DateTimeField extends Component { get formattedValue() { - return formatDateTime(this.props.value, { timezone: true }); + return formatDateTime(this.props.value); } onDateTimeChanged(date) { diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index 71da7c7aa27..ace65a32271 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -2468,15 +2468,9 @@ export class Group extends DataPoint { return false; } if (type === "date") { - return serializeDate( - DateTime.fromFormat(range.to, "yyyy-MM-dd", { zone: "utc" }).minus({ - day: 1, - }) - ); + return serializeDate(deserializeDate(range.to).minus({ day: 1 })); } else { - return serializeDateTime( - DateTime.fromFormat(range.to, "yyyy-MM-dd HH:mm:ss").minus({ second: 1 }) - ); + return serializeDateTime(deserializeDateTime(range.to).minus({ second: 1 })); } } default: { diff --git a/addons/web/static/tests/core/l10n/dates_tests.js b/addons/web/static/tests/core/l10n/dates_tests.js index 866f60fff96..3f8fd314b9e 100644 --- a/addons/web/static/tests/core/l10n/dates_tests.js +++ b/addons/web/static/tests/core/l10n/dates_tests.js @@ -19,7 +19,7 @@ import session from "web.session"; import test_utils from "web.test_utils"; import { registerCleanup } from "../../helpers/cleanup"; import { defaultLocalization } from "../../helpers/mock_services"; -import { patchWithCleanup } from "../../helpers/utils"; +import { patchDate, patchWithCleanup } from "../../helpers/utils"; const { DateTime, Settings } = luxon; @@ -119,42 +119,51 @@ QUnit.module( unpatch(localization, "dateformat"); }); + QUnit.test("formatDate (with DateTime.fromISO)", async (assert) => { + patchWithCleanup(localization, { dateFormat: "MM/dd/yyyy" }); + + // NB: the local timezone in test env is UTC+1 + let date = DateTime.fromISO("2022-07-21T22:00:00Z"); + assert.strictEqual(formatDate(date), "07/21/2022"); + assert.strictEqual(formatDate(date, { timezone: true }), "07/21/2022"); + + date = DateTime.fromISO("2022-07-21T23:00:00Z"); + assert.strictEqual(formatDate(date), "07/21/2022"); + assert.strictEqual(formatDate(date, { timezone: true }), "07/22/2022"); + + date = DateTime.fromISO("2022-07-22T00:00:00Z"); + assert.strictEqual(formatDate(date), "07/22/2022"); + assert.strictEqual(formatDate(date, { timezone: true }), "07/22/2022"); + + // Here are the 3 same instants as above, but expressed from a different ISO string + date = DateTime.fromISO("2022-07-22T00:00:00+02:00"); + assert.strictEqual(formatDate(date), "07/21/2022"); + assert.strictEqual(formatDate(date, { timezone: true }), "07/21/2022"); + + date = DateTime.fromISO("2022-07-22T01:00:00+02:00"); + assert.strictEqual(formatDate(date), "07/21/2022"); + assert.strictEqual(formatDate(date, { timezone: true }), "07/22/2022"); + + date = DateTime.fromISO("2022-07-22T02:00:00+02:00"); + assert.strictEqual(formatDate(date), "07/22/2022"); + assert.strictEqual(formatDate(date, { timezone: true }), "07/22/2022"); + }); + QUnit.test("formatDateTime", async (assert) => { - patch(localization, "datetimeformat", { dateTimeFormat: "MM/dd/yyyy HH:mm:ss" }); - - let formatted = formatDateTime(DateTime.utc(2009, 5, 4, 12, 34, 23)); - let expected = "05/04/2009 12:34:23"; - assert.strictEqual(formatted, expected); - - formatted = formatDateTime(DateTime.utc(2009, 5, 4, 12, 34, 23), { timezone: false }); - assert.strictEqual(formatted, expected); - - formatted = formatDateTime(DateTime.utc(2009, 5, 4, 12, 34, 23), { timezone: true }); - expected = "05/04/2009 13:34:23"; - assert.strictEqual(formatted, expected); - - unpatch(localization, "datetimeformat"); + patchWithCleanup(localization, { dateTimeFormat: "MM/dd/yyyy HH:mm:ss" }); + const date = DateTime.utc(2009, 5, 4, 12, 34, 23); + assert.strictEqual(formatDateTime(date), "05/04/2009 13:34:23"); + assert.strictEqual(formatDateTime(date, { timezone: false }), "05/04/2009 12:34:23"); }); QUnit.test("formatDateTime (with different timezone offset)", async (assert) => { - patch(localization, "datetimeformat", { dateTimeFormat: "MM/dd/yyyy HH:mm:ss" }); - - let str = formatDateTime(DateTime.utc(2017, 1, 1, 10, 0, 0, 0)); - assert.strictEqual(str, "01/01/2017 10:00:00"); - str = formatDateTime(DateTime.utc(2017, 6, 1, 10, 0, 0, 0)); - assert.strictEqual(str, "06/01/2017 10:00:00"); - - str = formatDateTime(DateTime.utc(2017, 1, 1, 10, 0, 0, 0), { timezone: false }); - assert.strictEqual(str, "01/01/2017 10:00:00"); - str = formatDateTime(DateTime.utc(2017, 6, 1, 10, 0, 0, 0), { timezone: false }); - assert.strictEqual(str, "06/01/2017 10:00:00"); - - str = formatDateTime(DateTime.utc(2017, 1, 1, 10, 0, 0, 0), { timezone: true }); - assert.strictEqual(str, "01/01/2017 11:00:00"); - str = formatDateTime(DateTime.utc(2017, 6, 1, 10, 0, 0, 0), { timezone: true }); - assert.strictEqual(str, "06/01/2017 11:00:00"); - - unpatch(localization, "datetimeformat"); + patchWithCleanup(localization, { dateTimeFormat: "MM/dd/yyyy HH:mm:ss" }); + let date = DateTime.fromISO("2017-01-01T11:00:00+01:00"); + assert.strictEqual(formatDateTime(date), "01/01/2017 11:00:00"); + assert.strictEqual(formatDateTime(date, { timezone: false }), "01/01/2017 10:00:00"); + date = DateTime.fromISO("2017-06-01T11:00:00+02:00"); + assert.strictEqual(formatDateTime(date), "06/01/2017 10:00:00"); + assert.strictEqual(formatDateTime(date, { timezone: false }), "06/01/2017 09:00:00"); }); QUnit.test("parseDateTime", async (assert) => { @@ -223,6 +232,24 @@ QUnit.module( unpatch(localization, "weird loc"); }); + QUnit.test("parseDate", async (assert) => { + patchWithCleanup(localization, defaultLocalization); + + let str = "07/21/2022"; + assert.strictEqual(parseDate(str).toISO(), "2022-07-21T00:00:00.000Z"); + assert.strictEqual( + parseDate(str, { timezone: true }).toISO(), + "2022-07-21T00:00:00.000+01:00" + ); + + str = "07/22/2022"; + assert.strictEqual(parseDate(str).toISO(), "2022-07-22T00:00:00.000Z"); + assert.strictEqual( + parseDate(str, { timezone: true }).toISO(), + "2022-07-22T00:00:00.000+01:00" + ); + }); + QUnit.test("parseDate without separator", async (assert) => { const dateFormat = strftimeToLuxonFormat("%d.%m/%Y"); const timeFormat = strftimeToLuxonFormat("%H:%M:%S"); @@ -373,6 +400,24 @@ QUnit.module( assert.strictEqual(serializeDate(date), "2022-02-21"); }); + QUnit.test("serializeDate, with DateTime.now()", async (assert) => { + patchDate(2022, 1, 21, 16, 11, 42); + const date = DateTime.now(); + assert.strictEqual(date.toFormat("yyyy-MM-dd"), "2022-02-21"); + assert.strictEqual(serializeDate(date), "2022-02-21"); + }); + + QUnit.test("serializeDate, with DateTime.now(), midnight", async (assert) => { + patchDate(2022, 1, 21, 0, 0, 0); + const date = DateTime.now(); + assert.strictEqual(date.toFormat("yyyy-MM-dd"), "2022-02-21"); + assert.strictEqual( + serializeDate(date), + "2022-02-20", + "serializeDate should output an UTC converted string" + ); + }); + QUnit.test("serializeDate with different numbering system", async (assert) => { patchWithCleanup(Settings, { defaultNumberingSystem: "arab" }); const date = DateTime.utc(2022, 2, 21, 16, 11, 42); @@ -386,6 +431,28 @@ QUnit.module( assert.strictEqual(serializeDateTime(date), "2022-02-21 16:11:42"); }); + QUnit.test("serializeDateTime, with DateTime.now()", async (assert) => { + patchDate(2022, 1, 21, 16, 11, 42); + const date = DateTime.now(); + assert.strictEqual(date.toFormat("yyyy-MM-dd HH:mm:ss"), "2022-02-21 16:11:42"); + assert.strictEqual( + serializeDateTime(date), + "2022-02-21 15:11:42", + "serializeDateTime should output an UTC converted string" + ); + }); + + QUnit.test("serializeDateTime, with DateTime.now(), midnight", async (assert) => { + patchDate(2022, 1, 21, 0, 0, 0); + const date = DateTime.now(); + assert.strictEqual(date.toFormat("yyyy-MM-dd HH:mm:ss"), "2022-02-21 00:00:00"); + assert.strictEqual( + serializeDateTime(date), + "2022-02-20 23:00:00", + "serializeDateTime should output an UTC converted string" + ); + }); + QUnit.test("serializeDateTime with different numbering system", async (assert) => { patchWithCleanup(Settings, { defaultNumberingSystem: "arab" }); const date = DateTime.utc(2022, 2, 21, 16, 11, 42);