[MERGE] web: improve the date/datetime utility functions
This PR: - adapts the following fix [1] in master branch, as the former has been fw-ported until saas-15.3 included - improves the dates utility functions (parse/format) by removing the timezone option, which is difficult to understand and has proven to be error prone - changes the records fields value/raw_value properties in kanban templates, in order to ease their usage [1] odoo/odoo#98413 Linked PR: odoo/enterprise#30780 closes odoo/odoo#98980 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -186,14 +186,14 @@
|
||||
<div t-attf-class="d-flex flex-column p-0 oe_kanban_card oe_kanban_global_click">
|
||||
<div class="o_kanban_content p-0 m-0 position-relative row d-flex flex-fill">
|
||||
<div class="col-3 text-bg-primary p-2 text-center d-flex flex-column justify-content-center">
|
||||
<div t-esc="record.date_begin.raw_value.getDate()" class="o_event_fontsize_20"/>
|
||||
<div t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('d')" class="o_event_fontsize_20"/>
|
||||
<div>
|
||||
<t t-esc="luxon.DateTime.fromJSDate(record.date_begin.raw_value).toFormat('MMM yyyy')"/>
|
||||
<t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('MMM yyyy')"/>
|
||||
</div>
|
||||
<div><t t-esc="luxon.DateTime.fromJSDate(record.date_begin.raw_value).toFormat('t')"/></div>
|
||||
<div t-if="luxon.DateTime.fromJSDate(record.date_begin.raw_value).toFormat('DDMMYYYY') !== luxon.DateTime.fromJSDate(record.date_end.raw_value).toFormat('DDMMYYYY')">
|
||||
<div><t t-esc="luxon.DateTime.fromISO(record.date_begin.raw_value).toFormat('t')"/></div>
|
||||
<div t-if="record.date_begin.raw_value !== record.date_end.raw_value">
|
||||
<i class="fa fa-arrow-right me-2 o_event_fontsize_09" title="End date"/>
|
||||
<t t-esc="luxon.DateTime.fromJSDate(record.date_end.raw_value).toFormat('d MMM')"/>
|
||||
<t t-esc="luxon.DateTime.fromISO(record.date_end.raw_value).toFormat('d MMM')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9 py-2 px-3 d-flex flex-column justify-content-between pt-3">
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<t t-if="new Date(record.expiration_date.raw_value) < (new Date())" t-set="expiration_class" t-value="'oe_kanban_text_red'"/>
|
||||
<t t-if="luxon.DateTime.fromISO(record.expiration_date.raw_value) < luxon.DateTime.local()" t-set="expiration_class" t-value="'oe_kanban_text_red'"/>
|
||||
<span t-att-class="expiration_class"><field name="start_date"/> - <field name="expiration_date"/></span>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<t t-if="record.comment.raw_value">
|
||||
<p class="o_kanban_record_subtitle"><em><field name="comment"/></em></p>
|
||||
</t>
|
||||
<p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="moment(record.create_date.raw_value).format('L')" /></p>
|
||||
<p>Granted by <a type="open"><field name="create_uid" /></a> the <t t-esc="luxon.DateTime.fromISO(record.create_date.raw_value).toFormat('D')" /></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<xpath expr="//li[@id='last_login']" position="inside">
|
||||
<span t-if="record.current_leave_id.raw_value" style="font-size: 100%"
|
||||
t-att-class="record.current_leave_state.raw_value=='validate'?'oe_kanban_button oe_kanban_color_3':'oe_kanban_button oe_kanban_color_2'"
|
||||
t-att-title="luxon.DateTime.fromJSDate(record.leave_date_from.raw_value).toFormat('ccc d MMM') + ' - ' + luxon.DateTime.fromJSDate(record.leave_date_to.raw_value).toFormat('ccc d MMM')">
|
||||
t-att-title="luxon.DateTime.fromISO(record.leave_date_from.raw_value).toFormat('ccc d MMM') + ' - ' + luxon.DateTime.fromISO(record.leave_date_to.raw_value).toFormat('ccc d MMM')">
|
||||
<field name="current_leave_id"/>
|
||||
</span>
|
||||
</xpath>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -465,7 +465,7 @@
|
||||
<t t-out="record.maintenance_open_count.raw_value"/> Request
|
||||
</div>
|
||||
<div class="badge text-bg-secondary" t-if="!selection_mode and record.next_action_date.raw_value" >
|
||||
<t t-esc="luxon.DateTime.fromJSDate(record.next_action_date.raw_value).toFormat('d MMMM')"/>
|
||||
<t t-esc="luxon.DateTime.fromISO(record.next_action_date.raw_value).toFormat('d MMMM')"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
<field name="user_id" widget="many2one_avatar_user"
|
||||
options="{'display_avatar_name': True}"/>
|
||||
-
|
||||
<t t-esc="luxon.DateTime.fromJSDate(record.create_date.raw_value).toFormat('MMM yyyy')"/>
|
||||
<t t-esc="luxon.DateTime.fromISO(record.create_date.raw_value).toFormat('MMM yyyy')"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -86,22 +86,22 @@ const FIELD_OPERATORS = {
|
||||
],
|
||||
};
|
||||
|
||||
function parseField(field, value, opts = {}) {
|
||||
function parseField(field, value) {
|
||||
if (FIELD_TYPES[field.type] === "char") {
|
||||
return value;
|
||||
}
|
||||
const type = field.type === "id" ? "integer" : field.type;
|
||||
const parse = parsers.contains(type) ? parsers.get(type) : (v) => v;
|
||||
return parse(value, { field, ...opts });
|
||||
return parse(value);
|
||||
}
|
||||
|
||||
function formatField(field, value, opts = {}) {
|
||||
function formatField(field, value) {
|
||||
if (FIELD_TYPES[field.type] === "char") {
|
||||
return value;
|
||||
}
|
||||
const type = field.type === "id" ? "integer" : field.type;
|
||||
const format = formatters.contains(type) ? formatters.get(type) : (v) => v;
|
||||
return format(value, { digits: field.digits, ...opts });
|
||||
return format(value, { digits: field.digits });
|
||||
}
|
||||
|
||||
export class CustomFilterItem extends Component {
|
||||
@@ -224,7 +224,7 @@ export class CustomFilterItem extends Component {
|
||||
domainValue = condition.value.map(serialize);
|
||||
descriptionArray.push(
|
||||
`"${condition.value
|
||||
.map((val) => formatField(field, val, { timezone: true }))
|
||||
.map((val) => formatField(field, val))
|
||||
.join(" " + this.env._t("and") + " ")}"`
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -112,7 +112,7 @@ function getRawValue(record, fieldName) {
|
||||
}
|
||||
case "date":
|
||||
case "datetime": {
|
||||
return value && value.toJSDate();
|
||||
return value && value.toISO();
|
||||
}
|
||||
default: {
|
||||
return value;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -446,6 +446,30 @@ QUnit.module("Search", (hooks) => {
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("custom filter date with equal operator", async function (assert) {
|
||||
patchTimeZone(-240);
|
||||
patchDate(2017, 1, 22, 12, 30, 0);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
await editConditionField(target, 0, "date_field");
|
||||
await editConditionOperator(target, 0, "=");
|
||||
await editConditionValue(target, 0, "01/01/2017");
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ['A date is equal to "01/01/2017"']);
|
||||
assert.deepEqual(getDomain(controlPanel), [["date_field", "=", "2017-01-01"]]);
|
||||
});
|
||||
|
||||
QUnit.test("custom filter datetime with equal operator", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7140,8 +7140,11 @@ QUnit.module("Views", (hooks) => {
|
||||
"</kanban>",
|
||||
});
|
||||
|
||||
assert.ok(getCard(0).querySelector(".date").innerText.startsWith("Wed Jan 25 2017"));
|
||||
assert.ok(getCard(1).querySelector(".datetime").innerText.startsWith("Mon Dec 12 2016"));
|
||||
assert.equal(getCard(0).querySelector(".date").innerText, "2017-01-25T00:00:00.000+01:00");
|
||||
assert.equal(
|
||||
getCard(1).querySelector(".datetime").innerText,
|
||||
"2016-12-12T11:55:05.000+01:00"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("rendering many2one (value)", async (assert) => {
|
||||
|
||||
Reference in New Issue
Block a user