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);