[IMP] web: simplify format/parse/deserialize date(time) utils
The goal is to improve the dates utility functions (parse/format) by removing the timezone option, which is difficult to understand and has proven to be error prone. - Rationale When manipulating DateTime objects on the JS side, associated timezones must be always coherent. Until now, there were several code parts that had to check the DateTime timezone and handle things differently in a case or another. This sometimes lead to wrong code. E.g. there was a bug in the DatePicker component because the DateTime object it receives in props is in local timezone, but once it updates it through the parse utility, the new object is in UTC. - After this commit The DateTime objects you'll get through deserialization or parsing are always set in the user's local timezone. The formatted strings you'll get through formatDate and formatDateTime utils will always be expressed in the user's local timezone. The formatted strings you'll get through serialization utils will always be expressed in UTC. - serializeDate and serializeDateTime - expected input: a DateTime object (its timezone does not matter) - outputs: a string formatted for the server expressed in UTC - formatDate and formatDateTime - expected input: a DateTime object (its timezone does not matter) - outputs: a string formatted for the user, expressed in the user's TZ - deserializeDate and deserializeDateTime - expected input: a date(time) string provided by the server, in UTC - outputs: a DateTime object in user's TZ - parseDate and parseDateTime - expected input: a date(time) string provided by the user, in its TZ - outputs: a DateTime object in user's TZ - Other changes in this commit As the timezone option has been removed from parsing/formatting utils, all their usage had been adapted in the codebase. The large diff in dates_tests.js is because some tests were reorganized, others were removed/adapted/joined. A test has been removed from daterange_field_tests.js, because it has no sense: it displays a date field as a datetime, but a date field does not have any time information. Part-of: odoo/odoo#98980 Co-authored-by: Julien Mougenot <jum@odoo.com>
This commit is contained in:
co-authored by
Julien Mougenot
parent
49caf7c06b
commit
0f49724436
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+14
-9
@@ -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, {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
<t t-name="web.DomainSelectorDateTimeField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<t t-component="component" date="parsedValue" onDateTimeChanged.bind="onChange" />
|
||||
<t t-component="component" date="deserializedValue" onDateTimeChanged.bind="onChange" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
<form>
|
||||
<field name="date" widget="daterange" options="{'related_end_date': 'date_end', 'format_type': 'datetime'}"/>
|
||||
<field name="date_end" widget="daterange" options="{'related_start_date': 'date', 'format_type': 'datetime'}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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": `
|
||||
<form>
|
||||
<field name="bar"/>
|
||||
<field name="foo" widget="domain" options="{'model': 'bar'}"/>
|
||||
</form>`,
|
||||
"partner,false,search": `<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" };
|
||||
|
||||
|
||||
@@ -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: `<form><field name="company_id"/><field name="properties"/></form>`,
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user