diff --git a/addons/mail/static/src/models/tracking_value_item.js b/addons/mail/static/src/models/tracking_value_item.js
index 726b4e58564..1be3032aa85 100644
--- a/addons/mail/static/src/models/tracking_value_item.js
+++ b/addons/mail/static/src/models/tracking_value_item.js
@@ -46,7 +46,7 @@ registerModel({
return format.date(this.value);
case 'datetime': {
const value = this.value ? deserializeDateTime(this.value) : this.value;
- return formatters.get("datetime")(value, { timezone: true });
+ return formatters.get("datetime")(value);
}
case 'float':
return format.float(this.value);
diff --git a/addons/web/static/src/core/datepicker/datepicker.js b/addons/web/static/src/core/datepicker/datepicker.js
index c66fefd9363..81ba4fdbfa0 100644
--- a/addons/web/static/src/core/datepicker/datepicker.js
+++ b/addons/web/static/src/core/datepicker/datepicker.js
@@ -31,7 +31,7 @@ let datePickerId = 0;
* @returns {moment}
*/
function luxonDateToMomentDate(date) {
- return window.moment(String(date));
+ return date.isValid ? window.moment(String(date)) : null;
}
/**
@@ -136,7 +136,6 @@ export class DatePicker extends Component {
format:
!useStatic || isValidStaticFormat(this.format) ? this.format : this.staticFormat,
locale: this.props.locale || (this.date && this.date.locale),
- timezone: this.isLocal,
};
}
diff --git a/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.js b/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.js
index cd3b62a497e..7a60c0e8a50 100644
--- a/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.js
+++ b/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.js
@@ -1,12 +1,16 @@
/** @odoo-module **/
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
-import { serializeDate, serializeDateTime } from "@web/core/l10n/dates";
+import {
+ deserializeDate,
+ deserializeDateTime,
+ serializeDate,
+ serializeDateTime,
+} from "@web/core/l10n/dates";
import { registry } from "@web/core/registry";
const { Component } = owl;
-const parsers = registry.category("parsers");
const dsf = registry.category("domain_selector/fields");
const dso = registry.category("domain_selector/operator");
@@ -15,16 +19,17 @@ export class DomainSelectorDateTimeField extends Component {
const { DatePicker, DateTimePicker } = this.constructor.components;
return this.props.field.type === "date" ? DatePicker : DateTimePicker;
}
- get parser() {
- return parsers.get(this.props.field.type);
+ get deserializedValue() {
+ const deserialize =
+ this.props.field.type === "date" ? deserializeDate : deserializeDateTime;
+ return this.props.value ? deserialize(this.props.value) : luxon.DateTime.local();
}
- get parsedValue() {
- return this.props.value ? this.parser(this.props.value) : luxon.DateTime.local();
- }
-
onChange(value) {
+ if (!this.deserializedValue.isValid && !value) {
+ return;
+ }
const serialize = this.props.field.type === "date" ? serializeDate : serializeDateTime;
- this.props.update({ value: serialize(value) });
+ this.props.update({ value: serialize(value || luxon.DateTime.local()) });
}
}
Object.assign(DomainSelectorDateTimeField, {
diff --git a/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.xml b/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.xml
index a1f20a38d20..bbd3c3e9a9c 100644
--- a/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.xml
+++ b/addons/web/static/src/core/domain_selector/fields/domain_selector_datetime_field.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/addons/web/static/src/core/l10n/dates.js b/addons/web/static/src/core/l10n/dates.js
index 78bd7175c15..4f779a6fd49 100644
--- a/addons/web/static/src/core/l10n/dates.js
+++ b/addons/web/static/src/core/l10n/dates.js
@@ -171,18 +171,16 @@ export const luxonToMomentFormat = memoize(function luxonToMomentFormat(format)
/**
* Formats a DateTime object to a date string
*
- * `options.timezone` is defaulted to false on dates since we assume they
- * shouldn't be affected by timezones like datetimes.
- *
* @see formatDateTime
* @returns {string}
*/
export function formatDate(value, options = {}) {
- return formatDateTime(value, {
- timezone: false, // Timezone should never alter a 'date' value.
- ...options,
- format: options.format || localization.dateFormat,
- });
+ if (value === false) {
+ return "";
+ }
+ const format = options.format || localization.dateFormat;
+ const numberingSystem = options.numberingSystem || Settings.defaultNumberingSystem || "latn";
+ return value.toFormat(format, { numberingSystem });
}
/**
@@ -195,12 +193,6 @@ export function formatDate(value, options = {}) {
*
* Default=the session localization format.
*
- * @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=true.
- *
* @param {string} [options.numberingSystem]
* Provided numbering system used to parse the input value.
*
@@ -215,9 +207,7 @@ export function formatDateTime(value, options = {}) {
}
const format = options.format || localization.dateTimeFormat;
const numberingSystem = options.numberingSystem || Settings.defaultNumberingSystem || "latn";
- const zone = !("timezone" in options) || options.timezone ? "default" : "utc";
- value = value.setZone(zone, { keepLocaltime: options.timezone });
- return value.toFormat(format, { numberingSystem });
+ return value.setZone("default").toFormat(format, { numberingSystem });
}
// -----------------------------------------------------------------------------
@@ -227,18 +217,15 @@ export function formatDateTime(value, options = {}) {
/**
* Parses a string value to a Luxon DateTime object.
*
- * `options.timezone` is defaulted to false on dates since we assume they
- * shouldn't be affected by timezones like datetimes.
- *
* @see parseDateTime (Note: since we're only interested by the date itself, the
* returned value will always be set at the start of the day)
- * @returns {DateTime | false} Luxon DateTime object
+ * @returns {DateTime | false} Luxon DateTime object in user's timezone
*/
export function parseDate(value, options = {}) {
if (!value) {
return false;
}
- return parseDateTime(value, { timezone: false, ...options }).startOf("day");
+ return parseDateTime(value, options).startOf("day");
}
/**
@@ -247,10 +234,14 @@ export function parseDate(value, options = {}) {
* @param {string} value value to parse.
* - Value can take the form of a smart date:
* e.g. "+3w" for three weeks from now.
- * (`options.format` and `options.timezone` are ignored in this case)
+ * (`options.format` is ignored in this case)
*
* - If value cannot be parsed within the provided format,
- * ISO8601 and SQL formats are then tried.
+ * ISO8601 and SQL formats are then tried. If these formats
+ * include a timezone information, the returned value will
+ * still be set to the user's timezone.
+ * e.g. "2020-01-01T12:00:00+06:00" with the user's timezone being UTC+1,
+ * the returned value will express the same timestamp but in UTC+1 (here time will be 7:00).
*
* @param {object} options
* @param {string} [options.format]
@@ -258,16 +249,6 @@ export function parseDate(value, options = {}) {
*
* Default=the session localization format
*
- * @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.
- *
- * NB: ISO strings containing timezone information
- * will have priority over this option.
- *
- * Default=false.
- *
* @param {string} [options.locale]
* Provided locale used to parse the input value.
*
@@ -279,7 +260,7 @@ export function parseDate(value, options = {}) {
* Default=the default numbering system assigned to luxon
* @see localization_service.js
*
- * @returns {DateTime | false} Luxon DateTime object
+ * @returns {DateTime | false} Luxon DateTime object in user's timezone
*/
export function parseDateTime(value, options = {}) {
if (!value) {
@@ -289,7 +270,7 @@ export function parseDateTime(value, options = {}) {
const fmt = options.format || localization.dateTimeFormat;
const parseOpts = {
setZone: true,
- zone: options.timezone ? "default" : "utc",
+ zone: "default",
locale: options.locale,
numberingSystem: options.numberingSystem || Settings.defaultNumberingSystem || "latn",
};
@@ -358,31 +339,31 @@ export function parseDateTime(value, options = {}) {
throw new Error(sprintf(_t("'%s' is not a correct date or datetime"), value));
}
- return options.timezone ? result : result.toUTC();
+ return result.setZone("default");
}
/**
* Returns a date object parsed from the given serialized string.
- * @param {string} value
- * @returns {DateTime | false}
+ * @param {string} value serialized date string, e.g. "2018-01-01"
+ * @returns {DateTime} parsed date object in user's timezone
*/
export function deserializeDate(value) {
- return DateTime.fromSQL(value, { zone: "utc", numberingSystem: "latn" });
+ return DateTime.fromSQL(value, { zone: "default", numberingSystem: "latn" });
}
/**
* Returns a datetime object parsed from the given serialized string.
- * @param {string} value
- * @returns {DateTime | false}
+ * @param {string} value serialized datetime string, e.g. "2018-01-01 00:00:00", expressed in UTC
+ * @returns {DateTime} parsed datetime object in user's timezone
*/
export function deserializeDateTime(value) {
- return DateTime.fromSQL(value, { zone: "utc", numberingSystem: "latn" });
+ return DateTime.fromSQL(value, { zone: "utc", numberingSystem: "latn" }).setZone("default");
}
/**
* Returns a serialized string representing the given date.
- * @param {DateTime} value
- * @returns {string}
+ * @param {DateTime} value DateTime object, its timezone does not matter
+ * @returns {string} serialized date, ready to be sent to the server
*/
export function serializeDate(value) {
return value.toFormat(SERVER_DATE_FORMAT, { numberingSystem: "latn" });
@@ -390,8 +371,8 @@ export function serializeDate(value) {
/**
* Returns a serialized string representing the given datetime.
- * @param {DateTime} value
- * @returns {string}
+ * @param {DateTime} value DateTime object, its timezone does not matter
+ * @returns {string} serialized datetime, ready to be sent to the server
*/
export function serializeDateTime(value) {
return value.setZone("utc").toFormat(SERVER_DATETIME_FORMAT, { numberingSystem: "latn" });
diff --git a/addons/web/static/src/search/search_bar/search_bar.js b/addons/web/static/src/search/search_bar/search_bar.js
index b5ac03aebd7..b07c70127c4 100644
--- a/addons/web/static/src/search/search_bar/search_bar.js
+++ b/addons/web/static/src/search/search_bar/search_bar.js
@@ -129,11 +129,11 @@ export class SearchBar extends Component {
try {
switch (type) {
case "date": {
- value = serializeDate(parser(trimmedQuery, { timezone: false }));
+ value = serializeDate(parser(trimmedQuery));
break;
}
case "datetime": {
- value = serializeDateTime(parser(trimmedQuery, { timezone: true }));
+ value = serializeDateTime(parser(trimmedQuery));
break;
}
case "many2one": {
diff --git a/addons/web/static/src/views/fields/date/date_field.js b/addons/web/static/src/views/fields/date/date_field.js
index eb2ddeccd1d..530ccedbbac 100644
--- a/addons/web/static/src/views/fields/date/date_field.js
+++ b/addons/web/static/src/views/fields/date/date_field.js
@@ -1,7 +1,8 @@
/** @odoo-module **/
import { DatePicker } from "@web/core/datepicker/datepicker";
-import { areDateEquals, formatDate } from "@web/core/l10n/dates";
+import { areDateEquals, formatDate, formatDateTime } from "@web/core/l10n/dates";
+import { localization } from "@web/core/l10n/localization";
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "../standard_field_props";
@@ -17,10 +18,9 @@ export class DateField extends Component {
}
get formattedValue() {
- return formatDate(this.props.value, {
- // get local date if field type is datetime
- timezone: this.isDateTime,
- });
+ return this.isDateTime
+ ? formatDateTime(this.props.value, { format: localization.dateFormat })
+ : formatDate(this.props.value);
}
onDateTimeChanged(date) {
diff --git a/addons/web/static/src/views/fields/daterange/daterange_field.js b/addons/web/static/src/views/fields/daterange/daterange_field.js
index 89e70cc74f1..7d1cd1e95f3 100644
--- a/addons/web/static/src/views/fields/daterange/daterange_field.js
+++ b/addons/web/static/src/views/fields/daterange/daterange_field.js
@@ -87,7 +87,7 @@ export class DateRangeField extends Component {
const formatter = formatters.get(format);
let formattedValue;
try {
- formattedValue = formatter(value, { timezone: this.isDateTime });
+ formattedValue = formatter(value);
} catch {
this.props.record.setInvalidField(this.props.name);
}
@@ -105,7 +105,7 @@ export class DateRangeField extends Component {
const parse = parsers.get(this.props.formatType);
let value;
try {
- value = parse(ev.target.value, { timezone: this.isDateTime });
+ value = parse(ev.target.value);
} catch {
this.props.record.setInvalidField(this.props.name);
return;
@@ -129,9 +129,7 @@ export class DateRangeField extends Component {
const end = this.isDateTime ? picker.endDate : picker.endDate.startOf("day");
const parser = parsers.get(this.props.formatType);
const dates = [start, end].map((date) => {
- return parser(date.format(this.momentFormat), {
- timezone: this.isDateTime,
- });
+ return parser(date.format(this.momentFormat));
});
await this.updateRange(dates[0], dates[1]);
const input = document.querySelector(
diff --git a/addons/web/static/src/views/fields/properties/property_value.js b/addons/web/static/src/views/fields/properties/property_value.js
index 21cc467f5f3..6fd11660f8c 100644
--- a/addons/web/static/src/views/fields/properties/property_value.js
+++ b/addons/web/static/src/views/fields/properties/property_value.js
@@ -12,16 +12,16 @@ import { m2oTupleFromData } from "@web/views/fields/many2one/many2one_field";
import { PropertyTags } from "./property_tags";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { formatFloat, formatInteger, formatMany2one } from "@web/views/fields/formatters";
-import { formatDate, formatDateTime } from "@web/core/l10n/dates";
+import {
+ deserializeDate,
+ deserializeDateTime,
+ formatDate,
+ formatDateTime,
+ serializeDate,
+ serializeDateTime,
+} from "@web/core/l10n/dates";
const { Component } = owl;
-const { DateTime } = luxon;
-
-// Formats to stringify the date / datetime in the JSON.
-// It's important to have the year first, then the day,
-// etc... in UTC, to be able to search on them.
-const DEFAULT_SERVER_DATETIME_FORMAT = "yyyy-LL-dd HH:mm:ss";
-const DEFAULT_SERVER_DATE_FORMAT = "yyyy-LL-dd";
/**
* Represent one property value.
@@ -79,24 +79,9 @@ export class PropertyValue extends Component {
// force to show at least 1 digit, even for integers
return value;
} else if (this.props.type === "datetime") {
- if (typeof value === "string") {
- // convert the datetime from the UTC format to the current timezone
- const datetimeValue = DateTime.fromFormat(
- value + " +00:00",
- DEFAULT_SERVER_DATETIME_FORMAT + " Z"
- );
- return datetimeValue.invalid ? false : datetimeValue;
- }
- return value instanceof DateTime ? value : false;
+ return typeof value === "string" ? deserializeDateTime(value) : value;
} else if (this.props.type === "date") {
- if (typeof value === "string") {
- const datetimeValue = DateTime.fromFormat(
- value + " +00:00",
- DEFAULT_SERVER_DATE_FORMAT + " Z"
- );
- return datetimeValue.invalid ? false : datetimeValue;
- }
- return value instanceof DateTime ? value : false;
+ return typeof value === "string" ? deserializeDate(value) : value;
} else if (this.props.type === "boolean") {
return !!value;
} else if (this.props.type === "selection") {
@@ -178,15 +163,9 @@ export class PropertyValue extends Component {
*/
async onValueChange(newValue) {
if (this.props.type === "datetime") {
- if (typeof newValue === "string") {
- newValue = DateTime.fromISO(newValue);
- }
- newValue = newValue.toUTC().toFormat(DEFAULT_SERVER_DATETIME_FORMAT);
+ newValue = serializeDateTime(newValue);
} else if (this.props.type === "date") {
- if (typeof newValue === "string") {
- newValue = DateTime.fromISO(newValue);
- }
- newValue = newValue.toFormat(DEFAULT_SERVER_DATE_FORMAT);
+ newValue = serializeDate(newValue);
} else if (this.props.type === "integer") {
newValue = parseInt(newValue) || 0;
} else if (this.props.type === "float") {
diff --git a/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js b/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js
index 9a3ea2607f1..e07c3a54dc3 100644
--- a/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js
+++ b/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js
@@ -1,7 +1,8 @@
/** @odoo-module **/
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
-import { formatDate } from "@web/core/l10n/dates";
+import { formatDate, formatDateTime } from "@web/core/l10n/dates";
+import { localization } from "@web/core/l10n/localization";
import { registry } from "@web/core/registry";
import { _lt } from "@web/core/l10n/translation";
import { standardFieldProps } from "../standard_field_props";
@@ -21,18 +22,14 @@ export class RemainingDaysField extends Component {
if (!this.props.value) {
return null;
}
- const today = this.correctDate(luxon.DateTime.utc()).startOf("day");
- return Math.floor(
- this.correctDate(this.props.value).startOf("day").diff(today, "days").days
- );
+ const today = luxon.DateTime.local().startOf("day");
+ return Math.floor(this.props.value.startOf("day").diff(today, "days").days);
}
get formattedValue() {
- return this.props.value ? formatDate(this.props.value, { timezone: this.hasTime }) : "";
- }
-
- correctDate(date) {
- return this.hasTime ? date.toLocal() : date.toUTC();
+ return this.hasTime
+ ? formatDateTime(this.props.value, { format: localization.dateFormat })
+ : formatDate(this.props.value);
}
onDateTimeChanged(datetime) {
diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js
index cca9fb72e8f..64aca9ba49a 100644
--- a/addons/web/static/src/views/list/list_renderer.js
+++ b/addons/web/static/src/views/list/list_renderer.js
@@ -690,7 +690,6 @@ export class ListRenderer extends Component {
isPassword: "password" in column.rawAttrs,
digits: column.rawAttrs.digits ? JSON.parse(column.rawAttrs.digits) : field.digits,
field: record.fields[fieldName],
- timezone: true,
};
return formatter(record.data[fieldName], formatOptions);
}
diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js
index 5f3e00c7a74..033c5dc801f 100644
--- a/addons/web/static/src/views/relational_model.js
+++ b/addons/web/static/src/views/relational_model.js
@@ -435,9 +435,9 @@ export class Record extends DataPoint {
evalContext[fieldName] = list.getContext();
// ---> implied to initialize (resIds, commands) currentIds before loading static list
} else if (value && this.fields[fieldName].type === "date") {
- evalContext[fieldName] = value.toFormat("yyyy-LL-dd");
+ evalContext[fieldName] = serializeDate(value);
} else if (value && this.fields[fieldName].type === "datetime") {
- evalContext[fieldName] = value.toFormat("yyyy-LL-dd HH:mm:ss");
+ evalContext[fieldName] = serializeDateTime(value);
} else if (value && this.fields[fieldName].type === "many2one") {
evalContext[fieldName] = value[0];
} else if (value && this.fields[fieldName].type === "reference") {
diff --git a/addons/web/static/tests/core/domain_selector_tests.js b/addons/web/static/tests/core/domain_selector_tests.js
index fc36adebeea..46cfcef9742 100644
--- a/addons/web/static/tests/core/domain_selector_tests.js
+++ b/addons/web/static/tests/core/domain_selector_tests.js
@@ -185,7 +185,7 @@ QUnit.module("Components", (hooks) => {
});
QUnit.test("building a domain with a datetime", async (assert) => {
- assert.expect(2);
+ assert.expect(4);
// Create the domain selector and its mock environment
await mountComponent(DomainSelector, {
@@ -205,8 +205,11 @@ QUnit.module("Components", (hooks) => {
// Check that there is a datepicker to choose the date
assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
- await click(target, ".o_datepicker_input");
+ // The input field should display the date and time in the user's timezone
+ assert.equal(target.querySelector(".o_datepicker_input").value, "03/27/2017 16:42:00");
+ // Change the date in the datepicker
+ await click(target, ".o_datepicker_input");
await click(
document.body.querySelector(
`.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]`
@@ -215,6 +218,38 @@ QUnit.module("Components", (hooks) => {
await click(
document.body.querySelector(`.bootstrap-datetimepicker-widget a[data-action="close"]`)
);
+
+ // The input field should display the date and time in the user's timezone
+ assert.equal(target.querySelector(".o_datepicker_input").value, "02/26/2017 16:42:00");
+ });
+
+ QUnit.test("building a domain with a datetime: context_today()", async (assert) => {
+ // Create the domain selector and its mock environment
+ await mountComponent(DomainSelector, {
+ props: {
+ resModel: "partner",
+ value: `[("datetime", "=", context_today())]`,
+ readonly: false,
+ update: () => {
+ assert.step("SHOULD NEVER BE CALLED");
+ },
+ },
+ });
+
+ // Check that there is a datepicker to choose the date
+ assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
+ // The input field should display that the date is invalid
+ assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime");
+
+ // Open and close the datepicker
+ await click(target, ".o_datepicker_input");
+ await click(
+ document.body.querySelector(`.bootstrap-datetimepicker-widget [data-action=close]`)
+ );
+
+ // The input field should display an empty value. NB: this could be improved, but OK for now
+ assert.equal(target.querySelector(".o_datepicker_input").value, "");
+ assert.verifySteps([]);
});
QUnit.test("building a domain with a m2o without following the relation", async (assert) => {
diff --git a/addons/web/static/tests/core/l10n/dates_tests.js b/addons/web/static/tests/core/l10n/dates_tests.js
index 48a239d8f4b..e181b881a53 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 { patchDate, patchWithCleanup } from "../../helpers/utils";
+import { patchDate, patchTimeZone, patchWithCleanup } from "../../helpers/utils";
const { DateTime, Settings } = luxon;
@@ -81,89 +81,75 @@ QUnit.module(
() => {
QUnit.module("dates");
- QUnit.test("formatDate", async (assert) => {
- patch(localization, "dateformat", { dateFormat: "MM/dd/yyyy" });
+ QUnit.test("formatDate/formatDateTime specs", async (assert) => {
+ patchWithCleanup(localization, {
+ dateFormat: "MM/dd/yyyy",
+ dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
+ });
+ patchTimeZone(60);
+ patchDate(2009, 4, 4, 12, 34, 56);
- let formatted = formatDate(DateTime.utc(2009, 5, 4, 12, 34, 23));
- let expected = "05/04/2009";
- assert.strictEqual(formatted, expected);
+ const utc = DateTime.utc(); // 2009-05-04T11:34:56.000Z
+ const local = DateTime.local(); // 2009-05-04T12:34:56.000+01:00
+ const minus13FromLocalTZ = local.setZone("UTC-12"); // 2009-05-03T23:34:56.000-12:00
- formatted = formatDate(DateTime.utc(2009, 5, 4, 12, 34, 23), { timezone: false });
- assert.strictEqual(formatted, expected);
+ // For dates, regardless of the input timezone, outputs only the date
+ assert.strictEqual(formatDate(utc), "05/04/2009");
+ assert.strictEqual(formatDate(local), "05/04/2009");
+ assert.strictEqual(formatDate(minus13FromLocalTZ), "05/03/2009");
- formatted = formatDate(DateTime.utc(2009, 5, 4, 12, 34, 23), { timezone: true });
- expected = "05/04/2009";
- assert.strictEqual(formatted, expected);
-
- unpatch(localization, "dateformat");
+ // For datetimes, input timezone is taken into account, outputs in local timezone
+ assert.strictEqual(formatDateTime(utc), "05/04/2009 12:34:56");
+ assert.strictEqual(formatDateTime(local), "05/04/2009 12:34:56");
+ assert.strictEqual(formatDateTime(minus13FromLocalTZ), "05/04/2009 12:34:56");
});
- QUnit.test("formatDate (with different timezone offset)", async (assert) => {
- patch(localization, "dateformat", { dateFormat: "MM/dd/yyyy" });
+ QUnit.test("formatDate/formatDateTime specs, at midnight", async (assert) => {
+ patchWithCleanup(localization, {
+ dateFormat: "MM/dd/yyyy",
+ dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
+ });
+ patchTimeZone(60);
+ patchDate(2009, 4, 4, 0, 0, 0);
- let str = formatDate(DateTime.utc(2017, 1, 1, 10, 0, 0, 0));
- assert.strictEqual(str, "01/01/2017");
- str = formatDate(DateTime.utc(2017, 6, 1, 10, 0, 0, 0));
- assert.strictEqual(str, "06/01/2017");
+ const utc = DateTime.utc(); // 2009-05-03T23:00:00.000Z
+ const local = DateTime.local(); // 2009-05-04T00:00:00.000+01:00
+ const minus13FromLocalTZ = local.setZone("UTC-12"); // 2009-05-03T11:00:00.000-12:00
- str = formatDate(DateTime.utc(2017, 1, 1, 10, 0, 0, 0), { timezone: false });
- assert.strictEqual(str, "01/01/2017");
- str = formatDate(DateTime.utc(2017, 6, 1, 10, 0, 0, 0), { timezone: false });
- assert.strictEqual(str, "06/01/2017");
+ // For dates, regardless of the input timezone, outputs only the date
+ assert.strictEqual(formatDate(utc), "05/03/2009");
+ assert.strictEqual(formatDate(local), "05/04/2009");
+ assert.strictEqual(formatDate(minus13FromLocalTZ), "05/03/2009");
- str = formatDate(DateTime.utc(2017, 1, 1, 10, 0, 0, 0), { timezone: true });
- assert.strictEqual(str, "01/01/2017");
- str = formatDate(DateTime.utc(2017, 6, 1, 10, 0, 0, 0), { timezone: true });
- assert.strictEqual(str, "06/01/2017");
-
- unpatch(localization, "dateformat");
+ // For datetimes, input timezone is taken into account, outputs in local timezone
+ assert.strictEqual(formatDateTime(utc), "05/04/2009 00:00:00");
+ assert.strictEqual(formatDateTime(local), "05/04/2009 00:00:00");
+ assert.strictEqual(formatDateTime(minus13FromLocalTZ), "05/04/2009 00:00:00");
});
- QUnit.test("formatDate (with DateTime.fromISO)", async (assert) => {
- patchWithCleanup(localization, { dateFormat: "MM/dd/yyyy" });
+ QUnit.test("parseDate(Time) outputs DateTime objects in local TZ", async (assert) => {
+ patchWithCleanup(localization, defaultLocalization);
- // 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");
+ patchTimeZone(60);
+ assert.equal(parseDate("01/13/2019").toISO(), "2019-01-13T00:00:00.000+01:00");
+ assert.equal(
+ parseDateTime("01/13/2019 10:05:45").toISO(),
+ "2019-01-13T10:05:45.000+01:00"
+ );
- date = DateTime.fromISO("2022-07-21T23:00:00Z");
- assert.strictEqual(formatDate(date), "07/21/2022");
- assert.strictEqual(formatDate(date, { timezone: true }), "07/22/2022");
+ patchTimeZone(330);
+ assert.equal(parseDate("01/13/2019").toISO(), "2019-01-13T00:00:00.000+05:30");
+ assert.equal(
+ parseDateTime("01/13/2019 10:05:45").toISO(),
+ "2019-01-13T10:05:45.000+05:30"
+ );
- 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) => {
- 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) => {
- 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");
+ patchTimeZone(-660);
+ assert.equal(parseDate("01/13/2019").toISO(), "2019-01-13T00:00:00.000-11:00");
+ assert.equal(
+ parseDateTime("01/13/2019 10:05:45").toISO(),
+ "2019-01-13T10:05:45.000-11:00"
+ );
});
QUnit.test("parseDateTime", async (assert) => {
@@ -194,7 +180,7 @@ QUnit.module(
parseDateTime("invalid value");
}, /is not a correct/);
- const expected = "2019-01-13T10:05:45.000Z";
+ const expected = "2019-01-13T10:05:45.000+01:00";
let dateStr = "01/13/2019 10:05:45";
assert.equal(parseDateTime(dateStr).toISO(), expected, "Date with leading 0");
dateStr = "1/13/2019 10:5:45";
@@ -221,7 +207,7 @@ QUnit.module(
Settings.defaultLocale = "no"; // Norwegian
const dateStr = "16. des 2019 10:05:45";
- const expected = "2019-12-16T10:05:45.000Z";
+ const expected = "2019-12-16T10:05:45.000+01:00";
assert.equal(
parseDateTime(dateStr).toISO(),
expected,
@@ -236,18 +222,10 @@ QUnit.module(
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"
- );
+ assert.strictEqual(parseDate(str).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"
- );
+ assert.strictEqual(parseDate(str).toISO(), "2022-07-22T00:00:00.000+01:00");
});
QUnit.test("parseDate without separator", async (assert) => {
@@ -376,15 +354,29 @@ QUnit.module(
);
});
+ QUnit.test("parseDateTime ISO8601 Format", async (assert) => {
+ patchWithCleanup(localization, defaultLocalization);
+ patchTimeZone(60);
+ assert.equal(
+ parseDateTime("2017-05-15T12:00:00.000+06:00").toISO(),
+ "2017-05-15T07:00:00.000+01:00"
+ );
+ // without the 'T' separator is not really ISO8601 compliant, but we still support it
+ assert.equal(
+ parseDateTime("2017-05-15 12:00:00.000+06:00").toISO(),
+ "2017-05-15T07:00:00.000+01:00"
+ );
+ });
+
QUnit.test("parseDateTime SQL Format", async (assert) => {
patch(localization, "default loc", defaultLocalization);
let dateStr = "2017-05-15 09:12:34";
- let expected = "2017-05-15T09:12:34.000Z";
+ let expected = "2017-05-15T09:12:34.000+01:00";
assert.equal(parseDateTime(dateStr).toISO(), expected, "Date with SQL format");
dateStr = "2017-05-08 09:12:34";
- expected = "2017-05-08T09:12:34.000Z";
+ expected = "2017-05-08T09:12:34.000+01:00";
assert.equal(
parseDateTime(dateStr).toISO(),
expected,
@@ -457,9 +449,9 @@ QUnit.module(
});
QUnit.test("deserializeDate", async (assert) => {
- const date = DateTime.utc(2022, 2, 21);
+ const date = DateTime.local(2022, 2, 21);
assert.strictEqual(
- DateTime.fromFormat("2022-02-21", "yyyy-MM-dd", { zone: "utc" }).toMillis(),
+ DateTime.fromFormat("2022-02-21", "yyyy-MM-dd").toMillis(),
date.toMillis()
);
assert.strictEqual(deserializeDate("2022-02-21").toMillis(), date.toMillis());
@@ -467,9 +459,9 @@ QUnit.module(
QUnit.test("deserializeDate with different numbering system", async (assert) => {
patchWithCleanup(Settings, { defaultNumberingSystem: "arab" });
- const date = DateTime.utc(2022, 2, 21);
+ const date = DateTime.local(2022, 2, 21);
assert.strictEqual(
- DateTime.fromFormat("٢٠٢٢-٠٢-٢١", "yyyy-MM-dd", { zone: "utc" }).toMillis(),
+ DateTime.fromFormat("٢٠٢٢-٠٢-٢١", "yyyy-MM-dd").toMillis(),
date.toMillis()
);
assert.strictEqual(deserializeDate("2022-02-21").toMillis(), date.toMillis());
@@ -504,6 +496,40 @@ QUnit.module(
);
});
+ QUnit.test("parseDate with short notations", async (assert) => {
+ assert.strictEqual(
+ parseDate("20-10-20", { format: "yyyy-MM-dd" }).toISO(),
+ "2020-10-20T00:00:00.000+01:00"
+ );
+ assert.strictEqual(
+ parseDate("20/10/20", { format: "yyyy/MM/dd" }).toISO(),
+ "2020-10-20T00:00:00.000+01:00"
+ );
+ assert.strictEqual(
+ parseDate("10-20-20", { format: "MM-dd-yyyy" }).toISO(),
+ "2020-10-20T00:00:00.000+01:00"
+ );
+ assert.strictEqual(
+ parseDate("10-20-20", { format: "MM-yyyy-dd" }).toISO(),
+ "2020-10-20T00:00:00.000+01:00"
+ );
+ assert.strictEqual(
+ parseDate("1-20-2", { format: "MM-yyyy-dd" }).toISO(),
+ "2020-01-02T00:00:00.000+01:00"
+ );
+ assert.strictEqual(
+ parseDate("20/1/2", { format: "yyyy/MM/dd" }).toISO(),
+ "2020-01-02T00:00:00.000+01:00"
+ );
+ });
+
+ QUnit.test("parseDateTime with short notations", async (assert) => {
+ assert.strictEqual(
+ parseDateTime("20-10-20 8:5:3", { format: "yyyy-MM-dd hh:mm:ss" }).toISO(),
+ "2020-10-20T08:05:03.000+01:00"
+ );
+ });
+
QUnit.module("dates utils compatibility with legacy", {
beforeEach() {
patchWithCleanup(localization, {
@@ -544,6 +570,9 @@ QUnit.module(
});
QUnit.test("parseDate", async (assert) => {
+ // Patch the timezone to no offset, as the legacy parsing always outputs
+ // a local date/datetime but as UTC (keeping the local time, which is wrong...)
+ patchTimeZone(0);
/**
* Type of testSet key: string
* Type of testSet value: [newExpected: string, legacyExpected: string]
@@ -674,6 +703,9 @@ QUnit.module(
});
QUnit.test("parseDate (with legacy options.isUTC = true)", async (assert) => {
+ // Patch the timezone to no offset, as the legacy parsing always outputs
+ // a local date/datetime but as UTC (keeping the local time, which is wrong...)
+ patchTimeZone(0);
/**
* Type of testSet key: string
* Type of testSet value: [newExpected: string, legacyExpected: string]
@@ -721,40 +753,16 @@ QUnit.module(
]);
runTestSet(assert, testSet, {
- newFn: (input) => parseDate(input, { format: "YYYY-MM-DD" }).toISO(),
+ newFn: (input) => parseDate(input).setZone("utc", { keepLocalTime: true }).toISO(),
legacyFn: (input) =>
legacy.field_utils.parse.date(input, null, { isUTC: true }).toISOString(),
});
});
- QUnit.test("parseDate with short notations", async (assert) => {
- assert.strictEqual(
- parseDate("20-10-20", { format: "yyyy-MM-dd" }).toISO(),
- "2020-10-20T00:00:00.000Z"
- );
- assert.strictEqual(
- parseDate("20/10/20", { format: "yyyy/MM/dd" }).toISO(),
- "2020-10-20T00:00:00.000Z"
- );
- assert.strictEqual(
- parseDate("10-20-20", { format: "MM-dd-yyyy" }).toISO(),
- "2020-10-20T00:00:00.000Z"
- );
- assert.strictEqual(
- parseDate("10-20-20", { format: "MM-yyyy-dd" }).toISO(),
- "2020-10-20T00:00:00.000Z"
- );
- assert.strictEqual(
- parseDate("1-20-2", { format: "MM-yyyy-dd" }).toISO(),
- "2020-01-02T00:00:00.000Z"
- );
- assert.strictEqual(
- parseDate("20/1/2", { format: "yyyy/MM/dd" }).toISO(),
- "2020-01-02T00:00:00.000Z"
- );
- });
-
QUnit.test("parseDateTime", async (assert) => {
+ // Patch the timezone to no offset, as the legacy parsing always outputs
+ // a local date/datetime but as UTC (keeping the local time, which is wrong...)
+ patchTimeZone(0);
/**
* Type of testSet key: string
* Type of testSet value: [newExpected: string, legacyExpected: string]
@@ -846,14 +854,10 @@ QUnit.module(
});
});
- QUnit.test("parseDateTime with short notations", async (assert) => {
- assert.strictEqual(
- parseDateTime("20-10-20 8:5:3", { format: "yyyy-MM-dd hh:mm:ss" }).toISO(),
- "2020-10-20T08:05:03.000Z"
- );
- });
-
QUnit.test("parseDateTime (with legacy options.isUTC = true)", async (assert) => {
+ // Patch the timezone to no offset, as the legacy parsing always outputs
+ // a local date/datetime but as UTC (keeping the local time, which is wrong...)
+ patchTimeZone(0);
/**
* Type of testSet key: string
* Type of testSet value: [newExpected: string, legacyExpected: string]
@@ -902,7 +906,8 @@ QUnit.module(
}
runTestSet(assert, testSet, {
- newFn: (input) => parseDateTime(input, { format: "YYYY-MM-DD HH:mm:ss" }).toISO(),
+ newFn: (input) =>
+ parseDateTime(input).setZone("utc", { keepLocalTime: true }).toISO(),
legacyFn: (input) =>
legacy.field_utils.parse.datetime(input, null, { isUTC: true }).toISOString(),
});
@@ -1019,7 +1024,7 @@ QUnit.module(
]);
runTestSet(assert, testSet, {
- newFn: (input) => parseDateTime(input, { timezone: true }).toUTC().toISO(),
+ newFn: (input) => parseDateTime(input).toUTC().toISO(),
legacyFn: (input) =>
legacy.field_utils.parse
.datetime(input, null, { timezone: true })
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 4a9cb67319a..568b6a26794 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -879,7 +879,7 @@ export class MockServer {
const [fieldName, aggregateFunction = "month"] = groupByField.split(":");
const { type } = fields[fieldName];
if (type === "date") {
- const date = deserializeDate(val).setZone("default");
+ const date = deserializeDate(val);
if (aggregateFunction === "day") {
return date.toFormat("yyyy-MM-dd");
} else if (aggregateFunction === "week") {
@@ -892,7 +892,7 @@ export class MockServer {
return date.toFormat("MMMM yyyy");
}
} else if (type === "datetime") {
- const date = deserializeDateTime(val).setZone("default");
+ const date = deserializeDateTime(val);
if (aggregateFunction === "hour") {
return date.toFormat("HH:00 dd MMM");
} else if (aggregateFunction === "day") {
@@ -987,57 +987,36 @@ export class MockServer {
switch (dateRange) {
case "hour": {
try {
- startDate = parseDateTime(value, {
- format: "HH dd MMM",
- timezone: type !== "date",
- });
+ startDate = parseDateTime(value, { format: "HH dd MMM" });
} catch {
- startDate = parseDateTime(value, {
- format: "HH:00 dd MMM",
- timezone: type !== "date",
- });
+ startDate = parseDateTime(value, { format: "HH:00 dd MMM" });
}
endDate = startDate.plus({ hours: 1 });
break;
}
case "day": {
- startDate = parseDateTime(value, {
- format: "yyyy-MM-dd",
- timezone: type !== "date",
- });
+ startDate = parseDateTime(value, { format: "yyyy-MM-dd" });
endDate = startDate.plus({ days: 1 });
break;
}
case "week": {
- startDate = parseDateTime(value, {
- format: "WW kkkk",
- timezone: type !== "date",
- });
+ startDate = parseDateTime(value, { format: "WW kkkk" });
endDate = startDate.plus({ weeks: 1 });
break;
}
case "quarter": {
- startDate = parseDateTime(value, {
- format: "q yyyy",
- timezone: type !== "date",
- });
+ startDate = parseDateTime(value, { format: "q yyyy" });
endDate = startDate.plus({ quarters: 1 });
break;
}
case "year": {
- startDate = parseDateTime(value, {
- format: "y",
- timezone: type !== "date",
- });
+ startDate = parseDateTime(value, { format: "y" });
endDate = startDate.plus({ years: 1 });
break;
}
case "month":
default: {
- startDate = parseDateTime(value, {
- format: "MMMM yyyy",
- timezone: type !== "date",
- });
+ startDate = parseDateTime(value, { format: "MMMM yyyy" });
endDate = startDate.plus({ months: 1 });
break;
}
diff --git a/addons/web/static/tests/views/fields/daterange_field_tests.js b/addons/web/static/tests/views/fields/daterange_field_tests.js
index b3e83d7fa2e..58c6bbc5028 100644
--- a/addons/web/static/tests/views/fields/daterange_field_tests.js
+++ b/addons/web/static/tests/views/fields/daterange_field_tests.js
@@ -601,32 +601,4 @@ QUnit.module("Fields", (hooks) => {
"the end date should only show date when option formatType is Date"
);
});
-
- QUnit.test("Date field with option format type is 'datetime'", async function (assert) {
- serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
- serverData.models.partner.records[0].date_end = "2017-03-13";
-
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: `
-
`,
- });
-
- assert.strictEqual(
- target.querySelector(".o_field_daterange[name='date']").textContent,
- "02/03/2017 05:30:00",
- "the start date should show date with time when option format_type is datatime"
- );
- assert.strictEqual(
- target.querySelector(".o_field_daterange[name='date_end']").textContent,
- "03/13/2017 05:30:00",
- "the end date should show date with time when option format_type is datatime"
- );
- });
});
diff --git a/addons/web/static/tests/views/fields/domain_field_tests.js b/addons/web/static/tests/views/fields/domain_field_tests.js
index 1c7715eaff1..a4460e31347 100644
--- a/addons/web/static/tests/views/fields/domain_field_tests.js
+++ b/addons/web/static/tests/views/fields/domain_field_tests.js
@@ -723,6 +723,83 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_form_button_save");
});
+ QUnit.test("domain field: edit through selector (dynamic content)", async function (assert) {
+ patchWithCleanup(odoo, { debug: true });
+
+ let rawDomain = `[("date", ">=", context_today())]`;
+ serverData.models.partner.records[0].foo = rawDomain;
+ serverData.models.partner.fields.bar.type = "char";
+ serverData.models.partner.records[0].bar = "partner";
+
+ serverData.views = {
+ "partner,false,form": `
+ `,
+ "partner,false,search": ``,
+ };
+
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "test",
+ res_id: 1,
+ res_model: "partner",
+ type: "ir.actions.act_window",
+ views: [[false, "form"]],
+ },
+ };
+
+ const webClient = await createWebClient({
+ serverData,
+ mockRPC(route, { method }) {
+ assert.step(method || route);
+ },
+ });
+ assert.verifySteps(["/web/webclient/load_menus"]);
+
+ await doAction(webClient, 1);
+ assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]);
+
+ await click(target, ".o_form_button_edit");
+ assert.verifySteps(["search_count"]);
+ assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
+ assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
+
+ // Open and close the datepicker
+ await click(target, ".o_datepicker_input");
+ assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
+ await triggerEvent(window, null, "scroll");
+ assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
+ assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
+ assert.verifySteps([]);
+
+ // Save
+ await click(target, ".o_form_button_save");
+ assert.verifySteps(["search_count"]);
+
+ // Edit again
+ await click(target, ".o_form_button_edit");
+ assert.verifySteps(["search_count"]);
+ assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
+
+ // Manually input a date
+ rawDomain = `[("date", ">=", "2020-09-09")]`;
+ await editInput(target, ".o_datepicker_input", "09/09/2020");
+ assert.verifySteps(["search_count"]);
+ assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
+
+ // Save
+ await click(target, ".o_form_button_save");
+ assert.verifySteps(["write", "read", "search_count"]);
+
+ // Edit again
+ await click(target, ".o_form_button_edit");
+ assert.verifySteps(["search_count"]);
+ assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
+ });
+
QUnit.test("domain field without model", async function (assert) {
serverData.models.partner.fields.model_name = { string: "Model name", type: "char" };
diff --git a/addons/web/static/tests/views/fields/properties_field_tests.js b/addons/web/static/tests/views/fields/properties_field_tests.js
index bec09bd8bb4..c84884210c2 100644
--- a/addons/web/static/tests/views/fields/properties_field_tests.js
+++ b/addons/web/static/tests/views/fields/properties_field_tests.js
@@ -862,4 +862,90 @@ QUnit.module("Fields", (hooks) => {
"Should have removed Bob from the list"
);
});
+
+ QUnit.test("properties: date(time) property manipulations", async function (assert) {
+ serverData.models.partner.records.push({
+ id: 3,
+ display_name: "third partner",
+ properties: [
+ {
+ name: "property_1",
+ string: "My Date",
+ type: "date",
+ value: "2019-01-01",
+ },
+ {
+ name: "property_2",
+ string: "My DateTime",
+ type: "datetime",
+ value: "2019-01-01 10:00:00",
+ },
+ ],
+ company_id: 37,
+ });
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ resId: 3,
+ serverData,
+ arch: ``,
+ mockRPC(route, { method, args }) {
+ assert.step(method);
+ if (method === "check_access_rights") {
+ return true;
+ }
+ if (method === "write") {
+ assert.deepEqual(args[1].properties, [
+ {
+ name: "property_1",
+ string: "My Date",
+ type: "date",
+ value: "2018-12-31",
+ },
+ {
+ name: "property_2",
+ string: "My DateTime",
+ type: "datetime",
+ value: "2018-12-31 11:01:01",
+ },
+ ]);
+ }
+ },
+ });
+ assert.verifySteps(["get_views", "read", "check_access_rights"]);
+
+ // check initial properties
+ assert.equal(
+ target.querySelector("[property-name=property_1] .o_property_field_value").innerText,
+ "01/01/2019"
+ );
+ assert.equal(
+ target.querySelector("[property-name=property_2] .o_property_field_value").innerText,
+ "01/01/2019 11:00:00"
+ );
+
+ // edit date property
+ await click(target, ".o_form_button_edit");
+ await click(target, ".o_property_field[property-name=property_1] input");
+ await click(document.body, ".datepicker [data-day='12/31/2018']");
+ assert.equal(target.querySelector("[property-name=property_1] input").value, "12/31/2018");
+
+ // edit date time property
+ await click(target, ".o_property_field[property-name=property_2] input");
+ await click(document.body, ".datepicker [data-day='12/31/2018']");
+ await click(document.body, ".picker-switch [data-action=togglePicker]");
+ await click(document.body, ".timepicker [data-action=incrementHours]");
+ await click(document.body, ".timepicker [data-action=incrementMinutes]");
+ await click(document.body, ".timepicker [data-action=incrementSeconds]");
+ await click(document.body, ".picker-switch [data-action=close]");
+ assert.equal(
+ target.querySelector("[property-name=property_2] input").value,
+ "12/31/2018 12:01:01"
+ );
+
+ // save
+ assert.verifySteps([]);
+ await click(target, ".o_form_button_save");
+ assert.verifySteps(["write", "read"]);
+ });
});