diff --git a/addons/hr_holidays/static/src/tours/hr_holidays_tour.js b/addons/hr_holidays/static/src/tours/hr_holidays_tour.js
index e0e5c8898aa..0e25e70c81a 100644
--- a/addons/hr_holidays/static/src/tours/hr_holidays_tour.js
+++ b/addons/hr_holidays/static/src/tours/hr_holidays_tour.js
@@ -37,13 +37,13 @@ registry.category("web_tour.tours").add('hr_holidays_tour', {
in_modal: false,
},
{
- trigger: '.o_field_widget[name="request_date_from"] input',
+ trigger: 'input[data-field=request_date_from]',
content: _t("You can select the period you need to take off, from start date to end date"),
position: 'right',
run: `text ${leaveDateFrom}`,
},
{
- trigger: '.o_field_widget[name="request_date_to"] input',
+ trigger: 'input[data-field=request_date_to]',
content: _t("You can select the period you need to take off, from start date to end date"),
position: 'right',
run: `text ${leaveDateTo}`,
diff --git a/addons/note/static/src/web/activity/activity_menu_patch.js b/addons/note/static/src/web/activity/activity_menu_patch.js
index 95cb66b36e7..474dadf10e7 100644
--- a/addons/note/static/src/web/activity/activity_menu_patch.js
+++ b/addons/note/static/src/web/activity/activity_menu_patch.js
@@ -1,11 +1,11 @@
/** @odoo-module */
-import { ActivityMenu } from "@mail/web/activity/activity_menu";
-import { patch } from "@web/core/utils/patch";
-import { useRef, useState, useEffect } from "@odoo/owl";
-import { useService } from "@web/core/utils/hooks";
-import { DatePicker } from "@web/core/datepicker/datepicker";
import { onExternalClick } from "@mail/utils/hooks";
+import { ActivityMenu } from "@mail/web/activity/activity_menu";
+import { useEffect, useRef, useState } from "@odoo/owl";
+import { DateTimeInput } from "@web/core/datetime/datetime_input";
+import { useService } from "@web/core/utils/hooks";
+import { patch } from "@web/core/utils/patch";
patch(ActivityMenu.prototype, "note", {
setup() {
@@ -25,7 +25,7 @@ patch(ActivityMenu.prototype, "note", {
onExternalClick("noteInput", (ev) => {
if (
ev.target.closest(".o-mail-ActivityMenu-show") ||
- ev.target.closest(".bootstrap-datetimepicker-widget")
+ ev.target.closest(".o_datetime_picker")
) {
return;
}
@@ -69,5 +69,5 @@ patch(ActivityMenu.prototype, "note", {
ActivityMenu.components = {
...ActivityMenu.components,
- DatePicker,
+ DateTimeInput,
};
diff --git a/addons/note/static/src/web/activity/activity_menu_patch.xml b/addons/note/static/src/web/activity/activity_menu_patch.xml
index 86567ac2355..f23e8c63e94 100644
--- a/addons/note/static/src/web/activity/activity_menu_patch.xml
+++ b/addons/note/static/src/web/activity/activity_menu_patch.xml
@@ -10,9 +10,10 @@
-
diff --git a/addons/note/static/tests/activity_menu_tests.js b/addons/note/static/tests/activity_menu_tests.js
index e94a4b9dd3b..7f76c4d67fe 100644
--- a/addons/note/static/tests/activity_menu_tests.js
+++ b/addons/note/static/tests/activity_menu_tests.js
@@ -55,7 +55,7 @@ QUnit.test("create note from activity menu with date", async function (assert) {
await editInput(target, "input.o-mail-ActivityMenu-input", "New Note");
await editInput(
target,
- "input.o_datepicker_input",
+ "input.o_datetime_input",
futureDay.toString(luxon.DateTime.DATE_SHORT)
);
await click("button:contains(SAVE)");
diff --git a/addons/project/static/tests/tours/project_update_tour_tests.js b/addons/project/static/tests/tours/project_update_tour_tests.js
index 716f5d5a5d1..958c6926fd0 100644
--- a/addons/project/static/tests/tours/project_update_tour_tests.js
+++ b/addons/project/static/tests/tours/project_update_tour_tests.js
@@ -106,7 +106,7 @@ registry.category("web_tour.tours").add('project_update_tour', {
trigger: "div.o_field_widget[name=name] input",
run: 'text New milestone'
}, {
- trigger: "div[name=deadline] .datetimepicker-input",
+ trigger: "input[data-field=deadline]",
run: 'text 12/12/2099'
}, {
trigger: ".modal-footer .o_form_button_save"
@@ -116,14 +116,14 @@ registry.category("web_tour.tours").add('project_update_tour', {
trigger: "div.o_field_widget[name=name] input",
run: 'text Second milestone'
}, {
- trigger: "div[name=deadline] .datetimepicker-input",
+ trigger: "input[data-field=deadline]",
run: 'text 12/12/2022'
}, {
trigger: ".modal-footer .o_form_button_save"
}, {
trigger: ".o_rightpanel_milestone:eq(1) .o_milestone_detail",
}, {
- trigger: "div[name=deadline] .datetimepicker-input",
+ trigger: "input[data-field=deadline]",
run: 'text 12/12/2100'
}, {
trigger: ".modal-footer .o_form_button_save"
diff --git a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js
index d6521b33e9a..10468ab7063 100644
--- a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js
+++ b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js
@@ -261,7 +261,7 @@ registry.category("web_tour.tours").add('sale_timesheet_tour', {
content: "Edit new Milestone",
run: 'text New milestone',
}, {
- trigger: "div[name=deadline] .datetimepicker-input",
+ trigger: "input[data-field=deadline]",
content: "Edit new Milestone",
run: 'text 12/12/2099',
}, {
diff --git a/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js
index 782aaf4603c..d84fa488faf 100644
--- a/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js
+++ b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.js
@@ -1,10 +1,10 @@
/** @odoo-module */
-import { YearPicker } from "../year_picker";
+import { Component, onWillUpdateProps } from "@odoo/owl";
import { dateOptions } from "@spreadsheet/global_filters/helpers";
+import { DateTimeInput } from "@web/core/datetime/datetime_input";
const { DateTime } = luxon;
-import { Component, onWillUpdateProps } from "@odoo/owl";
export class DateFilterValue extends Component {
setup() {
@@ -57,7 +57,7 @@ export class DateFilterValue extends Component {
}
}
DateFilterValue.template = "spreadsheet_edition.DateFilterValue";
-DateFilterValue.components = { YearPicker };
+DateFilterValue.components = { DateTimeInput };
DateFilterValue.props = {
// See @spreadsheet_edition/bundle/global_filters/filters_plugin.RangeType
diff --git a/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml
index 1048cf300cd..fb5863cc816 100644
--- a/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml
+++ b/addons/spreadsheet/static/src/global_filters/components/filter_date_value/filter_date_value.xml
@@ -13,10 +13,13 @@
-
diff --git a/addons/spreadsheet/static/src/global_filters/components/year_picker.js b/addons/spreadsheet/static/src/global_filters/components/year_picker.js
deleted file mode 100644
index 390e03970fa..00000000000
--- a/addons/spreadsheet/static/src/global_filters/components/year_picker.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/** @odoo-module */
-
-import { DatePicker } from "@web/core/datepicker/datepicker";
-const { DateTime } = luxon;
-
-const DEFAULT_DATE = DateTime.local();
-export class YearPicker extends DatePicker {
- /**
- * @override
- */
- initFormat() {
- super.initFormat();
- // moment.js format
- this.defaultFormat = "yyyy";
- this.staticFormat = "yyyy";
- }
-
- /**
- * @override
- */
- getOptions(useStatic = false) {
- return {
- format:
- !useStatic || this.isValidStaticFormat(this.format) ? this.format : this.staticFormat,
- locale: DEFAULT_DATE.locale,
- };
- }
-
- /**
- * @override
- */
- bootstrapDateTimePicker(commandOrParams) {
- if (typeof commandOrParams === "object") {
- const widgetParent = window.$(this.rootRef.el);
- commandOrParams = { ...commandOrParams, widgetParent };
- }
- super.bootstrapDateTimePicker(commandOrParams);
- }
-
- /**
- * @override
- */
- onWillUpdateProps(nextProps) {
- const pickerParams = {};
- let shouldUpdateInput = false;
- for (const prop in nextProps) {
- const prev = this.props[prop];
- const next = nextProps[prop];
- if (
- (prev instanceof DateTime && next instanceof DateTime && !prev.equals(next)) ||
- prev !== next
- ) {
- pickerParams[prop] = nextProps[prop];
- if (prop === "date") {
- this.setDateAndFormat(nextProps);
- shouldUpdateInput = true;
- }
- }
- }
- if (shouldUpdateInput) {
- this.updateInput();
- }
- this.bootstrapDateTimePicker(pickerParams);
- }
- /**
- * @override: allow displaying empty dates
- */
- updateInput({ useStatic } = {}) {
- const [formattedValue] = this.formatValue(this.date, this.getOptions(useStatic));
- this.inputRef.el.value = formattedValue || this.props.placeholder;
- }
-
- /**
- * @override
- */
- onDateChange({ useStatic } = {}) {
- const [date] = this.parseValue(this.inputRef.el.value, this.getOptions(useStatic));
- if (!date || (this.date instanceof DateTime && date.equals(this.date))) {
- this.updateInput();
- } else {
- this.state.warning = date > DateTime.local();
- this.props.onDateTimeChanged(date);
- }
- }
-}
-
-const props = {
- ...DatePicker.props,
- date: { type: DateTime, optional: true },
-};
-delete props["format"];
-
-YearPicker.props = props;
-
-YearPicker.defaultProps = {
- ...DatePicker.defaultProps,
-};
diff --git a/addons/spreadsheet_dashboard/static/tests/dashboard/dashboard_action_test.js b/addons/spreadsheet_dashboard/static/tests/dashboard/dashboard_action_test.js
index 7384c3c2a96..fdb2caac705 100644
--- a/addons/spreadsheet_dashboard/static/tests/dashboard/dashboard_action_test.js
+++ b/addons/spreadsheet_dashboard/static/tests/dashboard/dashboard_action_test.js
@@ -174,12 +174,10 @@ QUnit.test(
const serverData = getServerData(spreadsheetData);
const fixture = getFixture();
await createSpreadsheetDashboard({ serverData });
- const year = fixture.querySelector(".o_cp_top_right input.o_datepicker_input");
+ const year = fixture.querySelector(".o_cp_top_right input.o_datetime_input");
const this_year = luxon.DateTime.local().year;
assert.equal(year.value, String(this_year));
- const input = fixture.querySelector(
- "input.o_datepicker_input.o_input.datetimepicker-input"
- );
+ const input = fixture.querySelector("input.o_datetime_input");
await click(input);
await editInput(input, null, String(this_year - 1));
await nextTick();
@@ -189,6 +187,6 @@ QUnit.test(
await click(fixture.querySelector(".o_cp_top_right .fa-times"));
assert.containsNone(fixture, ".o_cp_top_right .fa-times");
- assert.equal(year.value, "Select year...");
+ assert.equal(year.placeholder, "Select year...");
}
);
diff --git a/addons/test_mail/static/tests/tracking_value_tests.js b/addons/test_mail/static/tests/tracking_value_tests.js
index 2f25d52a7ac..41bd0949dbd 100644
--- a/addons/test_mail/static/tests/tracking_value_tests.js
+++ b/addons/test_mail/static/tests/tracking_value_tests.js
@@ -228,7 +228,7 @@ QUnit.test(
});
await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(
- target.querySelector("div[name=date_field] .o_datepicker .o_datepicker_input"),
+ target.querySelector("div[name=date_field] input"),
"12/14/2018",
["change"]
);
@@ -249,7 +249,7 @@ QUnit.test(
});
await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(
- target.querySelector("div[name=date_field] .o_datepicker .o_datepicker_input"),
+ target.querySelector("div[name=date_field] input"),
"",
["change"]
);
@@ -271,7 +271,7 @@ QUnit.test(
});
await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(
- target.querySelector("div[name=datetime_field] .o_datepicker .o_datepicker_input"),
+ target.querySelector("div[name=datetime_field] input"),
"12/14/2018 13:42:28",
["change"]
);
@@ -297,7 +297,7 @@ QUnit.test(
});
await this.start({ res_id: mailTestTrackAllId1 });
await testUtils.fields.editAndTrigger(
- target.querySelector("div[name=datetime_field] .o_datepicker .o_datepicker_input"),
+ target.querySelector("div[name=datetime_field] input"),
"",
["change"]
);
diff --git a/addons/web/static/src/core/datepicker/datepicker.js b/addons/web/static/src/core/datepicker/datepicker.js
deleted file mode 100644
index bb101000db8..00000000000
--- a/addons/web/static/src/core/datepicker/datepicker.js
+++ /dev/null
@@ -1,354 +0,0 @@
-/** @odoo-module **/
-
-import {
- areDateEquals,
- formatDate,
- formatDateTime,
- luxonToMoment,
- luxonToMomentFormat,
- momentToLuxon,
- parseDate,
- parseDateTime,
-} from "@web/core/l10n/dates";
-import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
-import { localization } from "@web/core/l10n/localization";
-import { useAutofocus } from "@web/core/utils/hooks";
-import { isMobileOS } from "@web/core/browser/feature_detection";
-
-import {
- Component,
- onMounted,
- onWillUpdateProps,
- onWillUnmount,
- useExternalListener,
- useRef,
- useState,
-} from "@odoo/owl";
-const { DateTime } = luxon;
-
-let datePickerId = 0;
-
-/**
- * @param {Function} fn
- * @returns {[any, null] | [null, Error]}
- */
-function wrapError(fn) {
- return (...args) => {
- const result = [null, null];
- try {
- result[0] = fn(...args);
- } catch (_err) {
- result[1] = _err;
- }
- return result;
- };
-}
-
-/**
- * Date picker
- *
- * This component exposes the API of the tempusdominus datepicker library.
- * As such, its template is a simple input that will open the TD datepicker
- * when clicked on. The component will also synchronize any user-input value
- * with the library widget and vice-versa.
- *
- * Note that all props given to this component will be passed as arguments to
- * instantiate the picker widget. Also any luxon date is automatically
- * stringified since tempusdominus only works with moment objects.
- * @extends Component
- */
-export class DatePicker extends Component {
- setup() {
- this.rootRef = useRef("root");
- this.inputRef = useRef("input");
- this.state = useState({ warning: false });
-
- this.datePickerId = `o_datepicker_${datePickerId++}`;
- // Manually keep track of the "open" state to write the date in the
- // static format just before bootstrap parses it.
- this.datePickerShown = false;
-
- this.initFormat();
- this.setDateAndFormat(this.props);
-
- useAutofocus();
- useExternalListener(window, "scroll", this.onWindowScroll, { capture: true });
-
- onMounted(this.onMounted);
- onWillUpdateProps(this.onWillUpdateProps);
- onWillUnmount(this.onWillUnmount);
- }
-
- onMounted() {
- this.bootstrapDateTimePicker(this.props);
- this.updateInput();
-
- window.$(this.rootRef.el).on("show.datetimepicker", () => {
- this.datePickerShown = true;
- this.inputRef.el.select();
- });
- window.$(this.rootRef.el).on("hide.datetimepicker", ({date}) => {
- this.datePickerShown = false;
- this.onDateChange({ eventDate: date, useStatic: true });
- });
- window.$(this.rootRef.el).on("error.datetimepicker", () => false);
- }
-
- onWillUpdateProps(nextProps) {
- const pickerParams = {};
- for (const prop in nextProps) {
- if (!areDateEquals(this.props[prop], nextProps[prop])) {
- pickerParams[prop] = nextProps[prop];
- }
- }
- this.setDateAndFormat(nextProps);
- if ("date" in pickerParams || "format" in pickerParams) {
- this.updateInput();
- }
- this.bootstrapDateTimePicker(pickerParams);
- }
-
- onWillUnmount() {
- window.$(this.rootRef.el).off(); // Removes all jQuery events
- this.bootstrapDateTimePicker("destroy");
- }
-
- //---------------------------------------------------------------------
- // Protected
- //---------------------------------------------------------------------
-
- /**
- * @param {string} format
- * @returns {boolean}
- */
- isValidStaticFormat(format) {
- return /^[\d\s\/.,:-]+$/.test(DateTime.local().toFormat(format));
- }
-
- getOptions(useStatic = false) {
- return {
- format:
- !useStatic || this.isValidStaticFormat(this.format) ? this.format : this.staticFormat,
- locale: this.props.locale || (this.date && this.date.locale),
- };
- }
-
- /**
- * Initialises formatting and parsing parameters
- */
- initFormat() {
- this.defaultFormat = localization.dateFormat;
- this.staticFormat = "yyyy/MM/dd";
- this.formatValue = wrapError(formatDate);
- this.parseValue = wrapError(parseDate);
- this.isLocal = false;
- }
-
- /**
- * Sets the current date value. If a locale is provided, the given date
- * will first be set in that locale.
- * @param {Object} params
- * @param {DateTime} params.date
- * @param {string} [params.locale]
- * @param {string} [params.format]
- */
- setDateAndFormat({ date, locale, format }) {
- this.date = date && locale ? date.setLocale(locale) : date;
- // Fallback to default localization format in `@web/core/l10n/dates.js`.
- this.format = format || this.defaultFormat;
- }
-
- /**
- * Updates the input element with the current formatted date value.
- * @param {Object} [params={}]
- * @param {boolean} [params.useStatic]
- */
- updateInput({ useStatic } = {}) {
- const [formattedDate] = this.formatValue(this.date, this.getOptions(useStatic));
- if (formattedDate !== null) {
- this.inputRef.el.value = formattedDate;
- this.props.onUpdateInput(formattedDate);
- }
- }
-
- //---------------------------------------------------------------------
- // Bootstrap datepicker
- //---------------------------------------------------------------------
-
- /**
- * Handles bootstrap datetimepicker calls.
- * @param {string | Object} commandOrParams
- */
- bootstrapDateTimePicker(commandOrParams) {
- if (typeof commandOrParams === "object") {
- const format = this.isValidStaticFormat(this.format) ? this.format : this.staticFormat;
- const params = {
- ...commandOrParams,
- date: this.date || null,
- format: luxonToMomentFormat(format),
- locale: commandOrParams.locale || (this.date && this.date.locale),
- };
- for (const prop in params) {
- if (params[prop] instanceof DateTime) {
- params[prop] = luxonToMoment(params[prop]);
- }
- }
- commandOrParams = params;
- }
- return window.$(this.rootRef.el).datetimepicker(commandOrParams);
- }
-
- //---------------------------------------------------------------------
- // Handlers
- //---------------------------------------------------------------------
-
- onInputClick() {
- if (!this.datePickerShown) {
- this.updateInput({ useStatic: true });
- }
- this.bootstrapDateTimePicker("toggle");
- }
-
- /**
- * Called either when the input value has changed or when the boostrap
- * datepicker is closed. The onDateTimeChanged prop is only called if the
- * date value has changed.
- * @param {Object} [params={}]
- * @param {boolean} [params.useStatic]
- */
- onDateChange({ eventDate, useStatic } = {}) {
- const { value } = this.inputRef.el;
- let parsedDate = value && eventDate ? momentToLuxon(eventDate).setLocale(this.getOptions().locale): null;
- if (!parsedDate) {
- const options = this.getOptions(useStatic);
- parsedDate = this.parseValue(value, options)[0];
- }
- this.state.warning = parsedDate && parsedDate > DateTime.local();
- // Always update input.
- // if the date is invalid, it will reset to default (= given) date.
- // if the input is a computed date (+5d for instance), it will put the correct date.
- this.updateInput();
- if (parsedDate !== null && !areDateEquals(this.date, parsedDate)) {
- this.props.onDateTimeChanged(parsedDate);
- }
- }
-
- onInputKeydown(ev) {
- const hotkey = getActiveHotkey(ev);
- if (this.datePickerShown && hotkey === "escape") {
- this.bootstrapDateTimePicker("hide");
- this.inputRef.el.select();
- ev.preventDefault();
- ev.stopPropagation();
- }
- }
-
- /**
- * @param {Event} ev
- */
- onWindowScroll(ev) {
- if (!isMobileOS() && ev.target !== this.inputRef.el) {
- this.bootstrapDateTimePicker("hide");
- }
- }
-}
-
-DatePicker.defaultProps = {
- calendarWeeks: true,
- icons: {
- clear: "fa fa-delete",
- close: "fa fa-check primary",
- date: "fa fa-calendar",
- down: "fa fa-chevron-down",
- next: "fa fa-chevron-right",
- previous: "fa fa-chevron-left",
- time: "fa fa-clock-o",
- today: "fa fa-calendar-check-o",
- up: "fa fa-chevron-up",
- },
- inputId: "",
- maxDate: DateTime.fromObject({ year: 9999, month: 12, day: 31 }),
- minDate: DateTime.fromObject({ year: 1000 }),
- useCurrent: false,
- widgetParent: "body",
- onInput: () => {},
- onUpdateInput: () => {},
-};
-DatePicker.props = {
- // Components props
- onDateTimeChanged: Function,
- date: { type: [DateTime, { value: false }], optional: true },
- warn_future: { type: Boolean, optional: true },
- // Bootstrap datepicker options
- buttons: {
- type: Object,
- shape: {
- showClear: Boolean,
- showClose: Boolean,
- showToday: Boolean,
- },
- optional: true,
- },
- calendarWeeks: { type: Boolean, optional: true },
- format: { type: String, optional: true },
- icons: {
- type: Object,
- shape: {
- clear: String,
- close: String,
- date: String,
- down: String,
- next: String,
- previous: String,
- time: String,
- today: String,
- up: String,
- },
- optional: true,
- },
- inputId: { type: String, optional: true },
- keyBinds: { validate: (kb) => typeof kb === "object" || kb === null, optional: true },
- locale: { type: String, optional: true },
- maxDate: { type: DateTime, optional: true },
- minDate: { type: DateTime, optional: true },
- readonly: { type: Boolean, optional: true },
- useCurrent: { type: Boolean, optional: true },
- widgetParent: { type: String, optional: true },
- daysOfWeekDisabled: { type: Array, optional: true },
- placeholder: { type: String, optional: true },
- onInput: { type: Function, optional: true },
- onUpdateInput: { type: Function, optional: true },
-};
-DatePicker.template = "web.DatePicker";
-
-/**
- * Date/time picker
- *
- * Similar to the DatePicker component, adding the handling of more specific
- * time values: hour-minute-second.
- *
- * Once again, refer to the tempusdominus documentation for implementation
- * details.
- * @extends DatePicker
- */
-export class DateTimePicker extends DatePicker {
- /**
- * @override
- */
- initFormat() {
- this.defaultFormat = localization.dateTimeFormat;
- this.staticFormat = "yyyy/MM/dd HH:mm:ss";
- this.formatValue = wrapError(formatDateTime);
- this.parseValue = wrapError(parseDateTime);
- this.isLocal = true;
- }
-}
-
-DateTimePicker.defaultProps = {
- ...DatePicker.defaultProps,
- buttons: {
- showClear: false,
- showClose: true,
- showToday: false,
- },
-};
diff --git a/addons/web/static/src/core/datepicker/datepicker.xml b/addons/web/static/src/core/datepicker/datepicker.xml
deleted file mode 100644
index 99a82e6924c..00000000000
--- a/addons/web/static/src/core/datepicker/datepicker.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/web/static/src/core/datetime/datetime_hook.js b/addons/web/static/src/core/datetime/datetime_hook.js
new file mode 100644
index 00000000000..ddce0c2eec4
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_hook.js
@@ -0,0 +1,393 @@
+/** @odoo-module **/
+
+import {
+ onPatched,
+ onWillStart,
+ onWillUpdateProps,
+ reactive,
+ useComponent,
+ useEffect,
+ useEnv,
+ useRef,
+} from "@odoo/owl";
+import { areDatesEqual } from "../l10n/dates";
+import { usePopover } from "../popover/popover_hook";
+import { registry } from "../registry";
+import { ensureArray, zip, zipWith } from "../utils/arrays";
+import { DateTimePicker } from "./datetime_picker";
+import { DateTimePickerPopover } from "./datetime_picker_popover";
+
+/**
+ * @typedef {luxon.DateTime} DateTime
+ *
+ * @typedef DateTimePickerHookParams
+ * @property {string} [format]
+ * @property {(value: DateTimePickerProps["value"]) => any} [onChange]
+ * @property {(value: DateTimePickerProps["value"]) => any} [onApply]
+ * @property {DateTimePickerProps | (props: Record) => PromiseLike} [pickerProps]
+ * @property {string | ReturnType} [target]
+ *
+ * @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps
+ */
+
+/**
+ * @template {HTMLElement} T
+ * @typedef {{ el: T | null }} OwlRef
+ */
+
+const FOCUS_CLASSNAME = "text-primary";
+
+/**
+ * @param {DateTimePickerHookParams} hookParams
+ */
+export const useDateTimePicker = (hookParams) => {
+ // Hook methods
+
+ /**
+ * @param {HTMLElement} el
+ * @param {string} type
+ * @param {(ev: Event) => any} listener
+ */
+ const addListener = (el, type, listener) => {
+ el.addEventListener(type, listener);
+ cleanups.push(() => el.removeEventListener(type, listener));
+ };
+
+ /**
+ * Wrapper method on the "onApply" callback to only call it when the
+ * value has changed, and set other internal variables accordingly.
+ */
+ const checkAndApply = () => {
+ if (areDatesEqual(pickerProps.value, lastAppliedDate)) {
+ return;
+ }
+ lastAppliedDate = pickerProps.value;
+ inputsChanged = ensureArray(pickerProps.value).map(() => false);
+ onApply?.(pickerProps.value);
+ };
+
+ /**
+ * Ensures the given input has the focus and applies the class names on all
+ * inputs accordingly.
+ *
+ * @param {HTMLInputElement} inputEl
+ */
+ const focusInput = (inputEl) => {
+ const { activeElement } = inputEl.ownerDocument;
+ if (activeElement !== inputEl) {
+ inputEl.focus();
+ }
+ inputEl.selectionStart = 0;
+ inputEl.selectionEnd = inputEl.value.length;
+ setFocusClass(inputEl);
+ };
+
+ /**
+ * @param {number} valueIndex
+ * @returns
+ */
+ const getInput = (valueIndex) => {
+ const el = inputRefs[valueIndex]?.el;
+ if (el && document.body.contains(el)) {
+ return el;
+ }
+ return null;
+ };
+
+ /**
+ * Returns the appropriate root element to attach the popover:
+ * - if the value is a range: the closest common parent of the two inputs
+ * - if not: the first input
+ */
+ const getPopoverTarget = () => {
+ if (targetRef?.el) {
+ return targetRef.el;
+ }
+ if (isRange(pickerProps.value)) {
+ let parentElement = getInput(0).parentElement;
+ const inputEls = inputRefs.map((ref) => ref.el);
+ while (parentElement && !inputEls.every((inputEl) => parentElement.contains(inputEl))) {
+ parentElement = parentElement.parentElement;
+ }
+ return parentElement || getInput(0);
+ } else {
+ return getInput(0);
+ }
+ };
+
+ /**
+ * @param {unknown} [value]
+ * @returns {boolean}
+ */
+ const isRange = (value) => Array.isArray(value);
+
+ /**
+ * Inputs "change" event handler. This will trigger an "onApply" callback if
+ * one of the following is true:
+ * - there is only one input;
+ * - the popover is closed;
+ * - the other input has also changed.
+ *
+ * @param {Event} ev
+ */
+ const onInputChange = (ev) => {
+ updateValueFromInputs();
+ inputsChanged[ev.target === getInput(1) ? 1 : 0] = true;
+ if (!popover.isOpen || inputsChanged.every(Boolean)) {
+ saveAndClose();
+ }
+ };
+
+ /**
+ * @param {KeyboardEvent} ev
+ */
+ const onInputKeydown = (ev) => {
+ switch (ev.key) {
+ case "Enter":
+ case "Escape": {
+ ev.preventDefault();
+ return saveAndClose();
+ }
+ case "Tab": {
+ updateValueFromInputs();
+ if (!getInput(0) || !getInput(1) || ev.target !== getInput(ev.shiftKey ? 1 : 0)) {
+ return saveAndClose();
+ }
+ }
+ }
+ };
+
+ /**
+ * @param {DateTimePickerProps} nextProps
+ */
+ const onPropsUpdated = (nextProps) => {
+ const { pickerProps: getPickerProps } = hookParams;
+ const nextPickerProps =
+ typeof getPickerProps === "function"
+ ? getPickerProps(nextProps || comp.props)
+ : getPickerProps;
+
+ canNotify = false;
+ Object.assign(pickerProps, nextPickerProps);
+ canNotify = true;
+
+ lastChangedDate = lastAppliedDate = pickerProps.value;
+ inputsChanged = ensureArray(pickerProps.value).map(() => false);
+ };
+
+ /**
+ * @param {HTMLInputElement} inputEl
+ */
+ const openFromInput = (inputEl) => {
+ pickerProps.focusedDateIndex = inputEl === getInput(1) ? 1 : 0;
+
+ if (!popover.isOpen) {
+ if (inputEl && env.isSmall) {
+ inputEl.scrollIntoView(true);
+ }
+
+ popover.open(getPopoverTarget(), { pickerProps });
+ }
+
+ focusInput(inputEl);
+ };
+
+ /**
+ * @template {"format" | "parse"} T
+ * @param {T} operation
+ * @param {T extends "format" ? DateTime : string} value
+ * @returns {[T extends "format" ? string : DateTime, null] | [null, Error]}
+ */
+ const safeConvert = (operation, value) => {
+ const { type } = pickerProps;
+ const convertFn = registry
+ .category(operation === "format" ? "formatters" : "parsers")
+ .get(type);
+ try {
+ return [convertFn(value, { format }), null];
+ } catch (error) {
+ if (error?.name === "ConversionError") {
+ return [null, error];
+ } else {
+ throw error;
+ }
+ }
+ };
+
+ /**
+ * Wrapper method to ensure the "onApply" callback is called, either:
+ * - by closing the popover (if any);
+ * - or by directly calling "checkAndApply", without updating the values.
+ */
+ const saveAndClose = () => {
+ if (popover.isOpen) {
+ // check & apply will be done in the "onClose" callback
+ popover.close();
+ } else {
+ checkAndApply();
+ }
+ };
+
+ /**
+ * Updates class names on given inputs according to the currently selected input.
+ *
+ * @param {HTMLInputElement | null} input
+ */
+ const setFocusClass = (input) => {
+ for (const { el } of inputRefs) {
+ if (el) {
+ el.classList.toggle(FOCUS_CLASSNAME, popover.isOpen && el === input);
+ }
+ }
+ };
+
+ /**
+ * Reads every key of the reactive picker props to subscribe to their changes.
+ */
+ const subscribeToPickerProps = () =>
+ Object.keys(pickerProps).forEach((key) => pickerProps[key]);
+
+ /**
+ * Synchronizes the given input with the given value.
+ *
+ * @param {HTMLInputElement} el
+ * @param {DateTime} value
+ */
+ const updateInput = (el, value) => {
+ const [formattedValue] = safeConvert("format", value);
+ el.value = formattedValue || "";
+ };
+
+ /**
+ * @param {DateTimePickerProps["value"]} value
+ */
+ const updateValue = (value) => {
+ if (isRange(pickerProps.value)) {
+ // When in range: compare each individual value
+ const [currentStart, currentEnd] = ensureArray(pickerProps.value);
+ const [nextStart, nextEnd] = ensureArray(value);
+ const areStartDatesEqual = areDatesEqual(currentStart, nextStart);
+ const areEndDatesEqual = areDatesEqual(currentEnd, nextEnd);
+ if (
+ (pickerProps.focusedDateIndex === 0 && areEndDatesEqual) ||
+ (pickerProps.focusedDateIndex === 1 && areStartDatesEqual)
+ ) {
+ pickerProps.focusedDateIndex = pickerProps.focusedDateIndex === 1 ? 0 : 1;
+ }
+ }
+ pickerProps.value = value;
+ };
+
+ const updateValueFromInputs = () => {
+ const values = zipWith(
+ inputRefs,
+ ensureArray(pickerProps.value),
+ ({ el }, currentValue) => {
+ if (!el) {
+ return currentValue;
+ }
+ const [parsedValue, error] = safeConvert("parse", el.value);
+ if (error) {
+ updateInput(el, currentValue);
+ return currentValue;
+ } else {
+ return parsedValue;
+ }
+ }
+ );
+ updateValue(isRange(pickerProps.value) ? values : values[0]);
+ };
+
+ // Hook variables
+
+ const { format, onChange, onApply, target } = hookParams;
+ /** @type {ReturnType>[]} */
+ const targetRef = typeof target === "string" ? useRef(target) : target;
+ const inputRefs = [useRef("start-date"), useRef("end-date")];
+ const env = useEnv();
+ const comp = useComponent();
+ const popover = usePopover(DateTimePickerPopover, {
+ onClose: () => {
+ updateValueFromInputs();
+ checkAndApply();
+ setFocusClass(null);
+ },
+ });
+ const rawPickerProps = {
+ ...DateTimePicker.defaultProps,
+ onSelect: (value) => {
+ updateValue(value);
+ if (!isRange(value) && pickerProps.type === "date") {
+ saveAndClose();
+ }
+ },
+ };
+ const pickerProps = reactive(rawPickerProps, () => {
+ subscribeToPickerProps();
+
+ // Update inputs
+ for (const [{ el }, value] of zip(inputRefs, ensureArray(pickerProps.value), true)) {
+ if (el) {
+ updateInput(el, value);
+ }
+ }
+
+ // Will focus input on next patch
+ shouldFocus = true;
+ setFocusClass(getInput(pickerProps.focusedDateIndex));
+
+ if (!canNotify || areDatesEqual(lastChangedDate, pickerProps.value)) {
+ return;
+ }
+
+ lastChangedDate = pickerProps.value;
+ onChange?.(pickerProps.value);
+ });
+
+ let canNotify = false;
+ let inputsChanged = [];
+ let lastAppliedDate;
+ let lastChangedDate;
+ let shouldFocus = false;
+
+ subscribeToPickerProps();
+
+ onWillStart(onPropsUpdated);
+ onWillUpdateProps(onPropsUpdated);
+
+ onPatched(() => {
+ if (!shouldFocus || !popover.isOpen) {
+ return;
+ }
+ const input = getInput(pickerProps.focusedDateIndex);
+ if (input) {
+ shouldFocus = false;
+ focusInput(input);
+ }
+ });
+
+ const cleanups = [];
+ useEffect(() => {
+ let editableInputs = 0;
+ for (const [{ el }, value] of zip(inputRefs, ensureArray(pickerProps.value), true)) {
+ if (el && !el.disabled && !el.readOnly) {
+ addListener(el, "change", onInputChange);
+ addListener(el, "focus", (ev) => openFromInput(ev.target));
+ addListener(el, "keydown", onInputKeydown);
+
+ updateInput(el, value);
+ editableInputs++;
+ }
+ }
+ if (!editableInputs) {
+ saveAndClose();
+ }
+ return () => {
+ while (cleanups.length) {
+ cleanups.pop()();
+ }
+ };
+ });
+
+ return pickerProps;
+};
diff --git a/addons/web/static/src/core/datetime/datetime_input.js b/addons/web/static/src/core/datetime/datetime_input.js
new file mode 100644
index 00000000000..cc238bcef98
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_input.js
@@ -0,0 +1,40 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+import { omit } from "../utils/objects";
+import { useDateTimePicker } from "./datetime_hook";
+import { DateTimePicker } from "./datetime_picker";
+
+/**
+ * @typedef {import("./datetime_picker").DateTimePickerProps & {
+ * format?: string;
+ * id?: string;
+ * onApply?: (value: DateTime) => any;
+ * onChange?: (value: DateTime) => any;
+ * placeholder?: string;
+ * }} DateTimeInputProps
+ */
+
+/** @extends {Component} */
+export class DateTimeInput extends Component {
+ static props = {
+ ...DateTimePicker.props,
+ format: { type: String, optional: true },
+ id: { type: String, optional: true },
+ onChange: { type: Function, optional: true },
+ onApply: { type: Function, optional: true },
+ placeholder: { type: String, optional: true },
+ };
+
+ static template = "web.DateTimeInput";
+
+ setup() {
+ useDateTimePicker({
+ format: this.props.format,
+ pickerProps: (props) =>
+ omit(props, "format", "placeholder", "id", "onChange", "onApply"),
+ onApply: (...args) => this.props.onApply?.(...args),
+ onChange: (...args) => this.props.onChange?.(...args),
+ });
+ }
+}
diff --git a/addons/web/static/src/core/datetime/datetime_input.xml b/addons/web/static/src/core/datetime/datetime_input.xml
new file mode 100644
index 00000000000..8753b33d507
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_input.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/addons/web/static/src/core/datetime/datetime_picker.js b/addons/web/static/src/core/datetime/datetime_picker.js
new file mode 100644
index 00000000000..b878e2bb7cd
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_picker.js
@@ -0,0 +1,706 @@
+/** @odoo-module **/
+
+import {
+ Component,
+ onWillRender,
+ onWillUpdateProps,
+ useExternalListener,
+ useState,
+} from "@odoo/owl";
+import { _lt } from "@web/core/l10n/translation";
+import {
+ MAX_VALID_DATE,
+ MIN_VALID_DATE,
+ clampDate,
+ is24HourFormat,
+ isInRange,
+ isMeridiemFormat,
+ today,
+} from "../l10n/dates";
+import { localization } from "../l10n/localization";
+import { ensureArray } from "../utils/arrays";
+
+/**
+ * @typedef DateItem
+ * @property {string} id
+ * @property {boolean} includesToday
+ * @property {boolean} isOutOfRange
+ * @property {boolean} isInvalid
+ * @property {string} label
+ * @property {DateRange} range
+ *
+ * @typedef {"today" | NullableDateTime} DateLimit
+ *
+ * @typedef {[DateTime, DateTime]} DateRange
+ *
+ * @typedef {luxon.DateTime} DateTime
+ *
+ * @typedef DateTimePickerProps
+ * @property {number} [focusedDateIndex=0]
+ * @property {DateLimit} [maxDate]
+ * @property {PrecisionLevel} [maxPrecision="decades"]
+ * @property {DateLimit} [minDate]
+ * @property {PrecisionLevel} [minPrecision="days"]
+ * @property {(value: DateTime) => any} [onSelect]
+ * @property {number} [rounding=5]
+ * @property {{ buttons?: any }} [slots]
+ * @property {"date" | "datetime"} [type]
+ * @property {NullableDateTime | NullableDateRange} value
+ *
+ * @typedef {DateItem | MonthItem} Item
+ *
+ * @typedef MonthItem
+ * @property {[string, string][]} daysOfWeek
+ * @property {string} id
+ * @property {number} number
+ * @property {WeekItem[]} weeks
+ *
+ * @typedef {import("@web/core/l10n/dates").NullableDateTime} NullableDateTime
+ *
+ * @typedef {import("@web/core/l10n/dates").NullableDateRange} NullableDateRange
+ *
+ * @typedef PrecisionInfo
+ * @property {(date: DateTime, params: Partial) => string} getTitle
+ * @property {(date: DateTime, params: Partial) => Item[]} getItems
+ * @property {string} mainTitle
+ * @property {string} nextTitle
+ * @property {string} prevTitle
+ * @property {Record} step
+ *
+ * @typedef {"days" | "months" | "years" | "decades"} PrecisionLevel
+ *
+ * @typedef WeekItem
+ * @property {DateItem[]} days
+ * @property {number} number
+ */
+
+const { DateTime } = luxon;
+
+/**
+ * @param {DateTime} date
+ */
+const getStartOfDecade = (date) => Math.floor(date.year / 10) * 10;
+
+/**
+ * @param {DateTime} date
+ */
+const getStartOfCentury = (date) => Math.floor(date.year / 100) * 100;
+
+/**
+ * @param {DateTime} date
+ */
+const getStartOfWeek = (date) => {
+ const { weekStart } = localization;
+ return date.set({ weekday: date.weekday < weekStart ? weekStart - 7 : weekStart });
+};
+
+/**
+ * @param {number} min
+ * @param {number} max
+ */
+const numberRange = (min, max) => [...Array(max - min)].map((_, i) => i + min);
+
+/**
+ * @param {NullableDateTime | "today"} value
+ * @param {NullableDateTime | "today"} defaultValue
+ */
+const parseLimitDate = (value, defaultValue) =>
+ clampDate(value === "today" ? today() : value || defaultValue, MIN_VALID_DATE, MAX_VALID_DATE);
+
+/**
+ * @param {Object} params
+ * @param {boolean} [params.isOutOfRange=false]
+ * @param {boolean} [params.isInvalid=false]
+ * @param {keyof DateTime} params.label
+ * @param {[DateTime, DateTime]} params.range
+ * @returns {DateItem}
+ */
+const toDateItem = ({ isOutOfRange = false, isInvalid = false, label, range }) => ({
+ id: range[0].toISODate(),
+ includesToday: isInRange(today(), range),
+ isOutOfRange,
+ isInvalid,
+ label: String(range[0][label]),
+ range,
+});
+
+/**
+ * @param {DateItem[]} weekDayItems
+ * @returns {WeekItem}
+ */
+const toWeekItem = (weekDayItems) => ({
+ number: weekDayItems[3].range[0].weekNumber,
+ days: weekDayItems,
+});
+
+// Time constants
+const HOURS = numberRange(0, 24).map((hour) => [hour, String(hour)]);
+const MINUTES = numberRange(0, 60).map((minute) => [minute, String(minute || 0).padStart(2, "0")]);
+const MERIDIEMS = ["AM", "PM"];
+
+/**
+ * Precision levels
+ * @type {Map}
+ */
+const PRECISION_LEVELS = new Map()
+ .set("days", {
+ mainTitle: _lt("Select month"),
+ nextTitle: _lt("Next month"),
+ prevTitle: _lt("Previous month"),
+ step: { month: 1 },
+ getTitle: (date, { additionalMonth }) => {
+ const titles = [`${date.monthLong} ${date.year}`];
+ if (additionalMonth) {
+ const next = date.plus({ month: 1 });
+ titles.push(`${next.monthLong} ${next.year}`);
+ }
+ return titles;
+ },
+ getItems: (date, { additionalMonth, maxDate, minDate, showWeekNumbers }) => {
+ const startDates = [date];
+ if (additionalMonth) {
+ startDates.push(date.plus({ month: 1 }));
+ }
+ return startDates.map((date, i) => {
+ const monthRange = [date.startOf("month"), date.endOf("month")];
+ /** @type {WeekItem[]} */
+ const weeks = [];
+
+ // Generate 6 weeks for current month
+ let startOfNextWeek = getStartOfWeek(monthRange[0]);
+ for (let w = 0; w < 6; w++) {
+ const weekDayItems = [];
+ // Generate all days of the week
+ for (let d = 0; d < 7; d++) {
+ const day = startOfNextWeek.plus({ day: d });
+ const dayItem = toDateItem({
+ isOutOfRange: !isInRange(day, monthRange),
+ isInvalid: !isInRange(day, [minDate, maxDate]),
+ label: "day",
+ range: [day, day.endOf("day")],
+ });
+ weekDayItems.push(dayItem);
+ if (d === 6) {
+ startOfNextWeek = day.plus({ day: 1 });
+ }
+ }
+ weeks.push(toWeekItem(weekDayItems));
+ }
+
+ // Generate days of week labels
+ const daysOfWeek = weeks[0].days.map((d) => [
+ d.range[0].weekdayShort,
+ d.range[0].weekdayLong,
+ ]);
+ if (showWeekNumbers) {
+ daysOfWeek.unshift(["#", _lt("Week numbers")]);
+ }
+
+ return {
+ id: `__month__${i}`,
+ number: monthRange[0].month,
+ daysOfWeek,
+ weeks,
+ };
+ });
+ },
+ })
+ .set("months", {
+ mainTitle: _lt("Select year"),
+ nextTitle: _lt("Next year"),
+ prevTitle: _lt("Previous year"),
+ step: { year: 1 },
+ getTitle: (date) => String(date.year),
+ getItems: (date, { maxDate, minDate }) => {
+ const startOfYear = date.startOf("year");
+ return numberRange(0, 12).map((i) => {
+ const startOfMonth = startOfYear.plus({ month: i });
+ return toDateItem({
+ isInvalid: !isInRange(startOfMonth, [minDate, maxDate]),
+ label: "monthShort",
+ range: [startOfMonth, startOfMonth.endOf("month")],
+ });
+ });
+ },
+ })
+ .set("years", {
+ mainTitle: _lt("Select decade"),
+ nextTitle: _lt("Next decade"),
+ prevTitle: _lt("Previous decade"),
+ step: { year: 10 },
+ getTitle: (date) => `${getStartOfDecade(date) - 1} - ${getStartOfDecade(date) + 10}`,
+ getItems: (date, { maxDate, minDate }) => {
+ const startOfDecade = date.startOf("year").set({ year: getStartOfDecade(date) });
+ return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => {
+ const startOfYear = startOfDecade.plus({ year: i });
+ return toDateItem({
+ isOutOfRange: i < 0 || i >= GRID_COUNT,
+ isInvalid: !isInRange(startOfYear, [minDate, maxDate]),
+ label: "year",
+ range: [startOfYear, startOfYear.endOf("year")],
+ });
+ });
+ },
+ })
+ .set("decades", {
+ mainTitle: _lt("Select century"),
+ nextTitle: _lt("Next century"),
+ prevTitle: _lt("Previous century"),
+ step: { year: 100 },
+ getTitle: (date) => `${getStartOfCentury(date) - 10} - ${getStartOfCentury(date) + 100}`,
+ getItems: (date, { maxDate, minDate }) => {
+ const startOfCentury = date.startOf("year").set({ year: getStartOfCentury(date) });
+ return numberRange(-GRID_MARGIN, GRID_COUNT + GRID_MARGIN).map((i) => {
+ const startOfDecade = startOfCentury.plus({ year: i * 10 });
+ return toDateItem({
+ label: "year",
+ isOutOfRange: i < 0 || i >= GRID_COUNT,
+ isInvalid: !isInRange(startOfDecade, [minDate, maxDate]),
+ range: [startOfDecade, startOfDecade.plus({ year: 10, millisecond: -1 })],
+ });
+ });
+ },
+ });
+
+// Other constants
+const GRID_COUNT = 10;
+const GRID_MARGIN = 1;
+const NULLABLE_DATETIME_PROPERTY = [DateTime, { value: false }, { value: null }];
+
+/** @extends {Component} */
+export class DateTimePicker extends Component {
+ static props = {
+ focusedDateIndex: { type: Number, optional: true },
+ maxDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: "today" }], optional: true },
+ maxPrecision: {
+ type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })),
+ optional: true,
+ },
+ minDate: { type: [NULLABLE_DATETIME_PROPERTY, { value: "today" }], optional: true },
+ minPrecision: {
+ type: [...PRECISION_LEVELS.keys()].map((value) => ({ value })),
+ optional: true,
+ },
+ onSelect: { type: Function, optional: true },
+ rounding: { type: Number, optional: true },
+ slots: {
+ type: Object,
+ shape: { buttons: { type: Object, optional: true } },
+ optional: true,
+ },
+ type: { type: [{ value: "date" }, { value: "datetime" }], optional: true },
+ value: {
+ type: [
+ NULLABLE_DATETIME_PROPERTY,
+ {
+ type: Array,
+ element: NULLABLE_DATETIME_PROPERTY,
+ validate: (values) => values.length === 2,
+ },
+ ],
+ optional: true,
+ },
+ };
+
+ static defaultProps = {
+ focusedDateIndex: 0,
+ maxPrecision: "decades",
+ minPrecision: "days",
+ rounding: 5,
+ type: "datetime",
+ };
+
+ static template = "web.DateTimePicker";
+
+ //-------------------------------------------------------------------------
+ // Getters
+ //-------------------------------------------------------------------------
+
+ get activePrecisionLevel() {
+ return PRECISION_LEVELS.get(this.state.precision);
+ }
+
+ get isKeynavEnabled() {
+ return this.isRange || this.props.type === "datetime";
+ }
+
+ get isLastPrecisionLevel() {
+ return (
+ this.allowedPrecisionLevels.indexOf(this.state.precision) ===
+ this.allowedPrecisionLevels.length - 1
+ );
+ }
+
+ get titles() {
+ return ensureArray(this.title);
+ }
+
+ //-------------------------------------------------------------------------
+ // Lifecycle
+ //-------------------------------------------------------------------------
+
+ setup() {
+ this.availableHours = HOURS;
+ this.availableMinutes = MINUTES;
+ /** @type {PrecisionLevel[]} */
+ this.allowedPrecisionLevels = [];
+ /** @type {Item[]} */
+ this.items = [];
+ this.title = "";
+ this.isRange = false;
+ this.shouldAdjustFocusDate = false;
+
+ this.state = useState({
+ /** @type {DateTime | null} */
+ focusDate: null,
+ /** @type {DateTime | null} */
+ hoveredDate: null,
+ /** @type {[number, number, number][]} */
+ timeValues: [],
+ /** @type {PrecisionLevel} */
+ precision: this.props.minPrecision,
+ });
+
+ this.onPropsUpdated(this.props);
+ onWillUpdateProps((nextProps) => this.onPropsUpdated(nextProps));
+
+ onWillRender(() => this.onWillRender());
+
+ useExternalListener(
+ window,
+ "keydown",
+ (ev) => this.isKeynavEnabled && this.onWindowKeyDown(ev)
+ );
+ }
+
+ /**
+ * @param {DateTimePickerProps} props
+ */
+ onPropsUpdated(props) {
+ /** @type {[NullableDateTime] | NullableDateRange} */
+ this.values = ensureArray(props.value).map((value) =>
+ value && !value.isValid ? null : value
+ );
+ this.isRange = Array.isArray(props.value);
+ this.availableHours = HOURS;
+ this.availableMinutes = MINUTES.filter((minute) => !(minute[0] % props.rounding));
+ this.allowedPrecisionLevels = this.filterPrecisionLevels(
+ props.minPrecision,
+ props.maxPrecision
+ );
+
+ this.additionalMonth = this.isRange && !this.env.isSmall;
+ this.maxDate = parseLimitDate(props.maxDate, MAX_VALID_DATE).endOf("day");
+ this.minDate = parseLimitDate(props.minDate, MIN_VALID_DATE).startOf("day");
+
+ if (this.maxDate < this.minDate) {
+ throw new Error(`DateTimePicker error: given "maxDate" comes before "minDate".`);
+ }
+
+ this.state.timeValues = this.values.map((val) => [
+ (val || DateTime.local()).hour,
+ val?.minute || 0,
+ val?.second || 0,
+ ]);
+
+ this.adjustFocus(this.values, props.focusedDateIndex);
+ this.handle12HourSystem();
+ }
+
+ onWillRender() {
+ const precision = this.activePrecisionLevel;
+ const getterParams = {
+ additionalMonth: this.additionalMonth,
+ maxDate: this.maxDate,
+ minDate: this.minDate,
+ showWeekNumbers: !this.isRange,
+ };
+ const referenceDate = this.state.focusDate;
+ this.title = precision.getTitle(referenceDate, getterParams);
+ this.items = precision.getItems(referenceDate, getterParams);
+ }
+
+ //-------------------------------------------------------------------------
+ // Methods
+ //-------------------------------------------------------------------------
+
+ /**
+ * @param {NullableDateTime[]} values
+ * @param {number} focusedDateIndex
+ */
+ adjustFocus(values, focusedDateIndex) {
+ if (
+ !this.shouldAdjustFocusDate &&
+ this.state.focusDate &&
+ focusedDateIndex === this.props.focusedDateIndex
+ ) {
+ return;
+ }
+
+ let dateToFocus =
+ values[focusedDateIndex] || values[focusedDateIndex === 1 ? 0 : 1] || today();
+
+ if (
+ this.additionalMonth &&
+ focusedDateIndex === 1 &&
+ values[0] &&
+ values[1] &&
+ values[0].month !== values[1].month
+ ) {
+ dateToFocus = dateToFocus.minus({ month: 1 });
+ }
+
+ this.shouldAdjustFocusDate = false;
+ this.state.focusDate = this.clamp(dateToFocus.startOf("month"));
+ }
+
+ /**
+ * @param {DateTime} value
+ */
+ clamp(value) {
+ return clampDate(value, this.minDate, this.maxDate);
+ }
+
+ /**
+ * @param {PrecisionLevel} minPrecision
+ * @param {PrecisionLevel} maxPrecision
+ */
+ filterPrecisionLevels(minPrecision, maxPrecision) {
+ const levels = [...PRECISION_LEVELS.keys()];
+ return levels.slice(levels.indexOf(minPrecision), levels.indexOf(maxPrecision) + 1);
+ }
+
+ /**
+ * @param {DateItem} item
+ */
+ getActiveRangeInfo({ range }) {
+ const values = [...this.values];
+ const result = {
+ isActive: isInRange(values, range),
+ isStart: false,
+ isEnd: false,
+ };
+ if (this.isRange) {
+ // Handle active "start" and "end" dates
+ if (result.isActive) {
+ const [start, end] = values;
+ result.isStart = !start || isInRange(start, range);
+ result.isEnd = !end || isInRange(end, range);
+ }
+
+ // Handle preview state
+ if (this.state.hoveredDate) {
+ values[this.props.focusedDateIndex] = this.state.hoveredDate;
+ // TODO: comment this
+ if (values[0] && values[1] && values[1].ts < values[0].ts) {
+ values.reverse();
+ }
+ const [selectStart, selectEnd] = values;
+ if (isInRange(values, range)) {
+ result.isActive = true;
+ }
+ if (!selectStart || isInRange(selectStart, range)) {
+ result.isStart = true;
+ }
+ if (!selectEnd || isInRange(selectEnd, range)) {
+ result.isEnd = true;
+ }
+ }
+ } else {
+ result.isStart = result.isEnd = result.isActive;
+ }
+ return result;
+ }
+
+ getTimeValues(valueIndex) {
+ let [hour, minute, second] = this.state.timeValues[valueIndex].map(Number);
+ if (
+ this.is12HourFormat &&
+ this.meridiems &&
+ this.state.timeValues[valueIndex][3] === "PM"
+ ) {
+ hour += 12;
+ }
+ return [hour, minute, second];
+ }
+
+ handle12HourSystem() {
+ if (isMeridiemFormat()) {
+ this.meridiems = MERIDIEMS.map((m) => [m, m]);
+ for (const timeValues of this.state.timeValues) {
+ timeValues.push(MERIDIEMS[Math.floor(timeValues[0] / 12) || 0]);
+ }
+ }
+ this.is12HourFormat = !is24HourFormat();
+ if (this.is12HourFormat) {
+ this.availableHours = [[0, HOURS[12][1]], ...HOURS.slice(1, 12)];
+ for (const timeValues of this.state.timeValues) {
+ timeValues[0] %= 12;
+ }
+ }
+ }
+
+ /**
+ * @param {DateItem} item
+ */
+ isSelectedDate({ range }) {
+ return this.values.some((value) => isInRange(value, range));
+ }
+
+ /**
+ * Softly moves the selected date by the given duration.
+ * @param {import("luxon").DurationLike} duration
+ */
+ navigate(duration) {
+ const { focusedDateIndex } = this.props;
+ const value = (this.values[focusedDateIndex] || today()).plus(duration);
+
+ this.shouldAdjustFocusDate = this.validateAndSelect(value, focusedDateIndex);
+ }
+
+ /**
+ * Goes to the next panel (e.g. next month if precision is "days").
+ * If an event is given it will be prevented.
+ * @param {Event} [ev]
+ */
+ next(ev) {
+ ev?.preventDefault();
+ const { step } = this.activePrecisionLevel;
+ this.state.focusDate = this.clamp(this.state.focusDate.plus(step));
+ }
+
+ /**
+ * Goes to the previous panel (e.g. previous month if precision is "days").
+ * If an event is given it will be prevented.
+ * @param {Event} [ev]
+ */
+ previous(ev) {
+ ev?.preventDefault();
+ const { step } = this.activePrecisionLevel;
+ this.state.focusDate = this.clamp(this.state.focusDate.minus(step));
+ }
+
+ /**
+ * Happens when an hour or a minute (or AM/PM if can apply) is selected.
+ * @param {number} valueIndex
+ */
+ selectTime(valueIndex) {
+ const value = this.values[valueIndex] || today();
+ this.validateAndSelect(value, valueIndex);
+ }
+
+ /**
+ * @param {DateTime} value
+ * @param {number} valueIndex
+ */
+ validateAndSelect(value, valueIndex) {
+ if (!this.props.onSelect) {
+ // No onSelect handler
+ return false;
+ }
+ const result = [...this.values];
+ if (this.isRange) {
+ if (value.endOf("day") < result[0]) {
+ valueIndex = 0;
+ } else if (value.startOf("day") > result[1]) {
+ valueIndex = 1;
+ }
+ }
+ if (this.props.type === "datetime") {
+ // Adjusts result according to the current time values
+ const [hour, minute, second] = this.getTimeValues(valueIndex);
+ value = value.set({ hour, minute, second });
+ }
+ if (!isInRange(value, [this.minDate, this.maxDate])) {
+ // Date is outside range defined by min and max dates
+ return false;
+ }
+ result[valueIndex] = value;
+ this.props.onSelect(this.isRange ? result : result[0]);
+ return true;
+ }
+
+ /**
+ * Returns whether the zoom has occurred
+ * @param {DateTime} date
+ */
+ zoomIn(date) {
+ const index = this.allowedPrecisionLevels.indexOf(this.state.precision) - 1;
+ if (index in this.allowedPrecisionLevels) {
+ this.state.focusDate = this.clamp(date);
+ this.state.precision = this.allowedPrecisionLevels[index];
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the zoom has occurred
+ */
+ zoomOut() {
+ const index = this.allowedPrecisionLevels.indexOf(this.state.precision) + 1;
+ if (index in this.allowedPrecisionLevels) {
+ this.state.precision = this.allowedPrecisionLevels[index];
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Happens when a date item is selected:
+ * - first tries to zoom in on the item
+ * - if could not zoom in: date is considered as final value and triggers a hard select
+ * @param {DateItem} dateItem
+ */
+ zoomOrSelect(dateItem) {
+ if (dateItem.isInvalid) {
+ // Invalid item
+ return;
+ }
+ if (this.zoomIn(dateItem.range[0])) {
+ // Zoom was successful
+ return;
+ }
+ const [value] = dateItem.range;
+ const valueIndex = this.props.focusedDateIndex;
+ const isValid = this.validateAndSelect(value, valueIndex);
+ this.shouldAdjustFocusDate = isValid && !this.isRange;
+ }
+
+ //-------------------------------------------------------------------------
+ // Handlers
+ //-------------------------------------------------------------------------
+
+ /**
+ * @param {KeyboardEvent} ev
+ */
+ onWindowKeyDown(ev) {
+ const preventAnd = () => ev.preventDefault();
+
+ switch (ev.key) {
+ case "n": {
+ return preventAnd(this.next(ev));
+ }
+ case "p": {
+ return preventAnd(this.previous(ev));
+ }
+ }
+
+ if (this.state.precision === "days" && ev.ctrlKey) {
+ switch (ev.key) {
+ case "ArrowDown": {
+ return preventAnd(this.navigate({ week: +1 }));
+ }
+ case "ArrowLeft": {
+ return preventAnd(this.navigate({ day: -1 }));
+ }
+ case "ArrowRight": {
+ return preventAnd(this.navigate({ day: +1 }));
+ }
+ case "ArrowUp": {
+ return preventAnd(this.navigate({ week: -1 }));
+ }
+ }
+ }
+ }
+}
diff --git a/addons/web/static/src/core/datetime/datetime_picker.scss b/addons/web/static/src/core/datetime/datetime_picker.scss
new file mode 100644
index 00000000000..392369ffd8d
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_picker.scss
@@ -0,0 +1,159 @@
+.o_datetime_picker {
+ --DateTimePicker__Cell-size-md: 3rem;
+ --DateTimePicker__Cell-size-lg: 5rem;
+ --DateTimePicker__Template-rows: 3;
+ --DateTimePicker__Template-columns: 4;
+ --DateTimePicker__Day-template-rows: 6;
+ --DateTimePicker__Today-highlight-color: white;
+
+ // Header
+
+ .o_datetime_picker_header .o_header_part {
+ text-transform: none;
+ }
+
+ // Day
+
+ .o_active {
+ --DateTimePicker__Cell-border-radius-left: 0;
+ --DateTimePicker__Cell-border-radius-right: 0;
+
+ // border-radius: var(--DateTimePicker__Cell-border-radius-left)
+ // var(--DateTimePicker__Cell-border-radius-right)
+ // var(--DateTimePicker__Cell-border-radius-right)
+ // var(--DateTimePicker__Cell-border-radius-left);
+
+ &.o_active_start,
+ &.o_active_end {
+ color: var(--DateTimePicker__Today-highlight-color);
+ }
+
+ &:not(.o_active_start):not(.o_active_end) {
+ --bg-opacity: 0.4;
+ }
+ }
+
+ .o_active_start {
+ &.o_cell_md {
+ --DateTimePicker__Cell-border-radius-left: 100vmax;
+ }
+ &.o_cell_lg {
+ --DateTimePicker__Cell-border-radius-left: 0.5rem;
+ }
+ }
+
+ .o_active_end {
+ &.o_cell_md {
+ --DateTimePicker__Cell-border-radius-right: 100vmax;
+ }
+ &.o_cell_lg {
+ --DateTimePicker__Cell-border-radius-right: 0.5rem;
+ }
+ }
+
+ .o_preview_active {
+ border-width: 2px 0 2px 0;
+
+ &.o_preview_start {
+ border-width: 2px 0 2px 2px;
+ }
+
+ &.o_preview_end {
+ border-width: 2px 2px 2px 0;
+ }
+ }
+
+ .o_today:not(.o_out_of_range) {
+ --DateTimePicker__Today-border-color: var(--primary);
+ --DateTimePicker__Today-border-offset: 2px;
+ --DateTimePicker__Today-border-width: 5px;
+
+ color: var(--DateTimePicker__Today-border-color);
+ position: relative;
+ box-shadow: inset 0 0 0 var(--DateTimePicker__Today-border-offset)
+ var(--DateTimePicker__Today-border-color);
+ font-weight: bold;
+
+ &.o_active {
+ --DateTimePicker__Today-border-color: white;
+ }
+
+ &:before {
+ content: "";
+ right: var(--DateTimePicker__Today-border-offset);
+ bottom: var(--DateTimePicker__Today-border-offset);
+ border: solid transparent;
+ border-width: 0 0 var(--DateTimePicker__Today-border-width)
+ var(--DateTimePicker__Today-border-width);
+ border-bottom-color: var(--DateTimePicker__Today-border-color);
+ position: absolute;
+ }
+ }
+
+ .o_out_of_range {
+ color: var(--gray-400);
+ --bg-opacity: 0.5;
+ }
+
+ .o_time_picker_select {
+ background-image: none;
+
+ &:focus,
+ &:hover {
+ border-color: var(--primary);
+ }
+ }
+
+ // Grids
+
+ .o_date_picker {
+ grid-template-rows: repeat(var(--DateTimePicker__Day-template-rows), 1fr);
+ grid-template-columns: repeat(var(--DateTimePicker__Day-template-columns), 1fr);
+ }
+
+ .o_date_item_picker {
+ grid-template-rows: repeat(var(--DateTimePicker__Template-rows), 1fr);
+ grid-template-columns: repeat(var(--DateTimePicker__Template-columns), 1fr);
+ }
+
+ // Utilities
+
+ .o_datetime_button {
+ &:hover {
+ background-color: var(--gray-100);
+ border-color: var(--gray-200);
+ }
+ }
+
+ .o_center {
+ display: grid;
+ place-items: center;
+ }
+
+ .o_cell_md {
+ padding: 0.4rem;
+ width: var(--DateTimePicker__Cell-size-md);
+ height: var(--DateTimePicker__Cell-size-md);
+ }
+
+ .o_cell_lg {
+ width: var(--DateTimePicker__Cell-size-lg);
+ height: var(--DateTimePicker__Cell-size-lg);
+ }
+
+ .o_text_sm {
+ font-size: 0.875rem;
+ }
+
+ .o_bg_inherit {
+ background-color: inherit;
+ }
+
+ .o_text_inherit {
+ color: inherit;
+ }
+
+ .o_hover_primary:hover {
+ color: var(--primary);
+ }
+}
diff --git a/addons/web/static/src/core/datetime/datetime_picker.xml b/addons/web/static/src/core/datetime/datetime_picker.xml
new file mode 100644
index 00000000000..305f2a47441
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_picker.xml
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/core/datetime/datetime_picker_popover.js b/addons/web/static/src/core/datetime/datetime_picker_popover.js
new file mode 100644
index 00000000000..a343596ddc9
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_picker_popover.js
@@ -0,0 +1,33 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+import { useHotkey } from "../hotkeys/hotkey_hook";
+import { DateTimePicker } from "./datetime_picker";
+
+/**
+ * @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps
+ *
+ * @typedef DateTimePickerPopoverProps
+ * @property {() => void} close
+ * @property {DateTimePickerProps} pickerProps
+ */
+
+/** @extends {Component} */
+export class DateTimePickerPopover extends Component {
+ static components = { DateTimePicker };
+
+ static props = {
+ close: Function, // Given by the Popover service
+ pickerProps: { type: Object, shape: DateTimePicker.props },
+ };
+
+ static template = "web.DateTimePickerPopover";
+
+ //-------------------------------------------------------------------------
+ // Lifecycle
+ //-------------------------------------------------------------------------
+
+ setup() {
+ useHotkey("enter", () => this.props.close());
+ }
+}
diff --git a/addons/web/static/src/core/datetime/datetime_picker_popover.xml b/addons/web/static/src/core/datetime/datetime_picker_popover.xml
new file mode 100644
index 00000000000..42fae6ec48e
--- /dev/null
+++ b/addons/web/static/src/core/datetime/datetime_picker_popover.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/core/domain_selector/domain_selector_fields.js b/addons/web/static/src/core/domain_selector/domain_selector_fields.js
index 2fe11c26dbc..98a6333b2a2 100644
--- a/addons/web/static/src/core/domain_selector/domain_selector_fields.js
+++ b/addons/web/static/src/core/domain_selector/domain_selector_fields.js
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { Component, useRef } from "@odoo/owl";
-import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
import { selectOperators } from "@web/core/domain_selector/domain_selector_operators";
import {
deserializeDate,
@@ -13,6 +12,9 @@ import { _lt } from "@web/core/l10n/translation";
import { evaluateExpr, formatAST } from "@web/core/py_js/py";
import { toPyValue } from "@web/core/py_js/py_utils";
import { registry } from "@web/core/registry";
+import { DateTimeInput } from "../datetime/datetime_input";
+
+const { DateTime } = luxon;
// ============================================================================
@@ -78,6 +80,20 @@ function makeEditor(component, props) {
};
}
+/**
+ * @param {"date" | "datetime"} type
+ * @param {Parameters} args
+ */
+const genericSerializeDate = (type, ...args) =>
+ type === "date" ? serializeDate(...args) : serializeDateTime(...args);
+
+/**
+ * @param {"date" | "datetime"} type
+ * @param {Parameters} args
+ */
+const genericDeserializeDate = (type, ...args) =>
+ type === "date" ? deserializeDate(...args) : deserializeDateTime(...args);
+
const DEFAULT = {
operators: [
"equal",
@@ -122,33 +138,6 @@ const BOOLEAN = {
// ----------------------------------------------------------------------------
-const DATE = {
- operators: [
- "equal",
- "not_equal",
- "greater_than",
- "greater_equal",
- "less_than",
- "less_equal",
- "set",
- "not_set",
- ],
- editors: {
- default: makeEditor(DatePicker, ({ value, update }) => ({
- date: deserializeDate(value),
- onDateTimeChanged: (value) => {
- if (!value.isValid) {
- return;
- }
- return update(value ? serializeDate(value) : luxon.DateTime.local());
- },
- })),
- },
- defaultValue: () => serializeDate(luxon.DateTime.local()),
-};
-
-// ----------------------------------------------------------------------------
-
const DATETIME = {
operators: [
"equal",
@@ -161,17 +150,14 @@ const DATETIME = {
"not_set",
],
editors: {
- default: makeEditor(DateTimePicker, ({ value, update }) => ({
- date: deserializeDateTime(value),
- onDateTimeChanged: (value) => {
- if (!value.isValid) {
- return;
- }
- return update(value ? serializeDateTime(value) : luxon.DateTime.local());
- },
+ default: makeEditor(DateTimeInput, ({ field, value, update }) => ({
+ value: genericDeserializeDate(field.type, value),
+ type: field.type,
+ onApply: (value) =>
+ update(value ? genericSerializeDate(field.type, value) : DateTime.local()),
})),
},
- defaultValue: () => serializeDateTime(luxon.DateTime.local()),
+ defaultValue: ({ type }) => genericSerializeDate(type, DateTime.local()),
};
// ----------------------------------------------------------------------------
@@ -269,7 +255,7 @@ const PROPERTIES_RELATIONAL = {
export const FIELD_DESCRIPTIONS = {
boolean: BOOLEAN,
char: TEXT,
- date: DATE,
+ date: DATETIME,
datetime: DATETIME,
float: NUMBER,
html: TEXT,
diff --git a/addons/web/static/src/core/dropdown/dropdown.js b/addons/web/static/src/core/dropdown/dropdown.js
index 55f5036ce4f..d607ef74cc4 100644
--- a/addons/web/static/src/core/dropdown/dropdown.js
+++ b/addons/web/static/src/core/dropdown/dropdown.js
@@ -311,7 +311,7 @@ export class Dropdown extends Component {
return;
}
- if (ev.target.closest(".bootstrap-datetimepicker-widget")) {
+ if (ev.target.closest(".o_datetime_picker")) {
return;
}
diff --git a/addons/web/static/src/views/fields/date/date_field.js b/addons/web/static/src/views/fields/date/date_field.js
deleted file mode 100644
index e34109da8db..00000000000
--- a/addons/web/static/src/views/fields/date/date_field.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/** @odoo-module **/
-
-import { DatePicker } from "@web/core/datepicker/datepicker";
-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";
-
-import { Component } from "@odoo/owl";
-
-export class DateField extends Component {
- static template = "web.DateField";
- static components = {
- DatePicker,
- };
- static props = {
- ...standardFieldProps,
- pickerOptions: { type: Object, optional: true },
- placeholder: { type: String, optional: true },
- };
- static defaultProps = {
- pickerOptions: {},
- };
-
- setup() {
- /**
- * The last value that has been commited to the model.
- * Not changed in case of invalid field value.
- */
- this.lastSetValue = null;
- }
-
- get isDateTime() {
- return this.props.record.fields[this.props.name].type === "datetime";
- }
- get date() {
- return (
- this.props.record.data[this.props.name] &&
- this.props.record.data[this.props.name].startOf("day")
- );
- }
-
- get formattedValue() {
- return this.isDateTime
- ? formatDateTime(this.props.record.data[this.props.name], {
- format: localization.dateFormat,
- })
- : formatDate(this.props.record.data[this.props.name]);
- }
-
- onDateTimeChanged(date) {
- if (!areDateEquals(this.date || "", date)) {
- this.props.record.update({ [this.props.name]: date });
- }
- }
- onDatePickerInput(ev) {
- this.props.record.model.bus.trigger(
- "FIELD_IS_DIRTY",
- ev.target.value !== this.lastSetValue
- );
- }
- onUpdateInput(date) {
- this.props.record.model.bus.trigger("FIELD_IS_DIRTY", false);
- this.lastSetValue = date;
- }
-}
-
-export const dateField = {
- component: DateField,
- displayName: _lt("Date"),
- supportedTypes: ["date", "datetime"],
- extractProps: ({ attrs, options }) => ({
- pickerOptions: options.datepicker,
- placeholder: attrs.placeholder,
- }),
-};
-
-registry.category("fields").add("date", dateField);
diff --git a/addons/web/static/src/views/fields/date/date_field.xml b/addons/web/static/src/views/fields/date/date_field.xml
deleted file mode 100644
index 679adfcbbfb..00000000000
--- a/addons/web/static/src/views/fields/date/date_field.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/web/static/src/views/fields/daterange/daterange_field.js b/addons/web/static/src/views/fields/daterange/daterange_field.js
deleted file mode 100644
index 222e381e36c..00000000000
--- a/addons/web/static/src/views/fields/daterange/daterange_field.js
+++ /dev/null
@@ -1,193 +0,0 @@
-/** @odoo-module **/
-
-import { _lt } from "@web/core/l10n/translation";
-import { registry } from "@web/core/registry";
-import { loadJS } from "@web/core/assets";
-import { luxonToMoment, momentToLuxon } from "@web/core/l10n/dates";
-import { useService } from "@web/core/utils/hooks";
-import { standardFieldProps } from "../standard_field_props";
-
-import { Component, onWillStart, useExternalListener, useRef, useEffect } from "@odoo/owl";
-const formatters = registry.category("formatters");
-const parsers = registry.category("parsers");
-
-export class DateRangeField extends Component {
- static template = "web.DateRangeField";
- static props = {
- ...standardFieldProps,
- relatedEndDateField: { type: String, optional: true },
- relatedStartDateField: { type: String, optional: true },
- formatType: { type: String, optional: true },
- placeholder: { type: String, optional: true },
- };
-
- setup() {
- this.notification = useService("notification");
- this.root = useRef("root");
- this.isPickerShown = false;
- this.pickerContainer;
-
- useExternalListener(window, "scroll", this.onWindowScroll, { capture: true });
- onWillStart(() => loadJS("/web/static/lib/daterangepicker/daterangepicker.js"));
- useEffect(
- (el) => {
- if (el) {
- window.$(el).daterangepicker({
- timePicker: this.isDateTime,
- timePicker24Hour: true,
- timePickerIncrement: 5,
- autoUpdateInput: false,
- locale: {
- applyLabel: this.env._t("Apply"),
- cancelLabel: this.env._t("Cancel"),
- },
- startDate: this.startDate ? luxonToMoment(this.startDate) : window.moment(),
- endDate: this.endDate ? luxonToMoment(this.endDate) : window.moment(),
- drops: "auto",
- });
- this.pickerContainer = window.$(el).data("daterangepicker").container[0];
-
- window.$(el).on("apply.daterangepicker", this.onPickerApply.bind(this));
- window.$(el).on("show.daterangepicker", this.onPickerShow.bind(this));
- window.$(el).on("hide.daterangepicker", this.onPickerHide.bind(this));
-
- this.pickerContainer.dataset.name = this.props.name;
- }
-
- return () => {
- if (el) {
- this.pickerContainer.remove();
- }
- };
- },
- () => [this.root.el, this.props.record.data[this.props.name]]
- );
- }
-
- get formatType() {
- return this.props.formatType || this.props.record.fields[this.props.name].type;
- }
- get isDateTime() {
- return this.formatType === "datetime";
- }
- get formattedValue() {
- return this.formatValue(this.formatType, this.props.record.data[this.props.name]);
- }
- get formattedEndDate() {
- return this.formatValue(this.formatType, this.endDate);
- }
- get formattedStartDate() {
- return this.formatValue(this.formatType, this.startDate);
- }
- get startDate() {
- return this.props.record.data[this.props.relatedStartDateField || this.props.name];
- }
- get endDate() {
- return this.props.record.data[this.props.relatedEndDateField || this.props.name];
- }
- get relatedDateRangeField() {
- return this.props.relatedStartDateField
- ? this.props.relatedStartDateField
- : this.props.relatedEndDateField;
- }
-
- formatValue(format, value) {
- const formatter = formatters.get(format);
- let formattedValue;
- try {
- formattedValue = formatter(value);
- } catch {
- this.props.record.setInvalidField(this.props.name);
- }
- return formattedValue;
- }
-
- updateRange(start, end) {
- return this.props.record.update({
- [this.props.relatedStartDateField || this.props.name]: start,
- [this.props.relatedEndDateField || this.props.name]: end,
- });
- }
-
- onChangeInput(ev) {
- const parse = parsers.get(this.formatType);
- let value;
- try {
- value = parse(ev.target.value);
- } catch {
- this.props.record.setInvalidField(this.props.name);
- return;
- }
- this.props.record.update({ [this.props.name]: value });
- }
-
- onWindowScroll(ev) {
- const target = ev.target;
- if (
- this.isPickerShown &&
- !this.env.isSmall &&
- (target === window || !this.pickerContainer.contains(target))
- ) {
- window.$(this.root.el).data("daterangepicker").hide();
- }
- }
-
- async onPickerApply(ev, picker) {
- const start = this.isDateTime ? picker.startDate : picker.startDate.startOf("day");
- const end = this.isDateTime ? picker.endDate : picker.endDate.startOf("day");
- const dates = [start, end].map(momentToLuxon);
- await this.updateRange(dates[0], dates[1]);
- const input = document.querySelector(
- `.o_field_daterange[name='${this.relatedDateRangeField}'] input`
- );
- if (!input) {
- // Don't attempt to update the related daterange field if not present in the DOM
- return;
- }
- const target = window.$(input).data("daterangepicker");
- target.setStartDate(picker.startDate);
- target.setEndDate(picker.endDate);
- }
- onPickerShow() {
- this.isPickerShown = true;
- }
- onPickerHide() {
- this.isPickerShown = false;
- }
-}
-
-export const dateRangeField = {
- component: DateRangeField,
- supportedOptions: [
- {
- label: _lt("Format type"),
- name: "format_type",
- type: "selection",
- choices: [
- { value: "date", label: _lt("Date") },
- { value: "datetime", label: _lt("Datetime") },
- ],
- },
- {
- label: _lt("Related Start Date"),
- name: "related_start_date",
- type: "field",
- availableTypes: ["date", "datetime"],
- },
- {
- label: _lt("Related End Date"),
- name: "related_end_date",
- type: "field",
- availableTypes: ["date", "datetime"],
- },
- ],
- supportedTypes: ["date", "datetime"],
- extractProps: ({ attrs, options }) => ({
- relatedEndDateField: options.related_end_date,
- relatedStartDateField: options.related_start_date,
- formatType: options.format_type,
- placeholder: attrs.placeholder,
- }),
-};
-
-registry.category("fields").add("daterange", dateRangeField);
diff --git a/addons/web/static/src/views/fields/daterange/daterange_field.xml b/addons/web/static/src/views/fields/daterange/daterange_field.xml
deleted file mode 100644
index 0380d637b15..00000000000
--- a/addons/web/static/src/views/fields/daterange/daterange_field.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/web/static/src/views/fields/datetime/datetime_field.js b/addons/web/static/src/views/fields/datetime/datetime_field.js
index 1deac9a0a42..da33de41b58 100644
--- a/addons/web/static/src/views/fields/datetime/datetime_field.js
+++ b/addons/web/static/src/views/fields/datetime/datetime_field.js
@@ -1,64 +1,296 @@
/** @odoo-module **/
-import { DateTimePicker } from "@web/core/datepicker/datepicker";
-import { areDateEquals, formatDateTime } from "@web/core/l10n/dates";
+import { Component, onWillStart, onWillUpdateProps, useRef, useState } from "@odoo/owl";
+import { useDateTimePicker } from "@web/core/datetime/datetime_hook";
+import {
+ areDatesEqual,
+ deserializeDate,
+ deserializeDateTime,
+ formatDate,
+ formatDateTime,
+ today,
+} from "@web/core/l10n/dates";
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
+import { ensureArray } from "@web/core/utils/arrays";
+import { archParseBoolean } from "@web/views/utils";
import { standardFieldProps } from "../standard_field_props";
-import { Component } from "@odoo/owl";
+/**
+ * @typedef {luxon.DateTime} DateTime
+ *
+ * @typedef {import("../standard_field_props").StandardFieldProps & {
+ * endDateField?: string;
+ * maxDate?: string;
+ * minDate?: string;
+ * placeholder?: string;
+ * required?: boolean;
+ * rounding?: number;
+ * warnFuture?: boolean;
+ * }} DateTimeFieldProps
+ *
+ * @typedef {import("@web/core/datetime/datetime_picker").DateTimePickerProps} DateTimePickerProps
+ */
+/** @extends {Component} */
export class DateTimeField extends Component {
- static template = "web.DateTimeField";
- static components = {
- DateTimePicker,
- };
static props = {
...standardFieldProps,
- pickerOptions: { type: Object, optional: true },
+ endDateField: { type: String, optional: true },
+ maxDate: { type: String, optional: true },
+ minDate: { type: String, optional: true },
placeholder: { type: String, optional: true },
+ required: { type: Boolean, optional: true },
+ rounding: { type: Number, optional: true },
+ warnFuture: { type: Boolean, optional: true },
};
- static defaultProps = {
- pickerOptions: {},
- };
+
+ static template = "web.DateTimeField";
+
+ //-------------------------------------------------------------------------
+ // Getters
+ //-------------------------------------------------------------------------
+
+ get hasEmptyField() {
+ return this.values.length < 2 || !this.values.every(Boolean);
+ }
+
+ /** @type {string[]} */
+ get refValues() {
+ return this.inputRefs.map((ref) => ref.el?.value);
+ }
+
+ get showEndDateInput() {
+ return this.props.endDateField && this.values.filter(Boolean).length;
+ }
+
+ get values() {
+ return ensureArray(this.state.value);
+ }
+
+ //-------------------------------------------------------------------------
+ // Lifecycle
+ //-------------------------------------------------------------------------
setup() {
- /**
- * The last value that has been commited to the model.
- * Not changed in case of invalid field value.
- */
- this.lastSetValue = null;
+ const { endDateField, name } = this.props; // should not change
+
+ this.rootRef = useRef("root");
+ this.inputRefs = [useRef("start-date"), useRef("end-date")];
+
+ const state = useDateTimePicker({
+ target: "root",
+ pickerProps: (props) => this.getPickerProps(props),
+ onChange: (value) => {
+ if (Array.isArray(value)) {
+ if (value.every(Boolean)) {
+ this.emptyField = false;
+ } else {
+ this.emptyField = value[0] ? endDateField : name;
+ this.state.value = value.find(Boolean);
+ }
+ }
+ this.triggerIsDirty(this.props);
+ },
+ onApply: (value) => {
+ const toUpdate = {};
+ if (Array.isArray(value)) {
+ // Value is already a range
+ [toUpdate[name], toUpdate[endDateField]] = value;
+ } else {
+ toUpdate[this.emptyField === name ? endDateField : name] = value;
+ if (endDateField && this.emptyField) {
+ toUpdate[this.emptyField] = false;
+ }
+ }
+ // Remove values that did not change
+ for (const fieldName in toUpdate) {
+ if (areDatesEqual(toUpdate[fieldName], this.props.record.data[fieldName])) {
+ delete toUpdate[fieldName];
+ }
+ }
+ this.props.record.update(toUpdate);
+ },
+ });
+ this.state = useState(state);
+
+ onWillStart(() => this.triggerIsDirty(this.props));
+ onWillUpdateProps((nextProps) => this.triggerIsDirty(nextProps));
}
- get formattedValue() {
- return formatDateTime(this.props.record.data[this.props.name]);
+ //-------------------------------------------------------------------------
+ // Methods
+ //-------------------------------------------------------------------------
+
+ async addDate() {
+ const [value] = this.values;
+ this.state.focusedDateIndex = this.emptyField === this.props.name ? 0 : 1;
+ this.state.value = [value, value];
}
- onDateTimeChanged(date) {
- if (!areDateEquals(this.props.record.data[this.props.name] || "", date)) {
- this.props.record.update({ [this.props.name]: date });
+ formatDisplayValue(value) {
+ if (typeof value === "string") {
+ return value;
}
+ return value ? (this.type === "date" ? formatDate(value) : formatDateTime(value)) : "";
}
- onDatePickerInput(ev) {
+
+ /**
+ * @param {DateTimeFieldProps} props
+ */
+ getPickerProps(props) {
+ const { endDateField, record, name, maxDate, minDate, rounding } = props;
+ const value = this.getValueFromProps(props);
+
+ // Compute own props
+ this.type = record.fields[name].type;
+ if (endDateField) {
+ this.emptyField =
+ !Array.isArray(value) && (record.data[endDateField] ? name : endDateField);
+ }
+
+ // Compute picker props
+ /** @type {DateTimePickerProps} */
+ const pickerProps = { value, type: this.type };
+ if (maxDate) {
+ pickerProps.maxDate = maxDate === "today" ? maxDate : this.parseLimitDate(maxDate);
+ }
+ if (minDate) {
+ pickerProps.minDate = minDate === "today" ? minDate : this.parseLimitDate(minDate);
+ }
+ if (!isNaN(rounding)) {
+ pickerProps.rounding = rounding;
+ }
+ return pickerProps;
+ }
+
+ /**
+ * @param {DateTimeFieldProps} props
+ * @returns {DateTimePickerProps["value"]}
+ */
+ getValueFromProps({ endDateField, record, required, name }) {
+ const value = record.data[name];
+ if (endDateField) {
+ const endValue = record.data[endDateField];
+ if (required || (value && endValue)) {
+ return [value, endValue];
+ } else if (!value) {
+ return endValue;
+ }
+ }
+ return value;
+ }
+
+ /**
+ * @param {number} index
+ */
+ isDateInTheFuture(index) {
+ return this.values[index] > today();
+ }
+
+ /**
+ * @param {string} value
+ */
+ parseLimitDate(value) {
+ return this.type === "date" ? deserializeDate(value) : deserializeDateTime(value);
+ }
+
+ /**
+ * The given props are used to compute the current value and compare it to
+ * the state handled by the datetime hook.
+ *
+ * @param {DateTimeFieldProps} props
+ */
+ triggerIsDirty(props) {
this.props.record.model.bus.trigger(
"FIELD_IS_DIRTY",
- ev.target.value !== this.lastSetValue
+ !areDatesEqual(this.getValueFromProps(props), this.state.value)
);
}
- onUpdateInput(date) {
- this.props.record.model.bus.trigger("FIELD_IS_DIRTY", false);
- this.lastSetValue = date;
+
+ //-------------------------------------------------------------------------
+ // Handlers
+ //-------------------------------------------------------------------------
+
+ onInput() {
+ this.props.record.model.bus.trigger("FIELD_IS_DIRTY", true);
}
}
-export const dateTimeField = {
+const END_DATE_FIELD_OPTION = "end_date_field";
+
+export const dateField = {
component: DateTimeField,
- displayName: _lt("Date & Time"),
- supportedTypes: ["datetime"],
- extractProps: ({ attrs, options }) => ({
- pickerOptions: options.datepicker,
+ displayName: _lt("Date"),
+ supportedOptions: [
+ {
+ label: _lt("Earliest accepted date"),
+ name: "min_date",
+ type: "string",
+ help: _lt(`ISO-formatted date (e.g. "2018-12-31") or "today".`),
+ },
+ {
+ label: _lt("Latest accepted date"),
+ name: "max_date",
+ type: "string",
+ help: _lt(`ISO-formatted date (e.g. "2018-12-31") or "today".`),
+ },
+ {
+ label: _lt("Warning for future dates"),
+ name: "warn_future",
+ type: "boolean",
+ help: _lt(`Displays a warning icon if the input dates are in the future.`),
+ },
+ ],
+ supportedTypes: ["date"],
+ extractProps: ({ attrs, modifiers, options }) => ({
+ endDateField: options[END_DATE_FIELD_OPTION],
+ maxDate: options.max_date,
+ minDate: options.min_date,
placeholder: attrs.placeholder,
+ required: Boolean(modifiers.required),
+ rounding: parseInt(options.rounding),
+ warnFuture: archParseBoolean(options.warn_future),
}),
+ fieldDependencies: ({ type, modifiers, options }) =>
+ options[END_DATE_FIELD_OPTION] && [
+ { name: options[END_DATE_FIELD_OPTION], type, modifiers },
+ ],
};
-registry.category("fields").add("datetime", dateTimeField);
+export const dateTimeField = {
+ ...dateField,
+ displayName: _lt("Date & Time"),
+ supportedOptions: [
+ ...dateField.supportedOptions,
+ {
+ label: _lt("Rounding"),
+ name: "rounding",
+ type: "number",
+ default: 5,
+ help: _lt(`Increment used in the minutes selection dropdown.`),
+ },
+ ],
+ supportedTypes: ["datetime"],
+};
+
+export const dateRangeField = {
+ ...dateTimeField,
+ displayName: _lt("Date Range"),
+ supportedOptions: [
+ ...dateTimeField.supportedOptions,
+ {
+ label: _lt("End date field"),
+ name: "end_date_field",
+ type: "field",
+ availableTypes: ["date", "datetime"],
+ },
+ ],
+ supportedTypes: ["date", "datetime"],
+};
+
+registry
+ .category("fields")
+ .add("date", dateField)
+ .add("daterange", dateRangeField)
+ .add("datetime", dateTimeField);
diff --git a/addons/web/static/src/views/fields/datetime/datetime_field.scss b/addons/web/static/src/views/fields/datetime/datetime_field.scss
new file mode 100644
index 00000000000..502ff3af9f8
--- /dev/null
+++ b/addons/web/static/src/views/fields/datetime/datetime_field.scss
@@ -0,0 +1,14 @@
+.o_field_date,
+.o_field_datetime,
+.o_field_daterange {
+ .o_add_date {
+ visibility: hidden;
+ }
+
+ &:focus-within,
+ &:hover {
+ .o_add_date {
+ visibility: visible;
+ }
+ }
+}
diff --git a/addons/web/static/src/views/fields/datetime/datetime_field.xml b/addons/web/static/src/views/fields/datetime/datetime_field.xml
index b9f91c63953..3b6648aa45f 100644
--- a/addons/web/static/src/views/fields/datetime/datetime_field.xml
+++ b/addons/web/static/src/views/fields/datetime/datetime_field.xml
@@ -1,21 +1,63 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/addons/web/static/src/views/fields/properties/properties_field.js b/addons/web/static/src/views/fields/properties/properties_field.js
index 440d063854c..b48446fe9b1 100644
--- a/addons/web/static/src/views/fields/properties/properties_field.js
+++ b/addons/web/static/src/views/fields/properties/properties_field.js
@@ -143,7 +143,7 @@ export class PropertiesField extends Component {
* @returns {boolean}
*/
checkPopoverClose(target) {
- if (target.closest(".bootstrap-datetimepicker-widget")) {
+ if (target.closest(".o_datetime_picker")) {
// selected a datetime, do not close the definition popover
return false;
}
diff --git a/addons/web/static/src/views/fields/properties/property_value.js b/addons/web/static/src/views/fields/properties/property_value.js
index 1a5cd0446f5..05c90caf8ed 100644
--- a/addons/web/static/src/views/fields/properties/property_value.js
+++ b/addons/web/static/src/views/fields/properties/property_value.js
@@ -1,19 +1,12 @@
/** @odoo-module **/
-import { _lt } from "@web/core/l10n/translation";
+import { Component } from "@odoo/owl";
+import { AutoComplete } from "@web/core/autocomplete/autocomplete";
+import { CheckBox } from "@web/core/checkbox/checkbox";
+import { DateTimeInput } from "@web/core/datetime/datetime_input";
+import { Domain } from "@web/core/domain";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
-import { CheckBox } from "@web/core/checkbox/checkbox";
-import { DateTimePicker, DatePicker } from "@web/core/datepicker/datepicker";
-import { Domain } from "@web/core/domain";
-import { Many2XAutocomplete, useOpenMany2XRecord } from "@web/views/fields/relational_utils";
-import { useService } from "@web/core/utils/hooks";
-import { TagsList } from "@web/core/tags_list/tags_list";
-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 { parseFloat, parseInteger } from "@web/views/fields/parsers";
import {
deserializeDate,
deserializeDateTime,
@@ -22,8 +15,14 @@ import {
serializeDate,
serializeDateTime,
} from "@web/core/l10n/dates";
-
-import { Component } from "@odoo/owl";
+import { _lt } from "@web/core/l10n/translation";
+import { TagsList } from "@web/core/tags_list/tags_list";
+import { useService } from "@web/core/utils/hooks";
+import { formatFloat, formatInteger, formatMany2one } from "@web/views/fields/formatters";
+import { m2oTupleFromData } from "@web/views/fields/many2one/many2one_field";
+import { parseFloat, parseInteger } from "@web/views/fields/parsers";
+import { Many2XAutocomplete, useOpenMany2XRecord } from "@web/views/fields/relational_utils";
+import { PropertyTags } from "./property_tags";
/**
* Represent one property value.
@@ -341,8 +340,7 @@ PropertyValue.components = {
Dropdown,
DropdownItem,
CheckBox,
- DateTimePicker,
- DatePicker,
+ DateTimeInput,
Many2XAutocomplete,
TagsList,
AutoComplete,
diff --git a/addons/web/static/src/views/fields/properties/property_value.xml b/addons/web/static/src/views/fields/properties/property_value.xml
index 6c847797438..65cbc2e677b 100644
--- a/addons/web/static/src/views/fields/properties/property_value.xml
+++ b/addons/web/static/src/views/fields/properties/property_value.xml
@@ -13,18 +13,12 @@
onChange.bind="onValueChange"
/>
-
-
-
-
-
+
diff --git a/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js b/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js
index ccc9dae71a2..ba7883cb451 100644
--- a/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js
+++ b/addons/web/static/src/views/fields/remaining_days/remaining_days_field.js
@@ -1,53 +1,38 @@
/** @odoo-module **/
-import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
+import { Component } from "@odoo/owl";
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 { registry } from "@web/core/registry";
+import { DateTimeField } from "../datetime/datetime_field";
import { standardFieldProps } from "../standard_field_props";
-import { Component } from "@odoo/owl";
+const { DateTime } = luxon;
export class RemainingDaysField extends Component {
+ static components = { DateTimeField };
+
+ static props = standardFieldProps;
+
static template = "web.RemainingDaysField";
- static props = {
- ...standardFieldProps,
- };
-
- get hasTime() {
- return this.props.record.fields[this.props.name].type === "datetime";
- }
-
- get pickerComponent() {
- return this.hasTime ? DateTimePicker : DatePicker;
- }
get diffDays() {
- if (!this.props.record.data[this.props.name]) {
+ const { record, name } = this.props;
+ const value = record.data[name];
+ if (!value) {
return null;
}
- const today = luxon.DateTime.local().startOf("day");
- return Math.floor(
- this.props.record.data[this.props.name].startOf("day").diff(today, "days").days
- );
+ const today = DateTime.local().startOf("day");
+ const diff = value.startOf("day").diff(today, "days");
+ return Math.floor(diff.days);
}
get formattedValue() {
- return this.hasTime
- ? formatDateTime(this.props.record.data[this.props.name], {
- format: localization.dateFormat,
- })
- : formatDate(this.props.record.data[this.props.name]);
- }
-
- onDateTimeChanged(datetime) {
- if (datetime) {
- this.props.record.update({ [this.props.name]: datetime });
- } else if (typeof datetime === "string") {
- // when the date is cleared
- this.props.record.update({ [this.props.name]: false });
- }
+ const { record, name } = this.props;
+ return record.fields[name].type === "datetime"
+ ? formatDateTime(record.data[name], { format: localization.dateFormat })
+ : formatDate(record.data[name]);
}
}
diff --git a/addons/web/static/src/views/fields/remaining_days/remaining_days_field.xml b/addons/web/static/src/views/fields/remaining_days/remaining_days_field.xml
index cec5885218f..08af63bf7d3 100644
--- a/addons/web/static/src/views/fields/remaining_days/remaining_days_field.xml
+++ b/addons/web/static/src/views/fields/remaining_days/remaining_days_field.xml
@@ -1,6 +1,5 @@
-
@@ -24,10 +23,7 @@
-
-
-
+
-
diff --git a/addons/web/static/src/views/kanban/kanban_record_quick_create.js b/addons/web/static/src/views/kanban/kanban_record_quick_create.js
index 86600dcff23..c29da4bff9b 100644
--- a/addons/web/static/src/views/kanban/kanban_record_quick_create.js
+++ b/addons/web/static/src/views/kanban/kanban_record_quick_create.js
@@ -91,9 +91,9 @@ class KanbanQuickCreateController extends Component {
return;
}
const target = this.mousedownTarget || ev.target;
- // accounts for clicking on legacy daterangepicker and legacy autocomplete
+ // accounts for clicking on datetime picker and legacy autocomplete
const gotClickedInside =
- target.closest(".daterangepicker") ||
+ target.closest(".o_datetime_picker") ||
target.closest(".ui-autocomplete") ||
this.rootRef.el.contains(target);
if (!gotClickedInside) {
diff --git a/addons/web/static/tests/core/datepicker_tests.js b/addons/web/static/tests/core/datepicker_tests.js
deleted file mode 100644
index 4af80e8526d..00000000000
--- a/addons/web/static/tests/core/datepicker_tests.js
+++ /dev/null
@@ -1,657 +0,0 @@
-/** @odoo-module **/
-
-import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
-import { registry } from "@web/core/registry";
-import { uiService } from "@web/core/ui/ui_service";
-import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
-import { registerCleanup } from "../helpers/cleanup";
-import { makeTestEnv } from "../helpers/mock_env";
-import { makeFakeLocalizationService } from "../helpers/mock_services";
-import { click, getFixture, mount, triggerEvent } from "../helpers/utils";
-
-import { Component, useState, xml } from "@odoo/owl";
-const { DateTime } = luxon;
-
-const serviceRegistry = registry.category("services");
-
-let target;
-
-/**
- * @param {typeof DatePicker} Picker
- * @param {Object} props
- * @returns {Promise}
- */
-async function mountPicker(Picker, props) {
- serviceRegistry
- .add(
- "localization",
- makeFakeLocalizationService({
- dateFormat: "dd/MM/yyyy",
- dateTimeFormat: "dd/MM/yyyy HH:mm:ss",
- })
- )
- .add("ui", uiService)
- .add("hotkey", hotkeyService);
-
- class Parent extends Component {
- setup() {
- this.state = useState(props);
- }
-
- onDateChange(date) {
- if (props.onDateTimeChanged) {
- props.onDateTimeChanged(date);
- }
- this.state.date = date;
- }
- }
- Parent.template = xml/* xml */ `
-
- `;
-
- const env = await makeTestEnv();
- await mount(Parent, target, { env, props: { Picker } });
-}
-
-function useFRLocale() {
- if (!window.moment.locales().includes("fr")) {
- // Mocks the FR locale if not loaded
- const originalLocale = window.moment.locale();
- window.moment.defineLocale("fr", {
- months: "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split(
- "_"
- ),
- monthsShort: "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split(
- "_"
- ),
- code: "fr",
- monthsParseExact: true,
- week: { dow: 1, doy: 4 },
- });
- // Moment automatically assigns newly defined locales.
- window.moment.locale(originalLocale);
- registerCleanup(() => window.moment.updateLocale("fr", null));
- }
- return "fr";
-}
-
-var symbolMap = {
- 1: "૧",
- 2: "૨",
- 3: "૩",
- 4: "૪",
- 5: "૫",
- 6: "૬",
- 7: "૭",
- 8: "૮",
- 9: "૯",
- 0: "૦",
-};
-var numberMap = {
- "૧": "1",
- "૨": "2",
- "૩": "3",
- "૪": "4",
- "૫": "5",
- "૬": "6",
- "૭": "7",
- "૮": "8",
- "૯": "9",
- "૦": "0",
-};
-
-function useGULocale() {
- if (!window.moment.locales().includes("gu")) {
- const originalLocale = window.moment.locale();
- window.moment.defineLocale("gu", {
- months: "જાન્યુઆરી_ફેબ્રુઆરી_માર્ચ_એપ્રિલ_મે_જૂન_જુલાઈ_ઑગસ્ટ_સપ્ટેમ્બર_ઑક્ટ્બર_નવેમ્બર_ડિસેમ્બર".split(
- "_"
- ),
- monthsShort:
- "જાન્યુ._ફેબ્રુ._માર્ચ_એપ્રિ._મે_જૂન_જુલા._ઑગ._સપ્ટે._ઑક્ટ્._નવે._ડિસે.".split("_"),
- monthsParseExact: true,
- week: {
- dow: 0, // Sunday is the first day of the week.
- doy: 6, // The week that contains Jan 1st is the first week of the year.
- },
- preparse: function (string) {
- return string.replace(/[૧૨૩૪૫૬૭૮૯૦]/g, function (match) {
- return numberMap[match];
- });
- },
- postformat: function (string) {
- return string.replace(/\d/g, function (match) {
- return symbolMap[match];
- });
- },
- });
- // Moment automatically assigns newly defined locales.
- window.moment.locale(originalLocale);
- registerCleanup(() => window.moment.updateLocale("gu", null));
- }
- return "gu";
-}
-
-function useNOLocale() {
- if (!window.moment.locales().includes("nb")) {
- const originalLocale = window.moment.locale();
- window.moment.defineLocale("nb", {
- months: "januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split(
- "_"
- ),
- monthsShort: "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),
- monthsParseExact: true,
- week: {
- dow: 1, // Monday is the first day of the week.
- doy: 4, // The week that contains Jan 4th is the first week of the year.
- },
- });
- // Moment automatically assigns newly defined locales.
- window.moment.locale(originalLocale);
- registerCleanup(() => window.moment.updateLocale("nb", null));
- }
- return "nb";
-}
-
-QUnit.module("Components", ({ beforeEach }) => {
- beforeEach(() => {
- target = getFixture();
- });
-
- QUnit.module("DatePicker");
-
- QUnit.test("basic rendering", async function (assert) {
- assert.expect(8);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
- });
-
- assert.containsOnce(target, "input.o_input.o_datepicker_input");
- assert.containsOnce(target, "span.o_datepicker_button");
- assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget");
-
- const input = target.querySelector("input.o_input.o_datepicker_input");
- assert.strictEqual(input.value, "09/01/1997", "Value should be the one given");
- assert.strictEqual(
- input.dataset.target,
- `#${target.querySelector(".o_datepicker").id}`,
- "DatePicker id should match its input target"
- );
-
- await click(input);
-
- assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .datepicker");
- assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget .timepicker");
- assert.strictEqual(
- document.querySelector(".datepicker .day.active").dataset.day,
- "01/09/1997",
- "Datepicker should have set the correct day"
- );
- });
-
- QUnit.test("pick a date", async function (assert) {
- assert.expect(5);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy"),
- "08/02/1997",
- "Event should transmit the correct date"
- );
- },
- });
- const input = target.querySelector(".o_datepicker_input");
-
- await click(input);
- await click(document.querySelector(".datepicker th.next")); // next month
-
- assert.verifySteps([]);
-
- await click(document.querySelectorAll(".datepicker table td")[15]); // previous day
-
- assert.strictEqual(input.value, "08/02/1997");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("pick a date with locale (locale given in props)", async function (assert) {
- assert.expect(5);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
- format: "dd MMM, yyyy",
- locale: useFRLocale(),
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy"),
- "01/09/1997",
- "Event should transmit the correct date"
- );
- },
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.strictEqual(input.value, "09 janv., 1997");
-
- await click(input);
- await click(document.querySelector(".datepicker .picker-switch")); // month picker
- await click(document.querySelectorAll(".datepicker .month")[8]); // september
- await click(document.querySelector(".datepicker .day")); // first day
-
- assert.strictEqual(input.value, "01 sept., 1997");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("pick a date with locale (locale from date props)", async function (assert) {
- assert.expect(5);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", {
- zone: "utc",
- locale: useFRLocale(),
- }),
- format: "dd MMM, yyyy",
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy"),
- "01/09/1997",
- "Event should transmit the correct date"
- );
- },
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.strictEqual(input.value, "09 janv., 1997");
-
- await click(input);
- await click(document.querySelector(".datepicker .picker-switch")); // month picker
- await click(document.querySelectorAll(".datepicker .month")[8]); // september
- await click(document.querySelector(".datepicker .day")); // first day
-
- assert.strictEqual(input.value, "01 sept., 1997");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("pick a date with locale (locale with different symbols)", async function (assert) {
- assert.expect(6);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", {
- zone: "utc",
- locale: useGULocale(),
- }),
- format: "dd MMM, yyyy",
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy"),
- "01/09/1997",
- "Event should transmit the correct date"
- );
- },
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.strictEqual(input.value, "09 જાન્યુ, 1997");
-
- await click(input);
- assert.strictEqual(input.value, "૧૯૯૭/૦૧/૦૯");
- await click(document.querySelector(".datepicker .picker-switch")); // month picker
- await click(document.querySelectorAll(".datepicker .month")[8]); // september
- await click(document.querySelectorAll(".datepicker .day")[1]); // first day of september
-
- assert.strictEqual(input.value, "01 સપ્ટે, 1997");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("enter a date value", async function (assert) {
- assert.expect(5);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy"),
- "08/02/1997",
- "Event should transmit the correct date"
- );
- },
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.verifySteps([]);
-
- input.value = "08/02/1997";
- await triggerEvent(target, ".o_datepicker_input", "change");
-
- assert.verifySteps(["datetime-changed"]);
-
- await click(input);
-
- assert.strictEqual(
- document.querySelector(".datepicker .day.active").dataset.day,
- "02/08/1997",
- "Datepicker should have set the correct day"
- );
- });
-
- QUnit.test("Date format is correctly set", async function (assert) {
- assert.expect(2);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
- format: "yyyy/MM/dd",
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.strictEqual(input.value, "1997/01/09");
-
- // Forces an update to assert that the registered format is the correct one
- await click(input);
-
- assert.strictEqual(input.value, "1997/01/09");
- });
-
- QUnit.module("DateTimePicker");
-
- QUnit.test("basic rendering", async function (assert) {
- assert.expect(11);
-
- await mountPicker(DateTimePicker, {
- date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
- });
-
- assert.containsOnce(target, "input.o_input.o_datepicker_input");
- assert.containsOnce(target, "span.o_datepicker_button");
- assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget");
-
- const input = target.querySelector("input.o_input.o_datepicker_input");
- assert.strictEqual(input.value, "09/01/1997 12:30:01", "Value should be the one given");
- assert.strictEqual(
- input.dataset.target,
- `#${target.querySelector(".o_datepicker").id}`,
- "DateTimePicker id should match its input target"
- );
-
- await click(input);
-
- assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .datepicker");
- assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .timepicker");
- assert.strictEqual(
- document.querySelector(".datepicker .day.active").dataset.day,
- "01/09/1997",
- "Datepicker should have set the correct day"
- );
-
- assert.strictEqual(
- document.querySelector(".timepicker .timepicker-hour").innerText.trim(),
- "12",
- "Datepicker should have set the correct hour"
- );
- assert.strictEqual(
- document.querySelector(".timepicker .timepicker-minute").innerText.trim(),
- "30",
- "Datepicker should have set the correct minute"
- );
- assert.strictEqual(
- document.querySelector(".timepicker .timepicker-second").innerText.trim(),
- "01",
- "Datepicker should have set the correct second"
- );
- });
-
- QUnit.test("pick a date and time", async function (assert) {
- assert.expect(5);
-
- await mountPicker(DateTimePicker, {
- date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy HH:mm:ss"),
- "08/02/1997 15:45:05",
- "Event should transmit the correct date"
- );
- },
- });
- const input = target.querySelector("input.o_input.o_datepicker_input");
-
- await click(input);
- await click(document.querySelector(".datepicker th.next")); // February
- await click(document.querySelectorAll(".datepicker table td")[15]); // 08
- await click(document.querySelector('a[title="Select Time"]'));
- await click(document.querySelector(".timepicker .timepicker-hour"));
- await click(document.querySelectorAll(".timepicker .hour")[15]); // 15h
- await click(document.querySelector(".timepicker .timepicker-minute"));
- await click(document.querySelectorAll(".timepicker .minute")[9]); // 45m
- await click(document.querySelector(".timepicker .timepicker-second"));
-
- assert.verifySteps([]);
-
- await click(document.querySelectorAll(".timepicker .second")[1]); // 05s
-
- assert.strictEqual(input.value, "08/02/1997 15:45:05");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("pick a date and time with locale", async function (assert) {
- assert.expect(6);
-
- await mountPicker(DateTimePicker, {
- date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
- format: "dd MMM, yyyy HH:mm:ss",
- locale: useFRLocale(),
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy HH:mm:ss"),
- "01/09/1997 15:45:05",
- "Event should transmit the correct date"
- );
- },
- });
-
- const input = target.querySelector("input.o_input.o_datepicker_input");
-
- assert.strictEqual(input.value, "09 janv., 1997 12:30:01");
-
- await click(input);
-
- await click(document.querySelector(".datepicker .picker-switch")); // month picker
- await click(document.querySelectorAll(".datepicker .month")[8]); // september
- await click(document.querySelector(".datepicker .day")); // first day
-
- await click(document.querySelector('a[title="Select Time"]'));
- await click(document.querySelector(".timepicker .timepicker-hour"));
- await click(document.querySelectorAll(".timepicker .hour")[15]); // 15h
- await click(document.querySelector(".timepicker .timepicker-minute"));
- await click(document.querySelectorAll(".timepicker .minute")[9]); // 45m
- await click(document.querySelector(".timepicker .timepicker-second"));
-
- assert.verifySteps([]);
-
- await click(document.querySelectorAll(".timepicker .second")[1]); // 05s
-
- assert.strictEqual(input.value, "01 sept., 1997 15:45:05");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("pick a time with 12 hour format locale", async function (assert) {
- assert.expect(6);
-
- await mountPicker(DateTimePicker, {
- date: DateTime.fromFormat("09/01/1997 08:30:01", "dd/MM/yyyy hh:mm:ss"),
- format: "dd/MM/yyyy hh:mm:ss",
- locale: useFRLocale(),
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy HH:mm:ss"),
- "09/01/1997 20:30:02",
- "The new time should be in the afternoon"
- );
- },
- });
-
- const input = target.querySelector("input.o_input.o_datepicker_input");
-
- assert.strictEqual(input.value, "09/01/1997 08:30:01");
-
- await click(input);
-
- await click(document.querySelector('a[title="Select Time"]'));
- await click(document.querySelector('a[title="Increment Second"]'));
- await click(document.querySelector('button[title="Toggle Period"]'));
-
- assert.verifySteps([]);
-
- await click(document.querySelector('a[title="Close the picker"]'));
-
- assert.strictEqual(input.value, "09/01/1997 08:30:02");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("enter a datetime value", async function (assert) {
- assert.expect(9);
-
- await mountPicker(DateTimePicker, {
- date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
- onDateTimeChanged: (date) => {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy HH:mm:ss"),
- "08/02/1997 15:45:05",
- "Event should transmit the correct date"
- );
- },
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.verifySteps([]);
-
- input.value = "08/02/1997 15:45:05";
- await triggerEvent(target, ".o_datepicker_input", "change");
-
- assert.verifySteps(["datetime-changed"]);
-
- await click(input);
-
- assert.strictEqual(input.value, "08/02/1997 15:45:05");
- assert.strictEqual(
- document.querySelector(".datepicker .day.active").dataset.day,
- "02/08/1997",
- "Datepicker should have set the correct day"
- );
- assert.strictEqual(
- document.querySelector(".timepicker .timepicker-hour").innerText.trim(),
- "15",
- "Datepicker should have set the correct hour"
- );
- assert.strictEqual(
- document.querySelector(".timepicker .timepicker-minute").innerText.trim(),
- "45",
- "Datepicker should have set the correct minute"
- );
- assert.strictEqual(
- document.querySelector(".timepicker .timepicker-second").innerText.trim(),
- "05",
- "Datepicker should have set the correct second"
- );
- });
-
- QUnit.test("Date time format is correctly set", async function (assert) {
- assert.expect(2);
-
- await mountPicker(DateTimePicker, {
- date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
- format: "HH:mm:ss yyyy/MM/dd",
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.strictEqual(input.value, "12:30:01 1997/01/09");
-
- // Forces an update to assert that the registered format is the correct one
- await click(input);
-
- assert.strictEqual(input.value, "12:30:01 1997/01/09");
- });
-
- QUnit.test("Datepicker works with norwegian locale", async (assert) => {
- assert.expect(6);
-
- await mountPicker(DatePicker, {
- date: DateTime.fromFormat("09/04/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
- format: "dd MMM, yyyy",
- locale: useNOLocale(),
- onDateTimeChanged(date) {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy"),
- "01/04/1997",
- "Event should transmit the correct date"
- );
- },
- });
-
- const input = target.querySelector(".o_datepicker_input");
-
- assert.strictEqual(input.value, "09 apr., 1997");
-
- await click(input);
-
- assert.strictEqual(input.value, "1997/04/09");
-
- const days = [...document.querySelectorAll(".datepicker .day")];
- await click(days.find((d) => d.innerText.trim() === "1")); // first day of april
-
- assert.strictEqual(input.value, "01 apr., 1997");
- assert.verifySteps(["datetime-changed"]);
- });
-
- QUnit.test("Datepicker works with dots and commas in format", async (assert) => {
- assert.expect(2);
-
- await mountPicker(DateTimePicker, {
- date: DateTime.fromFormat("10/03/2023 13:14:27", "dd/MM/yyyy HH:mm:ss"),
- format: "dd.MM,yyyy",
- });
- const input = target.querySelector(".o_datepicker_input");
-
- assert.strictEqual(input.value, "10.03,2023");
-
- await click(input);
-
- assert.strictEqual(input.value, "10.03,2023");
- });
-
- QUnit.test("start with no value", async function (assert) {
- assert.expect(6);
-
- await mountPicker(DateTimePicker, {
- onDateTimeChanged(date) {
- assert.step("datetime-changed");
- assert.strictEqual(
- date.toFormat("dd/MM/yyyy HH:mm:ss"),
- "08/02/1997 15:45:05",
- "Event should transmit the correct date"
- );
- },
- });
-
- const input = target.querySelector(".o_datepicker_input");
- assert.strictEqual(input.value, "");
-
- assert.verifySteps([]);
- input.value = "08/02/1997 15:45:05";
- await triggerEvent(target, ".o_datepicker_input", "change");
-
- assert.verifySteps(["datetime-changed"]);
- assert.strictEqual(input.value, "08/02/1997 15:45:05");
- });
-});
diff --git a/addons/web/static/tests/core/datetime/datetime_input_tests.js b/addons/web/static/tests/core/datetime/datetime_input_tests.js
new file mode 100644
index 00000000000..72c045cef5e
--- /dev/null
+++ b/addons/web/static/tests/core/datetime/datetime_input_tests.js
@@ -0,0 +1,484 @@
+/** @odoo-module **/
+
+import { Component, xml } from "@odoo/owl";
+import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env";
+import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
+import {
+ click,
+ editInput,
+ editSelect,
+ getFixture,
+ mount,
+ patchWithCleanup,
+ triggerEvent,
+} from "@web/../tests/helpers/utils";
+import { DateTimeInput } from "@web/core/datetime/datetime_input";
+import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
+import { localization } from "@web/core/l10n/localization";
+import { popoverService } from "@web/core/popover/popover_service";
+import { registry } from "@web/core/registry";
+import { uiService } from "@web/core/ui/ui_service";
+import {
+ assertDateTimePicker,
+ getPickerCell,
+ getTimePickers,
+ zoomOut,
+} from "./datetime_test_helpers";
+
+const { DateTime } = luxon;
+
+/**
+ * @typedef {import("@web/core/datetime/datetime_input").DateTimeInputProps} DateTimeInputProps
+ */
+
+/**
+ * @param {DateTimeInputProps} props
+ */
+const mountInput = async (props) => {
+ const env = await makeTestEnv();
+ await mount(Root, getFixture(), { env, props });
+ return fixture.querySelector(".o_datetime_input");
+};
+
+class Root extends Component {
+ static components = { DateTimeInput };
+
+ static template = xml`
+
+
+
+
+ `;
+
+ setup() {
+ this.mainComponentEntries = mainComponentRegistry.getEntries();
+ }
+}
+
+const mainComponentRegistry = registry.category("main_components");
+const serviceRegistry = registry.category("services");
+
+let fixture;
+
+QUnit.module("Components", ({ beforeEach }) => {
+ beforeEach(() => {
+ clearRegistryWithCleanup(mainComponentRegistry);
+
+ serviceRegistry
+ .add("hotkey", hotkeyService)
+ .add(
+ "localization",
+ makeFakeLocalizationService({
+ dateFormat: "dd/MM/yyyy",
+ dateTimeFormat: "dd/MM/yyyy HH:mm:ss",
+ })
+ )
+ .add("popover", popoverService)
+ .add("ui", uiService);
+
+ fixture = getFixture();
+ });
+
+ QUnit.module("DateTimeInput (date)");
+
+ QUnit.test("basic rendering", async function (assert) {
+ await mountInput({
+ value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
+ type: "date",
+ });
+
+ assert.containsOnce(fixture, ".o_datetime_input");
+ assertDateTimePicker(false);
+
+ const input = fixture.querySelector(".o_datetime_input");
+ assert.strictEqual(input.value, "09/01/1997", "Value should be the one given");
+
+ await click(input);
+
+ assertDateTimePicker({
+ title: "January 1997",
+ date: [
+ {
+ cells: [
+ [-29, -30, -31, 1, 2, 3, 4],
+ [5, 6, 7, 8, [9], 10, 11],
+ [12, 13, 14, 15, 16, 17, 18],
+ [19, 20, 21, 22, 23, 24, 25],
+ [26, 27, 28, 29, 30, 31, -1],
+ [-2, -3, -4, -5, -6, -7, -8],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [1, 2, 3, 4, 5, 6],
+ },
+ ],
+ });
+ });
+
+ QUnit.test("pick a date", async function (assert) {
+ assert.expect(5);
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
+ type: "date",
+ onChange: (date) => {
+ assert.step("datetime-changed");
+ assert.strictEqual(
+ date.toFormat("dd/MM/yyyy"),
+ "08/02/1997",
+ "Event should transmit the correct date"
+ );
+ },
+ });
+
+ await click(input);
+ await click(getFixture(), ".o_datetime_picker .o_next"); // next month
+
+ assert.verifySteps([]);
+
+ await click(getPickerCell("8").at(0));
+
+ assert.strictEqual(input.value, "08/02/1997");
+ assert.verifySteps(["datetime-changed"]);
+ });
+
+ QUnit.test("pick a date with FR locale", async function (assert) {
+ assert.expect(5);
+
+ patchWithCleanup(luxon.Settings, { defaultLocale: "fr-FR" });
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
+ type: "date",
+ format: "dd MMM, yyyy",
+ onChange: (date) => {
+ assert.step("datetime-changed");
+ assert.strictEqual(
+ date.toFormat("dd/MM/yyyy"),
+ "01/09/1997",
+ "Event should transmit the correct date"
+ );
+ },
+ });
+
+ assert.strictEqual(input.value, "09 janv., 1997");
+
+ await click(input);
+ await zoomOut();
+ await click(getPickerCell("sept."));
+ await click(getPickerCell("1").at(0));
+
+ assert.strictEqual(input.value, "01 sept., 1997");
+ assert.verifySteps(["datetime-changed"]);
+ });
+
+ QUnit.test("pick a date with locale (locale with different symbols)", async function (assert) {
+ assert.expect(6);
+
+ patchWithCleanup(luxon.Settings, { defaultLocale: "gu" });
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
+ type: "date",
+ format: "dd MMM, yyyy",
+ onChange: (date) => {
+ assert.step("datetime-changed");
+ assert.strictEqual(
+ date.toFormat("dd/MM/yyyy"),
+ "01/09/1997",
+ "Event should transmit the correct date"
+ );
+ },
+ });
+
+ assert.strictEqual(input.value, "09 જાન્યુ, 1997");
+
+ await click(input);
+
+ assert.strictEqual(input.value, "09 જાન્યુ, 1997");
+
+ await zoomOut();
+ await click(getPickerCell("સપ્ટે"));
+ await click(getPickerCell("1").at(0));
+
+ assert.strictEqual(input.value, "01 સપ્ટે, 1997");
+ assert.verifySteps(["datetime-changed"]);
+ });
+
+ QUnit.test("enter a date value", async function (assert) {
+ assert.expect(5);
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
+ type: "date",
+ onChange: (date) => {
+ assert.step("datetime-changed");
+ assert.strictEqual(
+ date.toFormat("dd/MM/yyyy"),
+ "08/02/1997",
+ "Event should transmit the correct date"
+ );
+ },
+ });
+
+ assert.verifySteps([]);
+
+ await editInput(input, null, "08/02/1997");
+
+ assert.verifySteps(["datetime-changed"]);
+
+ await click(input);
+
+ assert.hasClass(getPickerCell("8").at(0), "o_active");
+ });
+
+ QUnit.test("Date format is correctly set", async function (assert) {
+ assert.expect(2);
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
+ type: "date",
+ format: "yyyy/MM/dd",
+ });
+
+ assert.strictEqual(input.value, "1997/01/09");
+
+ // Forces an update to assert that the registered format is the correct one
+ await click(input);
+
+ assert.strictEqual(input.value, "1997/01/09");
+ });
+
+ QUnit.module("DateTimeInput (datetime)");
+
+ QUnit.test("basic rendering", async function (assert) {
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ });
+
+ assert.containsOnce(fixture, ".o_datetime_input");
+ assertDateTimePicker(false);
+
+ assert.strictEqual(input.value, "09/01/1997 12:30:01", "Value should be the one given");
+
+ await click(input);
+
+ assertDateTimePicker({
+ title: "January 1997",
+ date: [
+ {
+ cells: [
+ [-29, -30, -31, 1, 2, 3, 4],
+ [5, 6, 7, 8, [9], 10, 11],
+ [12, 13, 14, 15, 16, 17, 18],
+ [19, 20, 21, 22, 23, 24, 25],
+ [26, 27, 28, 29, 30, 31, -1],
+ [-2, -3, -4, -5, -6, -7, -8],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [1, 2, 3, 4, 5, 6],
+ },
+ ],
+ time: [[12, 30]],
+ });
+ });
+
+ QUnit.test("pick a date and time", async function (assert) {
+ assert.expect(6);
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ onChange: (date) => assert.step(date.toSQL().split(".")[0]),
+ });
+
+ assert.strictEqual(input.value, "09/01/1997 12:30:01");
+
+ await click(input);
+
+ // Select February 8th
+ await click(getFixture(), ".o_datetime_picker .o_next");
+ await click(getPickerCell("8").at(0));
+
+ // Select 15:45
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "15");
+ await editSelect(minuteSelect, null, "45");
+
+ assert.strictEqual(input.value, "08/02/1997 15:45:01");
+ assert.verifySteps(["1997-02-08 12:30:01", "1997-02-08 15:30:01", "1997-02-08 15:45:01"]);
+ });
+
+ QUnit.test("pick a date and time with locale", async function (assert) {
+ assert.expect(6);
+
+ patchWithCleanup(luxon.Settings, { defaultLocale: "fr-FR" });
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ format: "dd MMM, yyyy HH:mm:ss",
+ onChange: (date) => assert.step(date.toSQL().split(".")[0]),
+ });
+
+ assert.strictEqual(input.value, "09 janv., 1997 12:30:01");
+
+ await click(input);
+
+ // Select September 1st
+ await zoomOut();
+ await click(getPickerCell("sept."));
+ await click(getPickerCell("1").at(0));
+
+ // Select 15:45
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "15");
+ await editSelect(minuteSelect, null, "45");
+
+ assert.strictEqual(input.value, "01 sept., 1997 15:45:01");
+ assert.verifySteps(["1997-09-01 12:30:01", "1997-09-01 15:30:01", "1997-09-01 15:45:01"]);
+ });
+
+ QUnit.test("pick a time with 12 hour format without meridiem", async function (assert) {
+ assert.expect(3);
+
+ patchWithCleanup(localization, {
+ dateFormat: "dd/MM/yyyy",
+ dateTimeFormat: "dd/MM/yyyy hh:mm:ss",
+ timeFormat: "hh:mm:ss",
+ });
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997 08:30:01", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ onChange: (date) => assert.step(date.toSQL().split(".")[0]),
+ });
+
+ assert.strictEqual(input.value, "09/01/1997 08:30:01");
+
+ await click(input);
+
+ const [, minuteSelect] = getTimePickers().at(0);
+ await editSelect(minuteSelect, null, "15");
+
+ assert.verifySteps(["1997-01-09 08:15:01"]);
+ });
+
+ QUnit.test("enter a datetime value", async function (assert) {
+ assert.expect(7);
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ onChange: (date) => {
+ assert.step("datetime-changed");
+ assert.strictEqual(
+ date.toFormat("dd/MM/yyyy HH:mm:ss"),
+ "08/02/1997 15:45:05",
+ "Event should transmit the correct date"
+ );
+ },
+ });
+
+ assert.verifySteps([]);
+
+ input.value = "08/02/1997 15:45:05";
+ await triggerEvent(fixture, ".o_datetime_input", "change");
+
+ assert.verifySteps(["datetime-changed"]);
+
+ await click(input);
+
+ assert.strictEqual(input.value, "08/02/1997 15:45:05");
+ assert.hasClass(getPickerCell("8").at(0), "o_active");
+ assert.deepEqual(getTimePickers({ parse: true }).at(0), [15, 45]);
+ });
+
+ QUnit.test("Date time format is correctly set", async function (assert) {
+ assert.expect(2);
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ format: "HH:mm:ss yyyy/MM/dd",
+ });
+
+ assert.strictEqual(input.value, "12:30:01 1997/01/09");
+
+ // Forces an update to assert that the registered format is the correct one
+ await click(input);
+
+ assert.strictEqual(input.value, "12:30:01 1997/01/09");
+ });
+
+ QUnit.test("Datepicker works with norwegian locale", async (assert) => {
+ assert.expect(6);
+
+ patchWithCleanup(luxon.Settings, { defaultLocale: "nb-NO" });
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("09/04/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ format: "dd MMM, yyyy",
+ onChange(date) {
+ assert.step("datetime-changed");
+ assert.strictEqual(
+ date.toFormat("dd/MM/yyyy"),
+ "01/04/1997",
+ "Event should transmit the correct date"
+ );
+ },
+ });
+
+ assert.strictEqual(input.value, "09 apr., 1997");
+
+ await click(input);
+
+ assert.strictEqual(input.value, "09 apr., 1997");
+
+ await click(getPickerCell("1").at(0));
+
+ assert.strictEqual(input.value, "01 apr., 1997");
+ assert.verifySteps(["datetime-changed"]);
+ });
+
+ QUnit.test("Datepicker works with dots and commas in format", async (assert) => {
+ assert.expect(2);
+
+ const input = await mountInput({
+ value: DateTime.fromFormat("10/03/2023 13:14:27", "dd/MM/yyyy HH:mm:ss"),
+ type: "datetime",
+ format: "dd.MM,yyyy",
+ });
+
+ assert.strictEqual(input.value, "10.03,2023");
+
+ await click(input);
+
+ assert.strictEqual(input.value, "10.03,2023");
+ });
+
+ QUnit.test("start with no value", async function (assert) {
+ assert.expect(6);
+
+ const input = await mountInput({
+ type: "datetime",
+ onChange(date) {
+ assert.step("datetime-changed");
+ assert.strictEqual(
+ date.toFormat("dd/MM/yyyy HH:mm:ss"),
+ "08/02/1997 15:45:05",
+ "Event should transmit the correct date"
+ );
+ },
+ });
+
+ assert.strictEqual(input.value, "");
+ assert.verifySteps([]);
+
+ await editInput(input, null, "08/02/1997 15:45:05");
+
+ assert.verifySteps(["datetime-changed"]);
+ assert.strictEqual(input.value, "08/02/1997 15:45:05");
+ });
+});
diff --git a/addons/web/static/tests/core/datetime/datetime_picker_tests.js b/addons/web/static/tests/core/datetime/datetime_picker_tests.js
new file mode 100644
index 00000000000..cfa2d6db04a
--- /dev/null
+++ b/addons/web/static/tests/core/datetime/datetime_picker_tests.js
@@ -0,0 +1,715 @@
+/** @odoo-module **/
+
+import { makeTestEnv } from "@web/../tests/helpers/mock_env";
+import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
+import { click, editSelect, getFixture, mount, patchDate } from "@web/../tests/helpers/utils";
+import { DateTimePicker } from "@web/core/datetime/datetime_picker";
+import { registry } from "@web/core/registry";
+import { ensureArray } from "@web/core/utils/arrays";
+import {
+ assertDateTimePicker,
+ getPickerCell,
+ getTexts,
+ getTimePickers,
+ useTwelveHourClockFormat,
+} from "./datetime_test_helpers";
+
+const { DateTime } = luxon;
+
+/**
+ * @typedef {import("@web/core/datetime/datetime_picker").DateTimePickerProps} DateTimePickerProps
+ */
+
+/**
+ * @param {DateTimePickerProps["value"]} value
+ */
+const formatForStep = (value) =>
+ ensureArray(value)
+ .map((val) => val.toISO().split(".")[0])
+ .join(",");
+
+/**
+ * @param {DateTimePickerProps} [props]
+ */
+const mountPicker = (props) => mount(DateTimePicker, getFixture(), { env, props });
+
+/**
+ * @param {any} value
+ */
+const pad2 = (value) => String(value).padStart(2, "0");
+
+/**
+ * @template {any} [T=number]
+ * @param {number} length
+ * @param {(index: number) => T} mapping
+ */
+const range = (length, mapping) => [...Array(length)].map((_, i) => mapping(i));
+
+let env;
+let isSmall;
+const serviceRegistry = registry.category("services");
+
+QUnit.module("Components", ({ beforeEach }) => {
+ beforeEach(async () => {
+ patchDate(2023, 3, 25, 13, 45, 1);
+
+ serviceRegistry.add(
+ "localization",
+ makeFakeLocalizationService({
+ dateFormat: "dd/MM/yyyy",
+ timeFormat: "HH:mm:ss",
+ })
+ );
+
+ isSmall = false;
+ env = await makeTestEnv();
+ Object.defineProperty(env, "isSmall", { get: () => isSmall });
+ });
+
+ QUnit.module("DateTimePicker");
+
+ //-------------------------------------------------------------------------
+ // Layout
+ //-------------------------------------------------------------------------
+
+ QUnit.test("default params", async (assert) => {
+ await mountPicker();
+
+ assertDateTimePicker({
+ title: "April 2023",
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [13, 14, 15, 16, 17, 18],
+ },
+ ],
+ time: [[13, 0]],
+ });
+
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ assert.deepEqual(getTexts(hourSelect, "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(minuteSelect, "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ });
+
+ QUnit.test("twelve-hour clock", async (assert) => {
+ useTwelveHourClockFormat();
+
+ await mountPicker();
+
+ assertDateTimePicker({
+ title: "April 2023",
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [13, 14, 15, 16, 17, 18],
+ },
+ ],
+ time: [[1, 0, "PM"]],
+ });
+
+ const [hourSelect, minuteSelect, meridiemSelect] = getTimePickers().at(0);
+ assert.deepEqual(getTexts(hourSelect, "option"), ["12", ...range(12, String).slice(1)]);
+ assert.deepEqual(
+ getTexts(minuteSelect, "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ assert.deepEqual(getTexts(meridiemSelect, "option"), ["AM", "PM"]);
+ });
+
+ QUnit.test("hide time picker", async (assert) => {
+ await mountPicker({
+ type: "date",
+ });
+
+ assertDateTimePicker({
+ title: "April 2023",
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [13, 14, 15, 16, 17, 18],
+ },
+ ],
+ });
+ });
+
+ QUnit.test("focus is adjusted to selected date", async (assert) => {
+ await mountPicker({
+ value: DateTime.fromObject({ month: 5, day: 29, hour: 23, minute: 55 }),
+ });
+
+ assertDateTimePicker({
+ title: "May 2023",
+ date: [
+ {
+ cells: [
+ [-30, 1, 2, 3, 4, 5, 6],
+ [7, 8, 9, 10, 11, 12, 13],
+ [14, 15, 16, 17, 18, 19, 20],
+ [21, 22, 23, 24, 25, 26, 27],
+ [28, [29], 30, 31, -1, -2, -3],
+ [-4, -5, -6, -7, -8, -9, -10],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [18, 19, 20, 21, 22, 23],
+ },
+ ],
+ time: [[23, 55]],
+ });
+ });
+
+ QUnit.test("next month and previous month", async (assert) => {
+ await mountPicker({
+ type: "date",
+ });
+
+ assertDateTimePicker({
+ title: "April 2023",
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [13, 14, 15, 16, 17, 18],
+ },
+ ],
+ });
+
+ await click(getFixture(), ".o_previous");
+
+ assertDateTimePicker({
+ title: "March 2023",
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, 1, 2, 3, 4],
+ [5, 6, 7, 8, 9, 10, 11],
+ [12, 13, 14, 15, 16, 17, 18],
+ [19, 20, 21, 22, 23, 24, 25],
+ [26, 27, 28, 29, 30, 31, -1],
+ [-2, -3, -4, -5, -6, -7, -8],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [9, 10, 11, 12, 13, 14],
+ },
+ ],
+ });
+
+ await click(getFixture(), ".o_next");
+
+ assertDateTimePicker({
+ title: "April 2023",
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [13, 14, 15, 16, 17, 18],
+ },
+ ],
+ });
+
+ await click(getFixture(), ".o_next");
+
+ assertDateTimePicker({
+ title: "May 2023",
+ date: [
+ {
+ cells: [
+ [-30, 1, 2, 3, 4, 5, 6],
+ [7, 8, 9, 10, 11, 12, 13],
+ [14, 15, 16, 17, 18, 19, 20],
+ [21, 22, 23, 24, 25, 26, 27],
+ [28, 29, 30, 31, -1, -2, -3],
+ [-4, -5, -6, -7, -8, -9, -10],
+ ],
+ daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ weekNumbers: [18, 19, 20, 21, 22, 23],
+ },
+ ],
+ });
+ });
+
+ QUnit.test("additional month, hide time picker", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ hour: 9, minute: 36 }),
+ DateTime.fromObject({ hour: 21, minute: 5 }),
+ ],
+ type: "date",
+ });
+
+ assertDateTimePicker({
+ title: ["April 2023", "May 2023"],
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, ["25"], 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ {
+ cells: [
+ [-30, 1, 2, 3, 4, 5, 6],
+ [7, 8, 9, 10, 11, 12, 13],
+ [14, 15, 16, 17, 18, 19, 20],
+ [21, 22, 23, 24, 25, 26, 27],
+ [28, 29, 30, 31, -1, -2, -3],
+ [-4, -5, -6, -7, -8, -9, -10],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ ],
+ });
+ });
+
+ QUnit.test("additional month, empty range value", async (assert) => {
+ await mountPicker({
+ value: [null, null],
+ });
+
+ assertDateTimePicker({
+ title: ["April 2023", "May 2023"],
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ {
+ cells: [
+ [-30, 1, 2, 3, 4, 5, 6],
+ [7, 8, 9, 10, 11, 12, 13],
+ [14, 15, 16, 17, 18, 19, 20],
+ [21, 22, 23, 24, 25, 26, 27],
+ [28, 29, 30, 31, -1, -2, -3],
+ [-4, -5, -6, -7, -8, -9, -10],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ ],
+ time: [
+ [13, 0],
+ [13, 0],
+ ],
+ });
+
+ const [firstTimePicker, secondTimePicker] = getTimePickers();
+ assert.deepEqual(getTexts(firstTimePicker[0], "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(firstTimePicker[1], "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ assert.deepEqual(getTexts(secondTimePicker[0], "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(secondTimePicker[1], "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ });
+
+ QUnit.test("range value", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ day: 5, hour: 17, minute: 18 }),
+ DateTime.fromObject({ month: 5, day: 18, hour: 5, minute: 25 }),
+ ],
+ });
+
+ assertDateTimePicker({
+ title: ["April 2023", "May 2023"],
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, [5], [6], [7], [8]],
+ [[9], [10], [11], [12], [13], [14], [15]],
+ [[16], [17], [18], [19], [20], [21], [22]],
+ [[23], [24], ["25"], [26], [27], [28], [29]],
+ [[30], -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ {
+ cells: [
+ [-30, [1], [2], [3], [4], [5], [6]],
+ [[7], [8], [9], [10], [11], [12], [13]],
+ [[14], [15], [16], [17], [18], 19, 20],
+ [21, 22, 23, 24, 25, 26, 27],
+ [28, 29, 30, 31, -1, -2, -3],
+ [-4, -5, -6, -7, -8, -9, -10],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ ],
+ time: [
+ [17, 0],
+ [5, 25],
+ ],
+ });
+
+ const [firstTimePicker, secondTimePicker] = getTimePickers();
+ assert.deepEqual(getTexts(firstTimePicker[0], "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(firstTimePicker[1], "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ assert.deepEqual(getTexts(secondTimePicker[0], "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(secondTimePicker[1], "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ });
+
+ QUnit.test("range value on small device", async (assert) => {
+ isSmall = true;
+
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ hour: 9, minute: 36 }),
+ DateTime.fromObject({ hour: 21, minute: 5 }),
+ ],
+ });
+
+ assertDateTimePicker({
+ title: "April 2023",
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, ["25"], 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ ],
+ time: [
+ [9, 0],
+ [21, 5],
+ ],
+ });
+
+ const [firstTimePicker, secondTimePicker] = getTimePickers();
+ assert.deepEqual(getTexts(firstTimePicker[0], "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(firstTimePicker[1], "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ assert.deepEqual(getTexts(secondTimePicker[0], "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(secondTimePicker[1], "option"),
+ range(12, (i) => pad2(i * 5))
+ );
+ });
+
+ QUnit.test("range value, previous month", async (assert) => {
+ await mountPicker({
+ value: [false, false],
+ });
+
+ assertDateTimePicker({
+ title: ["April 2023", "May 2023"],
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ {
+ cells: [
+ [-30, 1, 2, 3, 4, 5, 6],
+ [7, 8, 9, 10, 11, 12, 13],
+ [14, 15, 16, 17, 18, 19, 20],
+ [21, 22, 23, 24, 25, 26, 27],
+ [28, 29, 30, 31, -1, -2, -3],
+ [-4, -5, -6, -7, -8, -9, -10],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ ],
+ time: [
+ [13, 0],
+ [13, 0],
+ ],
+ });
+
+ await click(getFixture(), ".o_previous");
+
+ assertDateTimePicker({
+ title: ["March 2023", "April 2023"],
+ date: [
+ {
+ cells: [
+ [-26, -27, -28, 1, 2, 3, 4],
+ [5, 6, 7, 8, 9, 10, 11],
+ [12, 13, 14, 15, 16, 17, 18],
+ [19, 20, 21, 22, 23, 24, 25],
+ [26, 27, 28, 29, 30, 31, -1],
+ [-2, -3, -4, -5, -6, -7, -8],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ {
+ cells: [
+ [-26, -27, -28, -29, -30, -31, 1],
+ [2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15],
+ [16, 17, 18, 19, 20, 21, 22],
+ [23, 24, "25", 26, 27, 28, 29],
+ [30, -1, -2, -3, -4, -5, -6],
+ ],
+ daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+ },
+ ],
+ time: [
+ [13, 0],
+ [13, 0],
+ ],
+ });
+ });
+
+ //-------------------------------------------------------------------------
+ // Props and interactions
+ //-------------------------------------------------------------------------
+
+ QUnit.test("different rounding", async (assert) => {
+ await mountPicker({
+ rounding: 10,
+ });
+
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ assert.deepEqual(getTexts(hourSelect, "option"), range(24, String));
+ assert.deepEqual(
+ getTexts(minuteSelect, "option"),
+ range(6, (i) => pad2(i * 10))
+ );
+ });
+
+ QUnit.test("no value, select date without handler", async (assert) => {
+ await mountPicker();
+
+ await click(getPickerCell("12"));
+
+ assert.verifySteps([]); // This test just asserts that nothing happens
+ });
+
+ QUnit.test("no value, select date", async (assert) => {
+ await mountPicker({
+ onSelect: (value) => assert.step(formatForStep(value)),
+ });
+
+ await click(getPickerCell("5").at(0));
+ await click(getPickerCell("12"));
+
+ assert.verifySteps(["2023-04-05T13:00:00", "2023-04-12T13:00:00"]);
+ });
+
+ QUnit.test("no value, select time", async (assert) => {
+ await mountPicker({
+ onSelect: (value) => assert.step(formatForStep(value)),
+ });
+
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "18");
+ await editSelect(minuteSelect, null, "5");
+
+ assert.verifySteps(["2023-04-25T18:00:00", "2023-04-25T18:05:00"]);
+ });
+
+ QUnit.test("single value, select date", async (assert) => {
+ await mountPicker({
+ value: DateTime.fromObject({ day: 30, hour: 8, minute: 43 }),
+ onSelect: (value) => assert.step(formatForStep(value)),
+ });
+
+ await click(getPickerCell("5").at(0));
+
+ assert.verifySteps(["2023-04-05T08:43:00"]);
+ });
+
+ QUnit.test("single value, select time", async (assert) => {
+ await mountPicker({
+ value: DateTime.fromObject({ day: 30, hour: 8, minute: 43 }),
+ onSelect: (value) => assert.step(formatForStep(value)),
+ });
+
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "18");
+ await editSelect(minuteSelect, null, "5");
+
+ assert.verifySteps(["2023-04-30T18:43:00", "2023-04-30T18:05:00"]);
+ });
+
+ QUnit.test("single value, select time in twelve-hour clock format", async (assert) => {
+ useTwelveHourClockFormat();
+
+ await mountPicker({
+ value: DateTime.fromObject({ day: 30, hour: 8, minute: 43 }),
+ onSelect: (value) => assert.step(formatForStep(value)),
+ });
+
+ const [hourSelect, minuteSelect, meridiemSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "7");
+ await editSelect(minuteSelect, null, "5");
+ await editSelect(meridiemSelect, null, "PM");
+
+ assert.verifySteps(["2023-04-30T07:43:00", "2023-04-30T07:05:00", "2023-04-30T19:05:00"]);
+ });
+
+ QUnit.test("range value, select date for first value", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ day: 20, hour: 8, minute: 43 }),
+ DateTime.fromObject({ day: 23, hour: 17, minute: 16 }),
+ ],
+ // focusedDateIndex is implicitly 0
+ onSelect: (values) => assert.step(formatForStep(values)),
+ });
+
+ await click(getPickerCell("5").at(0));
+
+ assert.verifySteps(["2023-04-05T08:43:00,2023-04-23T17:16:00"]);
+ });
+
+ QUnit.test("range value, select time for first value", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ day: 20, hour: 8, minute: 43 }),
+ DateTime.fromObject({ day: 23, hour: 17, minute: 16 }),
+ ],
+ focusedDateIndex: 0,
+ onSelect: (values) => assert.step(formatForStep(values)),
+ });
+
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "18");
+ await editSelect(minuteSelect, null, "5");
+
+ assert.verifySteps([
+ "2023-04-20T18:43:00,2023-04-23T17:16:00",
+ "2023-04-20T18:05:00,2023-04-23T17:16:00",
+ ]);
+ });
+
+ QUnit.test("range value, select date for second value", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ day: 20, hour: 8, minute: 43 }),
+ DateTime.fromObject({ day: 23, hour: 17, minute: 16 }),
+ ],
+ focusedDateIndex: 1,
+ onSelect: (values) => assert.step(formatForStep(values)),
+ });
+
+ await click(getPickerCell("21").at(0));
+
+ assert.verifySteps(["2023-04-20T08:43:00,2023-04-21T17:16:00"]);
+ });
+
+ QUnit.test("range value, select time for second value", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ day: 20, hour: 8, minute: 43 }),
+ DateTime.fromObject({ day: 23, hour: 17, minute: 16 }),
+ ],
+ focusedDateIndex: 1,
+ onSelect: (values) => assert.step(formatForStep(values)),
+ });
+
+ const [hourSelect, minuteSelect] = getTimePickers().at(1);
+ await editSelect(hourSelect, null, "18");
+ await editSelect(minuteSelect, null, "5");
+
+ assert.verifySteps([
+ "2023-04-20T08:43:00,2023-04-23T18:16:00",
+ "2023-04-20T08:43:00,2023-04-23T18:05:00",
+ ]);
+ });
+
+ QUnit.test("range value, select date for second value before first value", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ day: 20, hour: 8, minute: 43 }),
+ DateTime.fromObject({ day: 23, hour: 17, minute: 16 }),
+ ],
+ focusedDateIndex: 1,
+ onSelect: (values) => assert.step(formatForStep(values)),
+ });
+
+ await click(getPickerCell("19").at(0));
+
+ assert.verifySteps(["2023-04-19T08:43:00,2023-04-23T17:16:00"]);
+ });
+
+ QUnit.test("range value, select date for first value after second value", async (assert) => {
+ await mountPicker({
+ value: [
+ DateTime.fromObject({ day: 20, hour: 8, minute: 43 }),
+ DateTime.fromObject({ day: 23, hour: 17, minute: 16 }),
+ ],
+ focusedDateIndex: 0,
+ onSelect: (values) => assert.step(formatForStep(values)),
+ });
+
+ await click(getPickerCell("27").at(1));
+
+ assert.verifySteps(["2023-04-20T08:43:00,2023-04-27T17:16:00"]);
+ });
+});
diff --git a/addons/web/static/tests/core/datetime/datetime_test_helpers.js b/addons/web/static/tests/core/datetime/datetime_test_helpers.js
new file mode 100644
index 00000000000..69325c85403
--- /dev/null
+++ b/addons/web/static/tests/core/datetime/datetime_test_helpers.js
@@ -0,0 +1,216 @@
+/** @odoo-module **/
+
+import { patchWithCleanup } from "@web/../tests/helpers/utils";
+import { localization } from "@web/core/l10n/localization";
+import { ensureArray } from "@web/core/utils/arrays";
+import { click, getFixture } from "../../helpers/utils";
+
+/**
+ * @typedef {import("@web/core/datetime/datetime_picker").DateTimePickerProps} DateTimePickerProps
+ */
+
+/**
+ * @param {false | {
+ * title?: string | string[],
+ * date?: {
+ * cells: (number | string | [number] | [string])[][],
+ * daysOfWeek?: string[],
+ * weekNumbers?: number[],
+ * }[],
+ * time?: ([number, number] | [number, number, "AM" | "PM"])[],
+ * }} expectedParams
+ */
+export function assertDateTimePicker(expectedParams) {
+ const assert = QUnit.assert;
+ const fixture = getFixture();
+
+ // Check for picker in DOM
+ if (expectedParams) {
+ assert.containsOnce(fixture, ".o_datetime_picker");
+ } else {
+ assert.containsNone(fixture, ".o_datetime_picker");
+ return;
+ }
+
+ const { title, date, time } = expectedParams;
+
+ // Title
+ if (title) {
+ const expectedTitle = ensureArray(title);
+ assert.containsOnce(fixture, ".o_datetime_picker_header");
+ assert.deepEqual(
+ getTexts(".o_datetime_picker_header", "h5"),
+ expectedTitle,
+ `title should be "${expectedTitle.join(" - ")}"`
+ );
+ } else {
+ assert.containsNone(fixture, ".o_datetime_picker_header");
+ }
+
+ // Time picker
+ if (time) {
+ assert.containsN(fixture, ".o_time_picker", time.length);
+ const timePickers = select(".o_time_picker");
+ for (let i = 0; i < time.length; i++) {
+ const expectedTime = time[i];
+ const values = select(timePickers[i], ".o_time_picker_select").map((sel) => sel.value);
+ const actual = [...values.slice(0, 2).map(Number), ...values.slice(2)];
+ assert.deepEqual(actual, expectedTime, `time values should be [${expectedTime}]`);
+ }
+ } else {
+ assert.containsNone(fixture, ".o_time_picker");
+ }
+
+ // Date picker
+ const datePickerEls = select(".o_date_picker");
+ assert.containsN(fixture, ".o_date_picker", date.length);
+
+ let activeCells = 0;
+ let outOfRangeCells = 0;
+ let todayCells = 0;
+ for (let i = 0; i < date.length; i++) {
+ const { cells, daysOfWeek, weekNumbers } = date[i];
+ const datePickerEl = datePickerEls[i];
+ const cellEls = select(datePickerEl, ".o_date_item_cell");
+
+ assert.strictEqual(
+ cellEls.length,
+ PICKER_ROWS * PICKER_COLS,
+ `picker should have ${
+ PICKER_ROWS * PICKER_COLS
+ } cells (${PICKER_ROWS} rows and ${PICKER_COLS} columns)`
+ );
+
+ if (daysOfWeek) {
+ const actualDow = getTexts(datePickerEl, ".o_day_of_week_cell");
+ assert.deepEqual(
+ actualDow,
+ daysOfWeek,
+ `picker should display the days of week: ${daysOfWeek
+ .map((dow) => `"${dow}"`)
+ .join(", ")}`
+ );
+ }
+
+ if (weekNumbers) {
+ assert.deepEqual(
+ getTexts(datePickerEl, ".o_week_number_cell").map(Number),
+ weekNumbers,
+ `picker should display the week numbers (${weekNumbers.join(", ")})`
+ );
+ }
+
+ // Date cells
+ const expectedCells = cells.flatMap((row, rowIndex) =>
+ row.map((cell, colIndex) => {
+ const cellEl = cellEls[rowIndex * PICKER_COLS + colIndex];
+
+ // Check flags
+ let value = cell;
+ const isActive = Array.isArray(cell);
+ if (isActive) {
+ value = value[0];
+ }
+ const isToday = typeof value === "string";
+ if (isToday) {
+ value = Number(value);
+ }
+ const isOutOfRange = value < 0;
+ if (isOutOfRange) {
+ value = Math.abs(value);
+ }
+
+ // Assert based on flags
+ if (isActive) {
+ activeCells++;
+ assert.hasClass(cellEl, "o_active");
+ }
+ if (isOutOfRange) {
+ outOfRangeCells++;
+ assert.hasClass(cellEl, "o_out_of_range");
+ }
+ if (isToday) {
+ todayCells++;
+ assert.hasClass(cellEl, "o_today");
+ }
+
+ return value;
+ })
+ );
+
+ assert.deepEqual(
+ cellEls.map((cell) => Number(getTexts(cell)[0])),
+ expectedCells,
+ `cell content should match the expected values: [${expectedCells.join(", ")}]`
+ );
+ }
+
+ assert.containsN(fixture, ".o_active", activeCells);
+ assert.containsN(fixture, ".o_out_of_range", outOfRangeCells);
+ assert.containsN(fixture, ".o_today", todayCells);
+}
+
+export function getPickerApplyButton() {
+ return select(".o_datetime_picker .o_datetime_buttons .o_apply").at(0);
+}
+
+/**
+ * @param {RegExp | string} expr
+ */
+export function getPickerCell(expr) {
+ const regex = expr instanceof RegExp ? expr : new RegExp(`^${expr}$`, "i");
+ const cells = select(".o_datetime_picker .o_date_item_cell").filter((cell) =>
+ regex.test(getTexts(cell)[0])
+ );
+ return cells.length === 1 ? cells[0] : cells;
+}
+
+/**
+ * @param {...(string | HTMLElement)} selectors
+ * @returns {string[]}
+ */
+export function getTexts(...selectors) {
+ return select(...selectors).map((e) => e.innerText.trim().replace(/\s+/g, " "));
+}
+
+/**
+ * @param {Object} [options={}]
+ * @param {boolean} [options.parse=false] whether to directly return the parsed
+ * values of the select elements
+ * @returns {HTMLSelectElement[] | (number | string)[]}
+ */
+export function getTimePickers({ parse = false } = {}) {
+ return select(".o_time_picker").map((timePickerEl) => {
+ const selects = select(timePickerEl, ".o_time_picker_select");
+ if (parse) {
+ return selects.map((sel) => (isNaN(sel.value) ? sel.value : Number(sel.value)));
+ } else {
+ return selects;
+ }
+ });
+}
+
+/**
+ * @param {...(string | HTMLElement)} selectors
+ * @returns {HTMLElement[]}
+ */
+const select = (...selectors) => {
+ const root = selectors[0] instanceof Element ? selectors.shift() : getFixture();
+ return selectors.length ? [...root.querySelectorAll(selectors.join(" "))] : [root];
+};
+
+export function useTwelveHourClockFormat() {
+ const { dateFormat = "dd/MM/yyyy", timeFormat = "HH:mm:ss" } = localization;
+ const twcTimeFormat = `${timeFormat.replace(/H/g, "h")} a`;
+ patchWithCleanup(localization, {
+ dateTimeFormat: `${dateFormat} ${twcTimeFormat}`,
+ timeFormat: twcTimeFormat,
+ });
+}
+
+export function zoomOut() {
+ return click(getFixture(), ".o_zoom_out");
+}
+
+const PICKER_ROWS = 6;
+const PICKER_COLS = 7;
diff --git a/addons/web/static/tests/core/domain_selector_tests.js b/addons/web/static/tests/core/domain_selector_tests.js
index 6c708664b43..869cc531fe1 100644
--- a/addons/web/static/tests/core/domain_selector_tests.js
+++ b/addons/web/static/tests/core/domain_selector_tests.js
@@ -14,6 +14,7 @@ import { registerCleanup } from "../helpers/cleanup";
import { makeTestEnv } from "../helpers/mock_env";
import { makeFakeLocalizationService } from "../helpers/mock_services";
import { click, editInput, editSelect, getFixture, mount, triggerEvent } from "../helpers/utils";
+import { getPickerApplyButton, getPickerCell } from "./datetime/datetime_test_helpers";
import { openModelFieldSelectorPopover } from "./model_field_selector_tests";
let serverData;
@@ -218,23 +219,17 @@ QUnit.module("Components", (hooks) => {
await mountComponent(Parent);
// Check that there is a datepicker to choose the date
- assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
+ assert.containsOnce(target, ".o_datetime_input", "there should be a datepicker");
// 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");
+ assert.equal(target.querySelector(".o_datetime_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"]`
- )
- ); // => February 26th
- await click(
- document.body.querySelector(`.bootstrap-datetimepicker-widget a[data-action="close"]`)
- );
+ await click(target, ".o_datetime_input");
+ await click(getPickerCell("26").at(0)); // => February 26th
+ await click(getPickerApplyButton());
// 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");
+ assert.equal(target.querySelector(".o_datetime_input").value, "02/26/2017 16:42:00");
});
QUnit.test("building a domain with a datetime: context_today()", async (assert) => {
@@ -251,19 +246,17 @@ QUnit.module("Components", (hooks) => {
});
// Check that there is a datepicker to choose the date
- assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
+ assert.containsOnce(target, ".o_datetime_input", "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");
+ assert.equal(target.querySelector(".o_datetime_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]`)
- );
+ await click(target, ".o_datetime_input");
+ await click(target);
// The input field should continue displaying 'Invalid DateTime'.
// The value is still invalid.
- assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime");
+ assert.equal(target.querySelector(".o_datetime_input").value, "Invalid DateTime");
assert.verifySteps([]);
});
diff --git a/addons/web/static/tests/core/dropdown_tests.js b/addons/web/static/tests/core/dropdown_tests.js
index 9d2d582e308..d0f90973baa 100644
--- a/addons/web/static/tests/core/dropdown_tests.js
+++ b/addons/web/static/tests/core/dropdown_tests.js
@@ -1,7 +1,9 @@
/** @odoo-module **/
+import { App, Component, xml } from "@odoo/owl";
+import { templates } from "@web/core/assets";
import { browser } from "@web/core/browser/browser";
-import { DateTimePicker } from "@web/core/datepicker/datepicker";
+import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
@@ -21,9 +23,8 @@ import {
triggerHotkey,
} from "../helpers/utils";
import { makeParent } from "./tooltip/tooltip_service_tests";
-import { templates } from "@web/core/assets";
+import { getPickerCell } from "./datetime/datetime_test_helpers";
-import { App, Component, xml } from "@odoo/owl";
const serviceRegistry = registry.category("services");
let env;
@@ -1146,10 +1147,10 @@ QUnit.module("Components", ({ beforeEach }) => {
Dropdown toggler
-
+
`;
- MyComponent.components = { Dropdown, DateTimePicker };
+ MyComponent.components = { DateTimeInput, Dropdown };
await makeParent(MyComponent);
@@ -1158,20 +1159,20 @@ QUnit.module("Components", ({ beforeEach }) => {
await click(target, ".dropdown-toggle");
assert.containsOnce(target, ".o-dropdown--menu");
- assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
- assert.strictEqual(target.querySelector(".o_datepicker_input").value, "");
+ assert.containsNone(target, ".o_datetime_picker");
+ assert.strictEqual(target.querySelector(".o_datetime_input").value, "");
- await click(target, ".o_datepicker_input");
+ await click(target, ".o_datetime_input");
assert.containsOnce(target, ".o-dropdown--menu");
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
- assert.strictEqual(target.querySelector(".o_datepicker_input").value, "");
+ assert.containsOnce(target, ".o_datetime_picker");
+ assert.strictEqual(target.querySelector(".o_datetime_input").value, "");
- await click(document.querySelectorAll(".datepicker table td")[15]); // select some day
+ await click(getPickerCell("15")); // select some day
assert.containsOnce(target, ".o-dropdown--menu");
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
- assert.notOk(target.querySelector(".o_datepicker_input").value === "");
+ assert.containsOnce(target, ".o_datetime_picker");
+ assert.notOk(target.querySelector(".o_datetime_input").value === "");
}
);
diff --git a/addons/web/static/tests/views/calendar/calendar_view_tests.js b/addons/web/static/tests/views/calendar/calendar_view_tests.js
index 1ef896bd2bc..d31e92734e0 100644
--- a/addons/web/static/tests/views/calendar/calendar_view_tests.js
+++ b/addons/web/static/tests/views/calendar/calendar_view_tests.js
@@ -5,6 +5,7 @@ import { defaultLocalization } from "../../helpers/mock_services";
import {
click,
editInput,
+ editSelect,
getFixture,
makeDeferred,
nextTick,
@@ -43,6 +44,7 @@ import { registry } from "@web/core/registry";
import { userService } from "@web/core/user_service";
import { CharField } from "@web/views/fields/char/char_field";
import { actionService } from "@web/webclient/actions/action_service";
+import { getTimePickers } from "../../core/datetime/datetime_test_helpers";
const fieldRegistry = registry.category("fields");
const preloadedDataRegistry = registry.category("preloadedData");
@@ -1622,44 +1624,12 @@ QUnit.module("Views", ({ beforeEach }) => {
);
// use datepicker to enter a date: 12/13/2016 08:00:00
- await click(target, `.o_field_widget[name="start"] .o_datepicker .o_datepicker_input`);
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]`
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .timepicker .timepicker-hour`
- );
- await click(
- document.body.querySelectorAll(
- `.bootstrap-datetimepicker-widget .timepicker-hours td.hour`
- )[8]
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]`
- );
+ await click(target, `.o_field_widget[name="start"] input`);
+ await editSelect(getTimePickers().at(0).at(0), null, "8");
// use datepicker to enter a date: 12/13/2016 10:00:00
- await click(target, `.o_field_widget[name="stop"] .o_datepicker .o_datepicker_input`);
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]`
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .timepicker .timepicker-hour`
- );
- await click(
- document.body.querySelectorAll(
- `.bootstrap-datetimepicker-widget .timepicker-hours td.hour`
- )[10]
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]`
- );
+ await click(target, `.o_field_widget[name="stop"] input`);
+ await editSelect(getTimePickers().at(0).at(0), null, "10");
await click(target, ".modal-footer .o_form_button_save");
assert.strictEqual(
@@ -1859,44 +1829,12 @@ QUnit.module("Views", ({ beforeEach }) => {
);
// use datepicker to enter a date: 12/13/2016 08:00:00
- await click(target, `.o_field_widget[name="start"] .o_datepicker .o_datepicker_input`);
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]`
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .timepicker .timepicker-hour`
- );
- await click(
- document.body.querySelectorAll(
- `.bootstrap-datetimepicker-widget .timepicker-hours td.hour`
- )[8]
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]`
- );
+ await click(target, `.o_field_widget[name="start"] input`);
+ await editSelect(getTimePickers().at(0).at(0), null, "8");
// use datepicker to enter a date: 12/13/2016 10:00:00
- await click(target, `.o_field_widget[name="stop"] .o_datepicker .o_datepicker_input`);
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]`
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .timepicker .timepicker-hour`
- );
- await click(
- document.body.querySelectorAll(
- `.bootstrap-datetimepicker-widget .timepicker-hours td.hour`
- )[10]
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]`
- );
+ await click(target, `.o_field_widget[name="stop"] input`);
+ await editSelect(getTimePickers().at(0).at(0), null, "10");
await click(target, ".modal-footer button.btn-primary:not(.d-none)");
assert.strictEqual(
@@ -3856,38 +3794,12 @@ QUnit.module("Views", ({ beforeEach }) => {
await click(target.querySelector(`.o_field_widget[name="allday"] input`));
// use datepicker to enter a date: 12/20/2016 07:00:00
- await click(target, `.o_field_widget[name="start"] .o_datepicker .o_datepicker_input`);
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]`
- );
- await click(document.body, `.bootstrap-datetimepicker-widget .timepicker .timepicker-hour`);
- await click(
- document.body.querySelectorAll(
- `.bootstrap-datetimepicker-widget .timepicker-hours td.hour`
- )[7]
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]`
- );
+ await click(target, `.o_field_widget[name="start"] input`);
+ await editSelect(getTimePickers().at(0).at(0), null, "7");
// use datepicker to enter a date: 12/20/2016 19:00:00
- await click(target, `.o_field_widget[name="stop"] .o_datepicker .o_datepicker_input`);
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]`
- );
- await click(document.body, `.bootstrap-datetimepicker-widget .timepicker .timepicker-hour`);
- await click(
- document.body.querySelectorAll(
- `.bootstrap-datetimepicker-widget .timepicker-hours td.hour`
- )[19]
- );
- await click(
- document.body,
- `.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]`
- );
+ await click(target, `.o_field_widget[name="stop"] input`);
+ await editSelect(getTimePickers().at(0).at(0), null, "19");
await click(target.querySelector(".modal .o_form_button_save"));
diff --git a/addons/web/static/tests/views/fields/date_field_tests.js b/addons/web/static/tests/views/fields/date_field_tests.js
index cd2df9ce7fa..0674200c53c 100644
--- a/addons/web/static/tests/views/fields/date_field_tests.js
+++ b/addons/web/static/tests/views/fields/date_field_tests.js
@@ -1,6 +1,6 @@
/** @odoo-module **/
-import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
+import { getPickerCell, zoomOut } from "@web/../tests/core/datetime/datetime_test_helpers";
import {
click,
clickCreate,
@@ -16,8 +16,7 @@ import {
triggerScroll,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
-import { strftimeToLuxonFormat } from "@web/core/l10n/dates";
-import { registry } from "@web/core/registry";
+import { localization } from "@web/core/l10n/localization";
let serverData;
let target;
@@ -70,7 +69,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("DateField");
- QUnit.test("DateField: toggle datepicker [REQUIRE FOCUS]", async function (assert) {
+ QUnit.test("DateField: toggle datepicker", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -81,75 +80,56 @@ QUnit.module("Fields", (hooks) => {
`,
});
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be closed initially"
- );
+ assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed initially");
- await click(target, ".o_datepicker input");
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
+ await click(target, ".o_field_date input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
// focus another field
- target.querySelector(".o_field_widget[name='foo'] input").focus();
+ await click(target, ".o_field_widget[name='foo'] input");
assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
+ target,
+ ".o_datetime_picker",
"datepicker should close itself when the user clicks outside"
);
});
- QUnit.test(
- "DateField: toggle datepicker far in the future [REQUIRE FOCUS]",
- async function (assert) {
- serverData.models.partner.records = [
- {
- id: 1,
- date: "9999-12-30",
- foo: "yop",
- },
- ];
+ QUnit.test("DateField: toggle datepicker far in the future", async (assert) => {
+ serverData.models.partner.records = [
+ {
+ id: 1,
+ date: "9999-12-30",
+ foo: "yop",
+ },
+ ];
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: `
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ resId: 1,
+ serverData,
+ arch: `
`,
- });
+ });
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be closed initially"
- );
+ assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed initially");
- await click(target, ".o_datepicker input");
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
+ await click(target, ".o_field_date input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
- // focus another field
- target.querySelector(".o_field_widget[name='foo'] input").focus();
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should close itself when the user clicks outside"
- );
- }
- );
+ // focus another field
+ await click(target, ".o_field_widget[name='foo'] input");
+ assert.containsNone(
+ target,
+ ".o_datetime_picker",
+ "datepicker should close itself when the user clicks outside"
+ );
+ });
- QUnit.test("date field is empty if no date is set", async function (assert) {
+ QUnit.test("date field is empty if no date is set", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -170,47 +150,24 @@ QUnit.module("Fields", (hooks) => {
);
});
- QUnit.test(
- "DateField: set an invalid date when the field is already set",
- async function (assert) {
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: '',
- });
+ QUnit.test("DateField: set an invalid date when the field is already set", async (assert) => {
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ resId: 1,
+ serverData,
+ arch: '',
+ });
- const input = target.querySelector(".o_field_widget[name='date'] input");
- assert.strictEqual(input.value, "02/03/2017");
+ const input = target.querySelector(".o_field_widget[name='date'] input");
+ assert.strictEqual(input.value, "02/03/2017");
- input.value = "mmmh";
- await triggerEvent(input, null, "change");
- assert.strictEqual(input.value, "02/03/2017", "should have reset the original value");
- }
- );
+ input.value = "mmmh";
+ await triggerEvent(input, null, "change");
+ assert.strictEqual(input.value, "02/03/2017", "should have reset the original value");
+ });
- QUnit.test(
- "DateField: set an invalid date when the field is not set yet",
- async function (assert) {
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 4,
- serverData,
- arch: '',
- });
-
- const input = target.querySelector(".o_field_widget[name='date'] input");
- assert.strictEqual(input.value, "");
-
- input.value = "mmmh";
- await triggerEvent(input, null, "change");
- assert.strictEqual(input.value, "", "The date field should be empty");
- }
- );
-
- QUnit.test("DateField value should not set on first click", async function (assert) {
+ QUnit.test("DateField: set an invalid date when the field is not set yet", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -219,25 +176,42 @@ QUnit.module("Fields", (hooks) => {
arch: '',
});
- await click(target, ".o_datepicker input");
+ const input = target.querySelector(".o_field_widget[name='date'] input");
+ assert.strictEqual(input.value, "");
+
+ input.value = "mmmh";
+ await triggerEvent(input, null, "change");
+ assert.strictEqual(input.value, "", "The date field should be empty");
+ });
+
+ QUnit.test("DateField value should not set on first click", async (assert) => {
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ resId: 4,
+ serverData,
+ arch: '',
+ });
+
+ await click(target, ".o_field_date input");
// open datepicker and select a date
assert.strictEqual(
target.querySelector(".o_field_widget[name='date'] input").value,
"",
"date field's input should be empty on first click"
);
- await click(document.body, ".day[data-day*='/22/']");
+ await click(getPickerCell("22"));
// re-open datepicker
- await click(target, ".o_datepicker input");
+ await click(target, ".o_field_date input");
assert.strictEqual(
- document.body.querySelector(".day.active").textContent,
+ target.querySelector(".o_date_item_cell.o_active").textContent,
"22",
"datepicker should be highlight with 22nd day of month"
);
});
- QUnit.test("DateField in form view (with positive time zone offset)", async function (assert) {
+ QUnit.test("DateField in form view (with positive time zone offset)", async (assert) => {
assert.expect(7);
patchTimeZone(120); // Should be ignored by date fields
@@ -260,39 +234,28 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
// open datepicker and select another value
- await click(target, ".o_datepicker_input");
+ await click(target, ".o_field_date input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
- assert.containsOnce(
- document.body,
- ".day.active[data-day='02/03/2017']",
- "datepicker should be highlight February 3"
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[1]);
- await click(document.body.querySelector(".day[data-day*='/22/']"));
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be closed"
+ target,
+ ".o_date_item_cell.o_active",
+ "datepicker should have a selected day"
);
+ // select 22 Feb 2017
+ await zoomOut();
+ await zoomOut();
+ await click(getPickerCell("2017"));
+ await click(getPickerCell("Feb"));
+ await click(getPickerCell("22"));
+ assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
"02/22/2017",
"the selected date should be displayed in the input"
);
@@ -306,7 +269,7 @@ QUnit.module("Fields", (hooks) => {
);
});
- QUnit.test("DateField in form view (with negative time zone offset)", async function (assert) {
+ QUnit.test("DateField in form view (with negative time zone offset)", async (assert) => {
patchTimeZone(-120); // Should be ignored by date fields
await makeView({
@@ -318,13 +281,13 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
});
- QUnit.test("DateField dropdown disappears on scroll", async function (assert) {
+ QUnit.test("DateField dropdown doesn't disappear on scroll", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -338,22 +301,14 @@ QUnit.module("Fields", (hooks) => {
`,
});
- await click(target, ".o_datepicker input");
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
+ await click(target, ".o_field_date input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await triggerScroll(target, { top: 50 });
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be closed"
- );
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should still be opened");
});
- QUnit.test("DateField with label opens datepicker on click", async function (assert) {
+ QUnit.test("DateField with label opens datepicker on click", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -367,14 +322,10 @@ QUnit.module("Fields", (hooks) => {
});
await click(target.querySelector("label.o_form_label"));
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
});
- QUnit.test("DateField with warn_future option", async function (assert) {
+ QUnit.test("DateField with warn_future option", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -382,42 +333,36 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
`,
});
// open datepicker and select another value
- await click(target, ".o_datepicker input");
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[11]);
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[11]);
- await click(document.body, ".day[data-day*='/31/']");
+ await click(target, ".o_field_date input");
+ await zoomOut();
+ await zoomOut();
+ await click(getPickerCell("2030"));
+ await click(getPickerCell("Dec"));
+ await click(getPickerCell("31"));
assert.containsOnce(
target,
- ".o_datepicker_warning",
+ ".fa-exclamation-triangle",
"should have a warning in the form view"
);
- const input = target.querySelector(".o_field_widget[name='date'] input");
- input.value = "";
- await triggerEvent(input, null, "change"); // remove the value
+ await editInput(target, ".o_field_widget[name='date'] input", "");
assert.containsNone(
target,
- ".o_datepicker_warning",
+ ".fa-exclamation-triangle",
"the warning in the form view should be hidden"
);
});
QUnit.test(
"DateField with warn_future option: do not overwrite datepicker option",
- async function (assert) {
+ async (assert) => {
// Making sure we don't have a legit default value
// or any onchange that would set the value
serverData.models.partner.fields.date.default = undefined;
@@ -431,7 +376,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
`,
});
@@ -450,7 +395,7 @@ QUnit.module("Fields", (hooks) => {
}
);
- QUnit.test("DateField in editable list view", async function (assert) {
+ QUnit.test("DateField in editable list view", async (assert) => {
await makeView({
type: "list",
resModel: "partner",
@@ -468,45 +413,33 @@ QUnit.module("Fields", (hooks) => {
assert.containsOnce(
target,
- "input.o_datepicker_input",
+ ".o_field_date input",
"the view should have a date input for editable mode"
);
assert.strictEqual(
- target.querySelector("input.o_datepicker_input"),
+ target.querySelector(".o_field_date input"),
document.activeElement,
"date input should have the focus"
);
assert.strictEqual(
- target.querySelector("input.o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
// open datepicker and select another value
- await click(target, ".o_datepicker_input");
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[1]);
- await click(document.body.querySelector(".day[data-day*='/22/']"));
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be closed"
- );
+ await click(target, ".o_field_date input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
+ await zoomOut();
+ await zoomOut();
+ await click(getPickerCell("2017"));
+ await click(getPickerCell("Feb"));
+ await click(getPickerCell("22"));
+ assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
"02/22/2017",
"the selected date should be displayed in the input"
);
@@ -520,44 +453,41 @@ QUnit.module("Fields", (hooks) => {
);
});
- QUnit.test(
- "multi edition of DateField in list view: clear date in input",
- async function (assert) {
- serverData.models.partner.records[1].date = "2017-02-03";
+ QUnit.test("multi edition of DateField in list view: clear date in input", async (assert) => {
+ serverData.models.partner.records[1].date = "2017-02-03";
- await makeView({
- serverData,
- type: "list",
- resModel: "partner",
- arch: '',
- });
+ await makeView({
+ serverData,
+ type: "list",
+ resModel: "partner",
+ arch: '',
+ });
- const rows = target.querySelectorAll(".o_data_row");
+ const rows = target.querySelectorAll(".o_data_row");
- // select two records and edit them
- await click(rows[0], ".o_list_record_selector input");
- await click(rows[1], ".o_list_record_selector input");
+ // select two records and edit them
+ await click(rows[0], ".o_list_record_selector input");
+ await click(rows[1], ".o_list_record_selector input");
- await click(rows[0], ".o_data_cell");
+ await click(rows[0], ".o_data_cell");
- assert.containsOnce(target, "input.o_datepicker_input");
- await editInput(target, ".o_datepicker_input", "");
+ assert.containsOnce(target, ".o_field_date input");
+ await editInput(target, ".o_field_date input", "");
- assert.containsOnce(document.body, ".modal");
- await click(target, ".modal .modal-footer .btn-primary");
+ assert.containsOnce(target, ".modal");
+ await click(target, ".modal .modal-footer .btn-primary");
- assert.strictEqual(
- target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
- ""
- );
- assert.strictEqual(
- target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
- ""
- );
- }
- );
+ assert.strictEqual(
+ target.querySelector(".o_data_row:first-child .o_data_cell").textContent,
+ ""
+ );
+ assert.strictEqual(
+ target.querySelector(".o_data_row:nth-child(2) .o_data_cell").textContent,
+ ""
+ );
+ });
- QUnit.test("DateField remove value", async function (assert) {
+ QUnit.test("DateField remove value", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -572,16 +502,16 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
"02/03/2017",
"the date should be correct in edit mode"
);
- const input = target.querySelector(".o_datepicker_input");
+ const input = target.querySelector(".o_field_date input");
input.value = "";
await triggerEvents(input, null, ["input", "change", "focusout"]);
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
"",
"should have correctly removed the value"
);
@@ -595,93 +525,32 @@ QUnit.module("Fields", (hooks) => {
);
});
- QUnit.test(
- "do not trigger a field_changed for datetime field with date widget",
- async function (assert) {
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: '',
- mockRPC(route, { method }) {
- assert.step(method);
- },
- });
-
- assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
- "02/08/2017",
- "the date should be correct"
- );
-
- const input = target.querySelector(".o_field_widget[name='datetime'] input");
- input.value = "02/08/2017";
- await triggerEvents(input, null, ["input", "change", "focusout"]);
-
- assert.containsOnce(target, ".o_form_saved");
- assert.verifySteps(["get_views", "read"]); // should not have save as nothing changed
- }
- );
-
- QUnit.test(
- "field date should select its content onclick when there is one",
- async function (assert) {
- assert.expect(3);
- const done = assert.async();
-
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: '',
- });
-
- $(target).on("show.datetimepicker", () => {
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "bootstrap-datetimepicker is visible"
- );
- const active = document.activeElement;
- assert.strictEqual(
- active.tagName,
- "INPUT",
- "The datepicker input should be focused"
- );
- assert.strictEqual(
- active.value.slice(active.selectionStart, active.selectionEnd),
- "02/03/2017",
- "The whole input of the date field should have been selected"
- );
- done();
- });
-
- await click(target, ".o_datepicker input");
- }
- );
-
- QUnit.test("DateField support internationalization", async function (assert) {
- // The DatePicker component needs the locale to be available since it
- // is still using Moment.js for the bootstrap datepicker
- const originalLocale = moment.locale();
- moment.defineLocale("no", {
- monthsShort: "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),
- monthsParseExact: true,
- dayOfMonthOrdinalParse: /\d{1,2}\./,
- ordinal: "%d.",
+ QUnit.test("field date should select its content onclick when there is one", async (assert) => {
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ resId: 1,
+ serverData,
+ arch: '',
});
- registry.category("services").remove("localization");
- registry
- .category("services")
- .add(
- "localization",
- makeFakeLocalizationService({ dateFormat: strftimeToLuxonFormat("%d-%m/%Y") })
- );
- patchWithCleanup(luxon.Settings, {
- defaultLocale: "no",
+ const input = target.querySelector(".o_field_date input");
+ await click(input);
+ input.focus();
+
+ assert.containsOnce(target, ".o_datetime_picker");
+ const active = document.activeElement;
+ assert.strictEqual(active.tagName, "INPUT", "The datepicker input should be focused");
+ assert.strictEqual(
+ active.value.slice(active.selectionStart, active.selectionEnd),
+ "02/03/2017",
+ "The whole input of the date field should have been selected"
+ );
+ });
+
+ QUnit.test("DateField supports custom format", async (assert) => {
+ patchWithCleanup(localization, {
+ dateFormat: "dd-MM-yyyy",
});
await makeView({
@@ -693,27 +562,60 @@ QUnit.module("Fields", (hooks) => {
});
const dateViewForm = target.querySelector(".o_field_date input").value;
- await click(target, ".o_datepicker input");
+ await click(target, ".o_field_date input");
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_date input").value,
dateViewForm,
"input date field should be the same as it was in the view form"
);
- await click(document.body.querySelector(".day[data-day*='/22/']"));
- const dateEditForm = target.querySelector(".o_datepicker_input").value;
+ await click(getPickerCell("22"));
+ const dateEditForm = target.querySelector(".o_field_date input").value;
await clickSave(target);
assert.strictEqual(
target.querySelector(".o_field_date input").value,
dateEditForm,
"date field should be the same as the one selected in the view form"
);
-
- moment.locale(originalLocale);
- moment.updateLocale("no", null);
});
- QUnit.test("DateField: hit enter should update value", async function (assert) {
+ QUnit.test("DateField supports internationalization", async (assert) => {
+ patchWithCleanup(luxon.Settings, {
+ defaultLocale: "nb-NO",
+ });
+
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ serverData,
+ arch: '',
+ resId: 1,
+ });
+
+ const dateViewForm = target.querySelector(".o_field_date input").value;
+ await click(target, ".o_field_date input");
+
+ assert.strictEqual(
+ target.querySelector(".o_field_date input").value,
+ dateViewForm,
+ "input date field should be the same as it was in the view form"
+ );
+ assert.strictEqual(
+ target.querySelector(".o_zoom_out h5").textContent,
+ "februar 2017",
+ "Norwegian locale should be correctly applied"
+ );
+ await click(getPickerCell("22"));
+ const dateEditForm = target.querySelector(".o_field_date input").value;
+ await clickSave(target);
+ assert.strictEqual(
+ target.querySelector(".o_field_date input").value,
+ dateEditForm,
+ "date field should be the same as the one selected in the view form"
+ );
+ });
+
+ QUnit.test("DateField: hit enter should update value", async (assert) => {
patchTimeZone(120);
await makeView({
@@ -744,7 +646,7 @@ QUnit.module("Fields", (hooks) => {
);
});
- QUnit.test("DateField: allow to use compute dates (+5d for instance)", async function (assert) {
+ QUnit.test("DateField: allow to use compute dates (+5d for instance)", async (assert) => {
patchDate(2021, 1, 15, 10, 0, 0); // current date : 15 Feb 2021 10:00:00
serverData.models.partner.fields.date.default = "2019-09-15";
diff --git a/addons/web/static/tests/views/fields/daterange_field_tests.js b/addons/web/static/tests/views/fields/daterange_field_tests.js
index 04715c05e69..6044f695712 100644
--- a/addons/web/static/tests/views/fields/daterange_field_tests.js
+++ b/addons/web/static/tests/views/fields/daterange_field_tests.js
@@ -1,20 +1,27 @@
/** @odoo-module **/
+import { getPickerCell, getTimePickers } from "@web/../tests/core/datetime/datetime_test_helpers";
import {
click,
clickSave,
editInput,
getFixture,
+ nextTick,
patchDate,
patchTimeZone,
- patchWithCleanup,
triggerEvent,
triggerScroll,
} from "@web/../tests/helpers/utils";
-import { localization } from "@web/core/l10n/localization";
-import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
-import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { pagerNext } from "@web/../tests/search/helpers";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+
+/**
+ * @param {HTMLElement} el
+ */
+const isHiddenByCSS = (el) => {
+ const style = getComputedStyle(el);
+ return style.visibility === "hidden" || style.opacity === "0";
+};
let serverData;
let target;
@@ -28,6 +35,7 @@ QUnit.module("Fields", (hooks) => {
fields: {
date: { string: "A date", type: "date", searchable: true },
datetime: { string: "A datetime", type: "datetime", searchable: true },
+ datetime_end: { string: "Datetime End", type: "datetime" },
},
records: [
{
@@ -53,13 +61,250 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("DateRangeField");
+ QUnit.test("Datetime field - interaction with the datepicker", async (assert) => {
+ serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
+
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ resId: 1,
+ serverData,
+ arch: `
+ `,
+ });
+
+ // Check date range picker initialization
+ assert.containsOnce(getFixture(), ".o_field_daterange");
+ assert.containsNone(getFixture(), ".o_datetime_picker");
+
+ // open the first one
+ const daterange = target.querySelector(".o_field_daterange");
+ await click(daterange.querySelector("input[data-field=datetime]"));
+
+ let datepicker = document.querySelector(".o_datetime_picker");
+ assert.isVisible(datepicker, "first date range picker should be opened");
+ assert.strictEqual(
+ datepicker.querySelector(".o_date_item_cell.o_active_start").textContent,
+ "8",
+ "active start date should be '8' in date range picker"
+ );
+ let [hourSelectStart, minuteSelectStart] = getTimePickers().at(0);
+ assert.strictEqual(
+ hourSelectStart.value,
+ "15",
+ "active start date hour should be '15' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectStart.value,
+ "30",
+ "active start date minute should be '30' in date range picker"
+ );
+ assert.strictEqual(
+ datepicker.querySelector(".o_date_item_cell.o_active_end").textContent,
+ "13",
+ "active end date should be '13' in date range picker"
+ );
+ let [hourSelectEnd, minuteSelectEnd] = getTimePickers().at(1);
+ assert.strictEqual(
+ hourSelectEnd.value,
+ "5",
+ "active end date hour should be '5' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectEnd.value,
+ "30",
+ "active end date minute should be '30' in date range picker"
+ );
+ assert.containsN(
+ minuteSelectStart,
+ "option",
+ 12,
+ "minute selection should contain 12 options (1 for each 5 minutes)"
+ );
+ // Close picker
+ await click(document.querySelector(".o_form_view_container"));
+ assert.containsNone(getFixture(), ".o_datetime_picker", "datepicker should be closed");
+
+ // Try to check with end date
+ await click(daterange.querySelector("input[data-field=datetime_end]"));
+
+ datepicker = document.querySelector(".o_datetime_picker");
+ assert.isVisible(datepicker, "first date range picker should be opened");
+ assert.strictEqual(
+ datepicker.querySelector(".o_date_item_cell.o_active_start").textContent,
+ "8",
+ "active start date should be '8' in date range picker"
+ );
+ [hourSelectStart, minuteSelectStart] = getTimePickers().at(0);
+ assert.strictEqual(
+ hourSelectStart.value,
+ "15",
+ "active start date hour should be '15' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectStart.value,
+ "30",
+ "active start date minute should be '30' in date range picker"
+ );
+ assert.strictEqual(
+ datepicker.querySelector(".o_date_item_cell.o_active_end").textContent,
+ "13",
+ "active end date should be '13' in date range picker"
+ );
+ [hourSelectEnd, minuteSelectEnd] = getTimePickers().at(1);
+ assert.strictEqual(
+ hourSelectEnd.value,
+ "5",
+ "active end date hour should be '5' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectEnd.value,
+ "30",
+ "active end date minute should be '30' in date range picker"
+ );
+ assert.containsN(
+ minuteSelectStart,
+ "option",
+ 12,
+ "minute selection should contain 12 options (1 for each 5 minutes)"
+ );
+
+ // Select a new range and check that inputs are updated
+ await click(getPickerCell("8").at(0)); // 02/08/2017
+ await click(getPickerCell("9").at(0)); // 02/09/2017
+ assert.equal(
+ target.querySelector("input[data-field=datetime]").value,
+ "02/08/2017 15:30:00"
+ );
+ assert.equal(
+ target.querySelector("input[data-field=datetime_end]").value,
+ "02/09/2017 05:30:00"
+ );
+
+ // Save
+ await clickSave(target);
+
+ // Check date after save
+ assert.strictEqual(
+ target.querySelector("input[data-field=datetime]").value,
+ "02/08/2017 15:30:00"
+ );
+ assert.strictEqual(
+ target.querySelector("input[data-field=datetime_end]").value,
+ "02/09/2017 05:30:00"
+ );
+ });
+
+ QUnit.test("Date field - interaction with the datepicker", async (assert) => {
+ serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
+ serverData.models.partner.records[0].date_end = "2017-02-08";
+
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ resId: 1,
+ serverData,
+ arch: `
+ `,
+ });
+
+ // Check date range picker initialization
+ assert.containsOnce(getFixture(), ".o_field_daterange");
+ assert.containsNone(getFixture(), ".o_datetime_picker");
+
+ // open the first one
+ await click(target.querySelector("input[data-field=date]"));
+ let datepicker = document.querySelector(".o_datetime_picker");
+
+ assert.isVisible(datepicker, "first date range picker should be opened");
+ assert.strictEqual(
+ datepicker.querySelector(".o_active_start").textContent,
+ "3",
+ "active start date should be '3' in date range picker"
+ );
+ assert.strictEqual(
+ datepicker.querySelector(".o_active_end").textContent,
+ "8",
+ "active end date should be '8' in date range picker"
+ );
+
+ // Change date
+ await click(getPickerCell("12").at(1));
+ await click(getPickerCell("16").at(0));
+ // Close picker
+ await click(document.querySelector(".o_form_view"));
+
+ // Check date after change
+ assert.isNotVisible(datepicker, "date range picker should be closed");
+ assert.strictEqual(
+ target.querySelector("input[data-field=date]").value,
+ "02/16/2017",
+ "the date should be '02/16/2017'"
+ );
+ assert.strictEqual(
+ target.querySelector("input[data-field=date_end]").value,
+ "03/12/2017",
+ "'the date should be '03/12/2017'"
+ );
+
+ // Try to change range with end date
+ await click(target.querySelector("input[data-field=date_end]"));
+ datepicker = document.querySelector(".o_datetime_picker");
+
+ assert.isVisible(datepicker, "date range picker should be opened");
+ assert.strictEqual(
+ datepicker.querySelector(".o_active_start").textContent,
+ "16",
+ "start date should be a 16 in date range picker"
+ );
+ assert.strictEqual(
+ datepicker.querySelector(".o_active_end").textContent,
+ "12",
+ "end date should be a 12 in date range picker"
+ );
+
+ // Change date
+ await click(getPickerCell("13").at(0));
+ await click(getPickerCell("18").at(1));
+ // Close picker
+ await click(document.querySelector(".o_form_view"));
+
+ // Check date after change
+ assert.isNotVisible(datepicker, "date range picker should be closed");
+ assert.strictEqual(
+ target.querySelector("input[data-field=date]").value,
+ "02/13/2017",
+ "the start date should be '02/13/2017'"
+ );
+ assert.strictEqual(
+ target.querySelector("input[data-field=date_end]").value,
+ "03/18/2017",
+ "the end date should be '03/18/2017'"
+ );
+
+ // Save
+ await clickSave(target);
+
+ // Check date after save
+ assert.strictEqual(
+ target.querySelector("input[data-field=date]").value,
+ "02/13/2017",
+ "the start date should be '02/13/2017' after save"
+ );
+ assert.strictEqual(
+ target.querySelector("input[data-field=date_end]").value,
+ "03/18/2017",
+ "the end date should be '03/18/2017' after save"
+ );
+ });
+
QUnit.test(
- "Datetime field - interaction with the datepicker [REQUIRE FOCUS]",
- async function (assert) {
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
+ "date picker should still be present when scrolling outside of it",
+ async (assert) => {
serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
await makeView({
@@ -69,323 +314,25 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
`,
});
- // Check date range picker initialization
- assert.containsN(
- document.body,
- ".daterangepicker",
- 2,
- "should initialize 2 date range picker"
- );
- const datepickers = document.querySelectorAll(`.daterangepicker`);
- assert.isNotVisible(
- datepickers[0],
- "first date range picker should be closed initially"
- );
- assert.isNotVisible(
- datepickers[datepickers.length - 1],
- "second date range picker should be closed initially"
- );
-
- // open the first one
- let fields = target.querySelectorAll(".o_field_daterange");
- await click(fields[0].querySelector("input"));
-
- let datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[0].getAttribute("name")}"]`
- );
- assert.isVisible(datepicker, "first date range picker should be opened");
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .active.start-date").textContent,
- "8",
- "active start date should be '8' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .hourselect").value,
- "15",
- "active start date hour should be '15' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .minuteselect").value,
- "30",
- "active start date minute should be '30' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .active.end-date").textContent,
- "13",
- "active end date should be '13' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .hourselect").value,
- "5",
- "active end date hour should be '5' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .minuteselect").value,
- "30",
- "active end date minute should be '30' in date range picker"
- );
- assert.containsN(
- datepicker.querySelector(".drp-calendar.left .minuteselect"),
- "option",
- 12,
- "minute selection should contain 12 options (1 for each 5 minutes)"
- );
- // Close picker
- await click(datepicker.querySelector(".cancelBtn"));
- assert.strictEqual(
- datepicker.style.display,
- "none",
- "date range picker should be closed"
- );
-
- // Try to check with end date
- await click(fields[fields.length - 1], "input");
- datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[fields.length - 1].getAttribute("name")}"]`
- );
-
- assert.isVisible(datepicker, "date range picker should be opened");
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .active.start-date").textContent,
- "8",
- "active start date should be '8' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .hourselect").value,
- "15",
- "active start date hour should be '15' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .minuteselect").value,
- "30",
- "active start date minute should be '30' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .active.end-date").textContent,
- "13",
- "active end date should be '13' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .hourselect").value,
- "5",
- "active end date hour should be '5' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .minuteselect").value,
- "30",
- "active end date minute should be '30' in date range picker"
- );
-
- // Select a new range and check that inputs are updated
- await triggerEvent(datepicker, ".start-date", "mousedown"); // 02/08/2017
- await triggerEvent(datepicker, ".start-date + .available", "mousedown"); // 02/09/2017
- await click(datepicker, ".applyBtn");
- assert.equal(
- target.querySelector("[name=datetime] input").value,
- "02/08/2017 15:30:00"
- );
- assert.equal(
- target.querySelector("[name=datetime_end] input").value,
- "02/09/2017 05:30:00"
- );
-
- // Save
- await clickSave(target);
- fields = target.querySelectorAll(".o_field_daterange input");
-
- // Check date after save
- assert.strictEqual(fields[0].value, "02/08/2017 15:30:00");
- assert.strictEqual(fields[fields.length - 1].value, "02/09/2017 05:30:00");
- }
- );
-
- QUnit.test(
- "Date field - interaction with the datepicker [REQUIRE FOCUS]",
- async function (assert) {
- serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
- serverData.models.partner.records[0].date_end = "2017-02-08";
-
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: `
- `,
- });
-
- let fields = target.querySelectorAll(".o_field_daterange");
- const datepickers = document.querySelectorAll(`.daterangepicker`);
-
- // Check date range picker initialization
- assert.containsN(
- document.body,
- ".daterangepicker",
- 2,
- "should initialize 2 date range picker"
- );
- assert.isNotVisible(
- datepickers[0],
- "first date range picker should be closed initially"
- );
- assert.isNotVisible(
- datepickers[datepickers.length - 1],
- "second date range picker should be closed initially"
- );
-
- // open the first one
- await click(fields[0].querySelector("input"));
- let datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[0].getAttribute("name")}"]`
- );
-
- assert.isVisible(datepicker, "first date range picker should be opened");
- assert.strictEqual(
- datepicker.querySelector(".active.start-date").textContent,
- "3",
- "active start date should be '3' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".active.end-date").textContent,
- "8",
- "active end date should be '8' in date range picker"
- );
-
- // Change date
- await triggerEvent(
- datepicker,
- ".drp-calendar.left .available[data-title='r2c4']",
- "mousedown"
- );
- await triggerEvent(
- datepicker,
- ".drp-calendar.right .available[data-title='r2c0']",
- "mousedown"
- );
- await click(datepicker.querySelector(".applyBtn"));
-
- // Check date after change
- assert.isNotVisible(datepicker, "date range picker should be closed");
- assert.strictEqual(
- fields[0].querySelector("input").value,
- "02/16/2017",
- "the date should be '02/16/2017'"
- );
- assert.strictEqual(
- fields[1].querySelector("input").value,
- "03/12/2017",
- "'the date should be '03/12/2017'"
- );
-
- // Try to change range with end date
- await click(fields[1].querySelector("input"));
- datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[1].getAttribute("name")}"]`
- );
-
- assert.isVisible(datepicker, "date range picker should be opened");
- assert.strictEqual(
- datepicker.querySelector(".active.start-date").textContent,
- "16",
- "start date should be a 16 in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".active.end-date").textContent,
- "12",
- "end date should be a 12 in date range picker"
- );
-
- // Change date
- await triggerEvent(
- datepicker,
- ".drp-calendar.left .available[data-title='r2c1']",
- "mousedown"
- );
- await triggerEvent(
- datepicker,
- ".drp-calendar.right .available[data-title='r2c6']",
- "mousedown"
- );
- await click(datepicker, ".applyBtn");
-
- // Check date after change
- assert.isNotVisible(
- datepickers[datepickers.length - 1],
- "date range picker should be closed"
- );
- assert.strictEqual(
- fields[0].querySelector("input").value,
- "02/13/2017",
- "the start date should be '02/13/2017'"
- );
- assert.strictEqual(
- fields[1].querySelector("input").value,
- "03/18/2017",
- "the end date should be '03/18/2017'"
- );
-
- // Save
- await clickSave(target);
- fields = target.querySelectorAll(".o_field_daterange");
-
- // Check date after save
- assert.strictEqual(
- fields[0].querySelector("input").value,
- "02/13/2017",
- "the start date should be '02/13/2017' after save"
- );
- assert.strictEqual(
- fields[1].querySelector("input").value,
- "03/18/2017",
- "the end date should be '03/18/2017' after save"
- );
- }
- );
-
- QUnit.test(
- "daterangepicker should disappear on scrolling outside of it",
- async function (assert) {
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
- serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
-
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: `
- `,
- });
-
- await click(target.querySelector(".o_field_daterange[name='datetime'] input"));
+ await click(target.querySelector("input[data-field=datetime]"));
assert.isVisible(
- document.querySelector(".daterangepicker[data-name='datetime']"),
+ document.querySelector(".o_datetime_picker"),
"date range picker should be opened"
);
await triggerScroll(target, { top: 50 });
- assert.isNotVisible(
- document.querySelector(".daterangepicker[data-name='datetime']"),
- "date range picker should be closed"
+ assert.isVisible(
+ document.querySelector(".o_datetime_picker"),
+ "date range picker should still be opened"
);
}
);
- QUnit.test("DateRangeField with label opens datepicker on click", async function (assert) {
+ QUnit.test("DateRangeField with label opens datepicker on click", async (assert) => {
serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
serverData.models.partner.records[0].date_end = "2017-02-08";
@@ -397,27 +344,22 @@ QUnit.module("Fields", (hooks) => {
arch: `
`,
});
await click(target.querySelector("label.o_form_label"));
assert.isVisible(
- document.querySelector(".daterangepicker[data-name='date']"),
+ document.querySelector(".o_datetime_picker"),
"date range picker should be opened"
);
});
QUnit.test(
"Datetime field manually input value should send utc value to server",
- async function (assert) {
+ async (assert) => {
assert.expect(4);
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
await makeView({
@@ -426,8 +368,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
`,
resId: 1,
mockRPC(route, args) {
@@ -450,11 +391,7 @@ QUnit.module("Fields", (hooks) => {
);
// update input for Datetime
- await editInput(
- target,
- ".o_field_daterange[name='datetime'] input",
- "02/08/2017 11:30:00"
- );
+ await editInput(target, "input[data-field=datetime]", "02/08/2017 11:30:00");
// save form
await clickSave(target);
@@ -466,115 +403,103 @@ QUnit.module("Fields", (hooks) => {
}
);
- QUnit.test(
- "Daterange field keyup should not erase end date [REQUIRE FOCUS]",
- async function (assert) {
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
- serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
+ QUnit.test("Daterange field keyup should not erase end date", async (assert) => {
+ serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
- await makeView({
- type: "form",
- resModel: "partner",
- serverData,
- arch: `
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ serverData,
+ arch: `
`,
- resId: 1,
- });
+ resId: 1,
+ });
- // check date display correctly in readonly
- assert.strictEqual(
- target.querySelector(".o_field_daterange input").value,
- "02/08/2017 15:30:00",
- "the start date should be correctly displayed"
- );
- assert.strictEqual(
- target.querySelectorAll(".o_field_daterange input")[1].value,
- "03/13/2017 05:30:00",
- "the end date should be correctly displayed"
- );
+ // check date display correctly in readonly
+ assert.strictEqual(
+ target.querySelector(".o_field_daterange input").value,
+ "02/08/2017 15:30:00",
+ "the start date should be correctly displayed"
+ );
+ assert.strictEqual(
+ target.querySelectorAll(".o_field_daterange input")[1].value,
+ "03/13/2017 05:30:00",
+ "the end date should be correctly displayed"
+ );
- // reveal the daterangepicker
- await click(target.querySelector(".o_field_daterange[name='datetime'] input"));
+ // reveal the o_datetime_picker
+ await click(target.querySelector("input[data-field=datetime]"));
- // the keyup event should not be handled by daterangepicker
- await triggerEvent(target, ".o_field_daterange[name='datetime'] input", "keyup", {
- key: "ArrowLeft",
- });
+ // the keyup event should not be handled by o_datetime_picker
+ await triggerEvent(target, "input[data-field=datetime]", "keyup", {
+ key: "ArrowLeft",
+ });
- const datepickers = document.querySelectorAll(`.daterangepicker`);
- // TODO: check if the range is correctly displayed in the picker
- await click(datepickers[1].querySelector(".applyBtn"));
+ assert.strictEqual(
+ target.querySelector(".o_field_daterange input").value,
+ "02/08/2017 15:30:00",
+ "the start date should be correctly displayed after onkeyup"
+ );
- assert.strictEqual(
- target.querySelector(".o_field_daterange input").value,
- "02/08/2017 15:30:00",
- "the start date should be correctly displayed after onkeyup"
- );
+ assert.strictEqual(
+ target.querySelectorAll(".o_field_daterange input")[1].value,
+ "03/13/2017 05:30:00",
+ "the end date should be correctly displayed after onkeyup"
+ );
+ });
- assert.strictEqual(
- target.querySelectorAll(".o_field_daterange input")[1].value,
- "03/13/2017 05:30:00",
- "the end date should be correctly displayed after onkeyup"
- );
- }
- );
+ // TODO: check if this test is still relevant
+ // QUnit.test(
+ // "DateRangeField manually input wrong value should show toaster",
+ // async (assert)=> {
+ // serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
+ // serverData.models.partner.records[0].date_end = "2017-02-08";
- QUnit.test(
- "DateRangeField manually input wrong value should show toaster",
- async function (assert) {
- serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
- serverData.models.partner.records[0].date_end = "2017-02-08";
+ // await makeView({
+ // type: "form",
+ // resModel: "partner",
+ // serverData,
+ // arch: `
+ // `,
+ // resId: 1,
+ // });
- await makeView({
- type: "form",
- resModel: "partner",
- serverData,
- arch: `
- `,
- resId: 1,
- });
+ // await editInput(target, "input[data-field=date]", "blabla");
+ // // click outside daterange field
+ // await click(target);
+ // assert.hasClass(
+ // target.querySelector(".o_field_daterange"),
+ // "o_field_invalid",
+ // "date field should be displayed as invalid"
+ // );
+ // // update input date with right value
+ // await editInput(target, "input[data-field=date]", "02/08/2017");
+ // assert.doesNotHaveClass(
+ // target.querySelector(".o_field_daterange[name='date']"),
+ // "o_field_invalid",
+ // "date field should not be displayed as invalid now"
+ // );
- await editInput(target, ".o_field_daterange[name='date'] input", "blabla");
- // click outside daterange field
- await click(target);
- assert.hasClass(
- target.querySelector(".o_field_daterange[name='date']"),
- "o_field_invalid",
- "date field should be displayed as invalid"
- );
- // update input date with right value
- await editInput(target, ".o_field_daterange[name='date'] input", "02/08/2017");
- assert.doesNotHaveClass(
- target.querySelector(".o_field_daterange[name='date']"),
- "o_field_invalid",
- "date field should not be displayed as invalid now"
- );
+ // // again enter wrong value and try to save should raise invalid fields value
+ // await editInput(target, "input[data-field=date]", "blabla");
+ // await clickSave(target);
+ // assert.strictEqual(
+ // target.querySelector(".o_notification_title").textContent,
+ // "Invalid fields: "
+ // );
+ // assert.strictEqual(
+ // target.querySelector(".o_notification_content").innerHTML,
+ // ""
+ // );
+ // assert.hasClass(target.querySelector(".o_notification"), "border-danger");
+ // }
+ // );
- // again enter wrong value and try to save should raise invalid fields value
- await editInput(target, ".o_field_daterange[name='date'] input", "blabla");
- await clickSave(target);
- assert.strictEqual(
- target.querySelector(".o_notification_title").textContent,
- "Invalid fields: "
- );
- assert.strictEqual(
- target.querySelector(".o_notification_content").innerHTML,
- ""
- );
- assert.hasClass(target.querySelector(".o_notification"), "border-danger");
- }
- );
-
- QUnit.test("Render with initial empty value: date field", async function (assert) {
+ QUnit.test("Render with initial empty value: date field", async (assert) => {
// 2014-08-14 12:34:56 -> the day E. Zuckerman, who invented pop-up ads, has apologised.
patchDate(2014, 7, 14, 12, 34, 56);
serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
@@ -585,24 +510,35 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
`,
});
- await click(document.body, ".o_field_daterange[name=date] input");
- assert.containsN(document.body, ".month", 2);
- assert.equal(document.body.querySelectorAll(".month")[0].textContent, "Aug 2014");
- assert.equal(document.body.querySelectorAll(".month")[1].textContent, "Sep 2014");
+ await click(target, "input[data-field=date]");
+ assert.containsOnce(target, ".o_datetime_picker", "check that the datepicker is opened");
+
+ // Select a value (today)
+ await click(target.querySelector(".o_today"));
+
+ assert.strictEqual(
+ target.querySelectorAll(".o_field_daterange input")[0].value,
+ "08/14/2014",
+ "start date should be set properly"
+ );
+
+ // Add an end date
+ await click(target.querySelector(".o_add_date"));
+
+ assert.strictEqual(
+ target.querySelectorAll(".o_field_daterange input")[0].value,
+ target.querySelectorAll(".o_field_daterange input")[1].value,
+ "the end date should be set to the same value as the start date"
+ );
});
- QUnit.test("Render with initial empty value: datetime field", async function (assert) {
+ QUnit.test("Render with initial empty value: datetime field", async (assert) => {
// 2014-08-14 12:34:56 -> the day E. Zuckerman, who invented pop-up ads, has apologised.
patchDate(2014, 7, 14, 12, 34, 56);
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
await makeView({
type: "form",
@@ -610,421 +546,197 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: `
`,
});
- await click(document.body, ".o_field_daterange[name=datetime] input");
- assert.containsN(document.body, ".month", 2);
- assert.equal(document.body.querySelectorAll(".month")[0].textContent, "Aug 2014");
- assert.equal(document.body.querySelectorAll(".month")[1].textContent, "Sep 2014");
+ await click(target, "input[data-field=datetime]");
+
+ assert.containsOnce(target, ".o_datetime_picker", "check that the datepicker is opened");
+ assert.containsNone(target, ".o_add_date");
+
+ // Select a value (today)
+ await click(target.querySelector(".o_today"));
+
+ assert.strictEqual(
+ target.querySelectorAll(".o_field_daterange input")[0].value,
+ "08/14/2014 12:00:00",
+ "start date should be set properly"
+ );
+ assert.notOk(isHiddenByCSS(target.querySelector(".o_add_date")));
+ assert.strictEqual(
+ target.querySelector(".o_add_date").innerText.trim().toLowerCase(),
+ "add end date"
+ );
+
+ // Add an end date
+ await click(target.querySelector(".o_add_date"));
+
+ const [startInput, endInput] = target.querySelectorAll(".o_field_daterange input");
+ assert.strictEqual(
+ startInput.value,
+ endInput.value,
+ "the end date should be set to the same value as the start date"
+ );
});
- QUnit.test("Datetime field with option format type is 'date'", async function (assert) {
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
+ QUnit.test("initial empty start date", async (assert) => {
+ // 2014-08-14 12:34:56 -> the day E. Zuckerman, who invented pop-up ads, has apologised.
+ patchDate(2014, 7, 14, 12, 34, 56);
+
+ serverData.models.partner.records[0].datetime = false;
serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
await makeView({
type: "form",
resModel: "partner",
- resId: 1,
serverData,
arch: `
`,
+ resId: 1,
});
+ assert.ok(isHiddenByCSS(target.querySelector(".o_add_date")));
+
+ target.querySelector(".o_field_daterange input").focus();
+ await nextTick();
+
+ assert.notOk(isHiddenByCSS(target.querySelector(".o_add_date")));
assert.strictEqual(
- target.querySelector(".o_field_daterange[name='datetime'] input").value,
- "02/08/2017",
- "the start date should only show date when option formatType is Date"
+ target.querySelector(".o_add_date").innerText.trim().toLowerCase(),
+ "add start date"
);
+
+ // Add an start date
+ await click(target.querySelector(".o_add_date"));
+
+ const [startInput, endInput] = target.querySelectorAll(".o_field_daterange input");
assert.strictEqual(
- target.querySelector(".o_field_daterange[name='datetime_end'] input").value,
- "03/13/2017",
- "the end date should only show date when option formatType is Date"
+ startInput.value,
+ endInput.value,
+ "the end date should be set to the same value as the start date"
);
});
- QUnit.test("when same locales have differences (moment vs luxon)", async function (assert) {
- patchWithCleanup(localization, { dateTimeFormat: "dd MMM, yyyy HH:mm:ss" });
-
- // Create a new pseudo locale that will get different from the luxon one
- const originalLocal = moment.locale();
- // Moment automatically assigns newly defined locales.
- moment.defineLocale("nonsenseForTest", {
- monthsShort: [
- "JaBon",
- "FeBob",
- "MaBor",
- "ApBor",
- "MaBoy",
- "JuBon",
- "JuBol",
- "AuBog",
- "SeBop",
- "OcBot",
- "NoBov",
- "DeBoc",
- ],
- });
-
- registerCleanup(() => {
- //Delete the previously created local
- moment.updateLocale("nonsenseForTest", null);
- // Restore the original locale
- moment.locale(originalLocal);
- });
-
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
+ QUnit.test("Datetime field - open datepicker and switch page", async (assert) => {
serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
+ serverData.models.partner.records.push({
+ id: 2,
+ date: "2017-03-04",
+ datetime: "2017-03-10 11:00:00",
+ datetime_end: "2017-04-15 00:00:00",
+ });
await makeView({
type: "form",
resModel: "partner",
resId: 1,
- serverData,
- arch: `
- `,
- });
-
- // open a datepicker field
- await click(target, ".o_field_daterange[name=datetime] input");
-
- const datepicker = document.querySelector(".daterangepicker[data-name=datetime]");
- assert.isVisible(datepicker, "date range picker should be opened");
- assert.deepEqual(
- [...datepicker.querySelectorAll(".month")].map((el) => el.textContent),
- ["FeBob 2017", "MaBor 2017"]
- );
-
- // Select a new range and check that inputs are updated
- await triggerEvent(datepicker, ".start-date", "mousedown"); // 02/08/2017
- await triggerEvent(datepicker, ".start-date + .available", "mousedown"); // 02/09/2017
- await click(datepicker, ".applyBtn");
- assert.equal(target.querySelector("[name=datetime] input").value, "08 Feb, 2017 15:30:00");
- assert.equal(
- target.querySelector("[name=datetime_end] input").value,
- "09 Feb, 2017 05:30:00"
- );
- });
-
- QUnit.test(
- "Datetime field - open datepicker and switch page [REQUIRE FOCUS]",
- async function (assert) {
- serverData.models.partner.fields.datetime_end = {
- string: "Datetime End",
- type: "datetime",
- };
- serverData.models.partner.records[0].datetime_end = "2017-03-13 00:00:00";
- serverData.models.partner.records.push({
- id: 2,
- date: "2017-03-04",
- datetime: "2017-03-10 11:00:00",
- datetime_end: "2017-04-15 00:00:00",
- });
-
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- resIds: [1, 2],
- serverData,
- arch: `
- `,
- });
-
- // Check date range picker initialization
- let datepickers = document.querySelectorAll(`.daterangepicker`);
- assert.isNotVisible(
- datepickers[0],
- "first date range picker should be closed initially"
- );
- assert.isNotVisible(
- datepickers[datepickers.length - 1],
- "second date range picker should be closed initially"
- );
-
- // open datepicker
- let fields = target.querySelectorAll(".o_field_daterange");
- await click(fields[0].querySelector("input"));
-
- let datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[0].getAttribute("name")}"]`
- );
- assert.isVisible(datepicker, "first date range picker should be opened");
-
- // Start date: id=1
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .active.start-date").textContent,
- "8",
- "active start date should be '8' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .hourselect").value,
- "15",
- "active start date hour should be '15' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .minuteselect").value,
- "30",
- "active start date minute should be '30' in date range picker"
- );
-
- // End date: id=1
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .active.end-date").textContent,
- "13",
- "active end date should be '13' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .hourselect").value,
- "5",
- "active end date hour should be '5' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .minuteselect").value,
- "30",
- "active end date minute should be '30' in date range picker"
- );
-
- // Close picker
- await click(datepicker.querySelector(".cancelBtn"));
- assert.strictEqual(
- datepicker.style.display,
- "none",
- "date range picker should be closed"
- );
-
- await pagerNext(target);
-
- // Check date range picker initialization
- assert.containsN(
- document.body,
- ".daterangepicker",
- 2,
- "should initialize 2 date range picker"
- );
- datepickers = document.querySelectorAll(`.daterangepicker`);
- assert.isNotVisible(
- datepickers[0],
- "first date range picker should be closed initially"
- );
- assert.isNotVisible(
- datepickers[datepickers.length - 1],
- "second date range picker should be closed initially"
- );
-
- // open date range picker
- fields = target.querySelectorAll(".o_field_daterange");
- await click(fields[0].querySelector("input"));
-
- datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[0].getAttribute("name")}"]`
- );
- assert.isVisible(datepicker, "first date range picker should be opened");
-
- // Start date: id=2
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .active.start-date").textContent,
- "10",
- "active start date should be '10' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .hourselect").value,
- "16",
- "active start date hour should be '16' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.left .minuteselect").value,
- "30",
- "active start date minute should be '30' in date range picker"
- );
-
- // End date id=2
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .active.end-date").textContent,
- "15",
- "active end date should be '15' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .hourselect").value,
- "5",
- "active end date hour should be '5' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".drp-calendar.right .minuteselect").value,
- "30",
- "active end date minute should be '30' in date range picker"
- );
- }
- );
-
- QUnit.test(
- "Render with one visible field: start date [REQUIRE FOCUS]",
- async function (assert) {
- serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
- serverData.models.partner.records[0].date_end = "2017-02-08";
-
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
- serverData,
- arch: `
- `,
- });
-
- let fields = target.querySelectorAll(".o_field_daterange");
- // Check date range picker initialization
- assert.containsOnce(
- document.body,
- ".daterangepicker",
- "should initialize 1 date range picker"
- );
-
- // open the start date
- await click(fields[0].querySelector("input"));
- const datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[0].getAttribute("name")}"]`
- );
-
- assert.isVisible(datepicker, "date range picker should be opened");
- assert.strictEqual(
- datepicker.querySelector(".active.start-date").textContent,
- "3",
- "active start date should be '3' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".active.end-date").textContent,
- "8",
- "active end date should be '8' in date range picker"
- );
-
- // Change date
- await triggerEvent(
- datepicker,
- ".drp-calendar.left .available[data-title='r2c4']",
- "mousedown"
- );
- await triggerEvent(
- datepicker,
- ".drp-calendar.right .available[data-title='r2c0']",
- "mousedown"
- );
- await click(datepicker.querySelector(".applyBtn"));
-
- // Check date after change
- assert.isNotVisible(datepicker, "date range picker should be closed");
- assert.strictEqual(
- fields[0].querySelector("input").value,
- "02/16/2017",
- "the date should be '02/16/2017'"
- );
-
- // Save
- await clickSave(target);
- fields = target.querySelectorAll(".o_field_daterange");
-
- // Check date after save
- assert.strictEqual(
- fields[0].querySelector("input").value,
- "02/16/2017",
- "the start date should be '02/16/2017' after save"
- );
- }
- );
-
- QUnit.test("Render with one visible field: end date [REQUIRE FOCUS]", async function (assert) {
- serverData.models.partner.fields.date_end = { string: "Date End", type: "date" };
- serverData.models.partner.records[0].date_end = "2017-02-08";
-
- await makeView({
- type: "form",
- resModel: "partner",
- resId: 1,
+ resIds: [1, 2],
serverData,
arch: `
`,
});
- let fields = target.querySelectorAll(".o_field_daterange");
// Check date range picker initialization
- assert.containsOnce(
- document.body,
- ".daterangepicker",
- "should initialize 1 date range picker"
- );
+ assert.containsOnce(getFixture(), ".o_field_daterange");
+ assert.containsNone(getFixture(), ".o_datetime_picker");
- // open the end date
- await click(fields[0].querySelector("input"));
- const datepicker = document.querySelector(
- `.daterangepicker[data-name="${fields[0].getAttribute("name")}"]`
- );
+ // open datepicker
+ await click(target.querySelector("input[data-field=datetime]"));
+ let datepicker = document.querySelector(".o_datetime_picker");
assert.isVisible(datepicker, "date range picker should be opened");
+
+ // Start date: id=1
assert.strictEqual(
- datepicker.querySelector(".active.start-date").textContent,
- "3",
- "active start date should be '3' in date range picker"
- );
- assert.strictEqual(
- datepicker.querySelector(".active.end-date").textContent,
+ datepicker.querySelector(".o_active_start").textContent,
"8",
- "active end date should be '8' in date range picker"
+ "active start date should be '8' in date range picker"
+ );
+ let [hourSelectStart, minuteSelectStart] = getTimePickers().at(0);
+ assert.strictEqual(
+ hourSelectStart.value,
+ "15",
+ "active start date hour should be '15' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectStart.value,
+ "30",
+ "active start date minute should be '30' in date range picker"
);
- // Change date
- await triggerEvent(
- datepicker,
- ".drp-calendar.left .available[data-title='r2c4']",
- "mousedown"
+ // End date: id=1
+ assert.strictEqual(
+ datepicker.querySelector(".o_active_end").textContent,
+ "13",
+ "active end date should be '13' in date range picker"
);
- await triggerEvent(
- datepicker,
- ".drp-calendar.right .available[data-title='r2c0']",
- "mousedown"
+ let [hourSelectEnd, minuteSelectEnd] = getTimePickers().at(1);
+ assert.strictEqual(
+ hourSelectEnd.value,
+ "5",
+ "active end date hour should be '5' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectEnd.value,
+ "30",
+ "active end date minute should be '30' in date range picker"
);
- await click(datepicker.querySelector(".applyBtn"));
- // Check date after change
+ // Close picker
+ await click(document.querySelector(".o_form_view"));
assert.isNotVisible(datepicker, "date range picker should be closed");
+
+ await pagerNext(target);
+
+ // Check date range picker initialization
+ assert.containsOnce(getFixture(), ".o_field_daterange");
+ assert.containsNone(getFixture(), ".o_datetime_picker");
+
+ // open date range picker
+ await click(target.querySelector("input[data-field=datetime]"));
+
+ datepicker = document.querySelector(".o_datetime_picker");
+ assert.isVisible(datepicker, "first date range picker should be opened");
+
+ // Start date: id=2
assert.strictEqual(
- fields[0].querySelector("input").value,
- "03/12/2017",
- "the date should be '03/12/2017'"
+ datepicker.querySelector(".o_active_start").textContent,
+ "10",
+ "active start date should be '10' in date range picker"
+ );
+ [hourSelectStart, minuteSelectStart] = getTimePickers().at(0);
+ assert.strictEqual(
+ hourSelectStart.value,
+ "16",
+ "active start date hour should be '16' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectStart.value,
+ "30",
+ "active start date minute should be '30' in date range picker"
);
- // Save
- await clickSave(target);
- fields = target.querySelectorAll(".o_field_daterange");
-
- // Check date after save
+ // End date id=2
assert.strictEqual(
- fields[0].querySelector("input").value,
- "03/12/2017",
- "the end date should be '02/13/2017' after save"
+ datepicker.querySelector(".o_active_end").textContent,
+ "15",
+ "active end date should be '15' in date range picker"
+ );
+ [hourSelectEnd, minuteSelectEnd] = getTimePickers().at(1);
+ assert.strictEqual(
+ hourSelectEnd.value,
+ "5",
+ "active end date hour should be '5' in date range picker"
+ );
+ assert.strictEqual(
+ minuteSelectEnd.value,
+ "30",
+ "active end date minute should be '30' in date range picker"
);
});
});
diff --git a/addons/web/static/tests/views/fields/datetime_field_tests.js b/addons/web/static/tests/views/fields/datetime_field_tests.js
index 1f6b8ef478b..0203d1a9703 100644
--- a/addons/web/static/tests/views/fields/datetime_field_tests.js
+++ b/addons/web/static/tests/views/fields/datetime_field_tests.js
@@ -1,17 +1,24 @@
/** @odoo-module **/
-import { registry } from "@web/core/registry";
+import {
+ getPickerApplyButton,
+ getPickerCell,
+ getTimePickers,
+ zoomOut,
+} from "@web/../tests/core/datetime/datetime_test_helpers";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import {
click,
clickSave,
editInput,
+ editSelect,
getFixture,
patchTimeZone,
triggerEvent,
triggerEvents,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+import { registry } from "@web/core/registry";
let serverData;
let target;
@@ -55,7 +62,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.module("DatetimeField");
- QUnit.test("DatetimeField in form view", async function (assert) {
+ QUnit.test("DatetimeField in form view", async (assert) => {
patchTimeZone(120);
await makeView({
@@ -70,56 +77,32 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_field_datetime input").value,
expectedDateString,
- "the datetime should be correctly displayed in readonly"
- );
-
- assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
- expectedDateString,
- "the datetime should be correct in edit mode"
+ "the datetime should be correctly displayed"
);
// datepicker should not open on focus
- assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
+ assert.containsNone(target, ".o_datetime_picker");
- await click(target, ".o_datepicker_input");
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
+ await click(target, ".o_field_datetime input");
+ assert.containsOnce(target, ".o_datetime_picker");
- // select 22 February at 8:25:35
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]);
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .day[data-day*='/22/']")
- );
- await click(document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o"));
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]);
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]);
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]);
+ // select 22 April 2018 at 8:25
+ await zoomOut();
+ await zoomOut();
+ await click(getPickerCell("2018"));
+ await click(getPickerCell("Apr"));
+ await click(getPickerCell("22"));
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "8");
+ await editSelect(minuteSelect, null, "25");
+ // Close the datepicker
+ await click(target, ".o_form_view_container");
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be closed"
- );
+ assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
- const newExpectedDateString = "04/22/2017 08:25:35";
+ const newExpectedDateString = "04/22/2018 08:25:00";
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_datetime input").value,
newExpectedDateString,
"the selected date should be displayed in the input"
);
@@ -134,8 +117,8 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
- "DatetimeField does not trigger fieldChange before datetime completly picked",
- async function (assert) {
+ "DatetimeField only triggers fieldChange when a day is picked and when an hour/minute is selected",
+ async (assert) => {
patchTimeZone(120);
serverData.models.partner.onchanges = {
@@ -154,60 +137,38 @@ QUnit.module("Fields", (hooks) => {
},
});
- await click(target, ".o_datepicker_input");
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
+ await click(target, ".o_field_datetime input");
- // select a date and time
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]
- );
- await click(
- document.body.querySelector(
- ".bootstrap-datetimepicker-widget .day[data-day*='/22/']"
- )
- );
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o")
- );
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]
- );
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]
- );
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
- );
- assert.verifySteps([], "should not have done any onchange yet");
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]
- );
+ assert.containsOnce(target, ".o_datetime_picker");
- assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
+ // select 22 April 2018 at 8:25
+ await zoomOut();
+ await zoomOut();
+ await click(getPickerCell("2018"));
+ await click(getPickerCell("Apr"));
+ await click(getPickerCell("22"));
+
+ assert.verifySteps([]);
+
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "8");
+ await editSelect(minuteSelect, null, "25");
+
+ assert.verifySteps([]);
+
+ // Close the datepicker
+ await click(target);
+
+ assert.containsNone(target, ".o_datetime_picker");
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
- "04/22/2017 08:25:35"
+ target.querySelector(".o_field_datetime input").value,
+ "04/22/2018 08:25:00"
);
- assert.verifySteps(["onchange"], "should have done only one onchange");
+ assert.verifySteps(["onchange"]);
}
);
- QUnit.test("DatetimeField with datetime formatted without second", async function (assert) {
+ QUnit.test("DatetimeField with datetime formatted without second", async (assert) => {
patchTimeZone(0);
serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
@@ -234,14 +195,13 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_field_datetime input").value,
expectedDateString,
- "the datetime should be correctly displayed in readonly"
+ "the datetime should be correctly displayed"
);
await click(target, ".o_form_button_cancel");
- assert.containsNone(document.body, ".modal", "there should not be a Warning dialog");
+ assert.containsNone(target, ".modal", "there should not be a Warning dialog");
});
-
- QUnit.test("DatetimeField in editable list view", async function (assert) {
+ QUnit.test("DatetimeField in editable list view", async (assert) => {
patchTimeZone(120);
await makeView({
@@ -256,69 +216,51 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
cell.textContent,
expectedDateString,
- "the datetime should be correctly displayed in readonly"
+ "the datetime should be correctly displayed"
);
// switch to edit mode
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.containsOnce(
target,
- "input.o_datepicker_input",
+ ".o_field_datetime input",
"the view should have a date input for editable mode"
);
assert.strictEqual(
- target.querySelector("input.o_datepicker_input"),
+ target.querySelector(".o_field_datetime input"),
document.activeElement,
"date input should have the focus"
);
assert.strictEqual(
- target.querySelector("input.o_datepicker_input").value,
+ target.querySelector(".o_field_datetime input").value,
expectedDateString,
"the date should be correct in edit mode"
);
- assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
- await click(target, ".o_datepicker_input");
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
+ assert.containsNone(target, ".o_datetime_picker");
+ await click(target, ".o_field_datetime input");
+ assert.containsOnce(target, ".o_datetime_picker");
- // select 22 February at 8:25:35
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[0]
- );
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")[8]);
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[3]);
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .day[data-day*='/22/']")
- );
- await click(document.body.querySelector(".bootstrap-datetimepicker-widget .fa-clock-o"));
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-hour")
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .hour")[8]);
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-minute")
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .minute")[5]);
- await click(
- document.body.querySelector(".bootstrap-datetimepicker-widget .timepicker-second")
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .second")[7]);
+ // select 22 April 2018 at 8:25
+ await zoomOut();
+ await zoomOut();
+ await click(getPickerCell("2018"));
+ await click(getPickerCell("Apr"));
+ await click(getPickerCell("22"));
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "8");
+ await editSelect(minuteSelect, null, "25");
+ // Apply changes
+ await click(getPickerApplyButton());
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be closed"
- );
+ assert.containsNone(target, ".o_datetime_picker", "datepicker should be closed");
- const newExpectedDateString = "04/22/2017 08:25:35";
+ const newExpectedDateString = "04/22/2018 08:25:00";
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_datetime input").value,
newExpectedDateString,
- "the selected datetime should be displayed in the input"
+ "the date should be updated in the input"
);
// save
@@ -331,8 +273,8 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
- "multi edition of DatetimeField in list view: edit date in input",
- async function (assert) {
+ "multi edition of DatetimeField in list view: edit date in input [REQUIRE FOCUS]",
+ async (assert) => {
await makeView({
serverData,
type: "list",
@@ -348,10 +290,12 @@ QUnit.module("Fields", (hooks) => {
await click(rows[0], ".o_data_cell");
- assert.containsOnce(target, "input.o_datepicker_input");
- await editInput(target, ".o_datepicker_input", "10/02/2019 09:00:00");
+ assert.containsOnce(target, ".o_field_datetime input");
+
+ await editInput(target, ".o_field_datetime input", "10/02/2019 09:00:00");
+
+ assert.containsOnce(target, ".modal");
- assert.containsOnce(document.body, ".modal");
await click(target.querySelector(".modal .modal-footer .btn-primary"));
assert.strictEqual(
@@ -366,8 +310,8 @@ QUnit.module("Fields", (hooks) => {
);
QUnit.test(
- "multi edition of DatetimeField in list view: clear date in input",
- async function (assert) {
+ "multi edition of DatetimeField in list view: clear date in input [REQUIRE FOCUS]",
+ async (assert) => {
serverData.models.partner.records[1].datetime = "2017-02-08 10:00:00";
await makeView({
@@ -385,10 +329,12 @@ QUnit.module("Fields", (hooks) => {
await click(rows[0], ".o_data_cell");
- assert.containsOnce(target, "input.o_datepicker_input");
- await editInput(target, ".o_datepicker_input", "");
+ assert.containsOnce(target, ".o_field_datetime input");
+
+ await editInput(target, ".o_field_datetime input", "");
+
+ assert.containsOnce(target, ".modal");
- assert.containsOnce(document.body, ".modal");
await click(target, ".modal .modal-footer .btn-primary");
assert.strictEqual(
@@ -402,7 +348,7 @@ QUnit.module("Fields", (hooks) => {
}
);
- QUnit.test("DatetimeField remove value", async function (assert) {
+ QUnit.test("DatetimeField remove value", async (assert) => {
assert.expect(4);
patchTimeZone(120);
@@ -425,16 +371,16 @@ QUnit.module("Fields", (hooks) => {
});
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_datetime input").value,
"02/08/2017 12:00:00",
"the date time should be correct in edit mode"
);
- const input = target.querySelector(".o_datepicker_input");
+ const input = target.querySelector(".o_field_datetime input");
input.value = "";
await triggerEvents(input, null, ["input", "change", "focusout"]);
assert.strictEqual(
- target.querySelector(".o_datepicker_input").value,
+ target.querySelector(".o_field_datetime input").value,
"",
"should have an empty input"
);
@@ -449,8 +395,8 @@ QUnit.module("Fields", (hooks) => {
});
QUnit.test(
- "DatetimeField with date/datetime widget (with day change)",
- async function (assert) {
+ "DatetimeField with date/datetime widget (with day change) does not care about widget",
+ async (assert) => {
patchTimeZone(-240);
serverData.models.partner.records[0].p = [2];
@@ -484,16 +430,16 @@ QUnit.module("Fields", (hooks) => {
// switch to form view
await click(target, ".o_field_widget[name='p'] .o_data_cell");
assert.strictEqual(
- document.body.querySelector(".modal .o_field_date[name='datetime'] input").value,
- "02/07/2017",
+ target.querySelector(".modal .o_field_date[name='datetime'] input").value,
+ "02/07/2017 22:00:00",
"the datetime (date widget) should be correctly displayed in form view"
);
}
);
QUnit.test(
- "DatetimeField with date/datetime widget (without day change)",
- async function (assert) {
+ "DatetimeField with date/datetime widget (without day change) does not care about widget",
+ async (assert) => {
patchTimeZone(-240);
serverData.models.partner.records[0].p = [2];
@@ -527,44 +473,14 @@ QUnit.module("Fields", (hooks) => {
// switch to form view
await click(target, ".o_field_widget[name='p'] .o_data_cell");
assert.strictEqual(
- document.body.querySelector(".modal .o_field_date[name='datetime'] input").value,
- "02/08/2017",
+ target.querySelector(".modal .o_field_date[name='datetime'] input").value,
+ "02/08/2017 06:00:00",
"the datetime (date widget) should be correctly displayed in form view"
);
}
);
- QUnit.test("datepicker option: daysOfWeekDisabled", async function (assert) {
- serverData.models.partner.fields.datetime.default = "2017-08-02 12:00:05";
- serverData.models.partner.fields.datetime.required = true;
-
- await makeView({
- type: "form",
- resModel: "partner",
- serverData,
- arch: `
- `,
- });
-
- await click(target, ".o_datepicker_input");
-
- for (const el of document.body.querySelectorAll(".day:nth-child(2), .day:last-child")) {
- assert.hasClass(el, "disabled", "first and last days must be disabled");
- }
-
- // the assertions below could be replaced by a single hasClass classic on the jQuery set using the idea
- // All not <=> not Exists. But we want to be sure that the set is non empty. We don't have an helper
- // function for that.
- for (const el of document.body.querySelectorAll(
- ".day:not(:nth-child(2)):not(:last-child)"
- )) {
- assert.doesNotHaveClass(el, "disabled", "other days must stay clickable");
- }
- });
-
- QUnit.test("datetime field: hit enter should update value", async function (assert) {
+ QUnit.test("datetime field: hit enter should update value", async (assert) => {
// This test verifies that the field datetime is correctly computed when:
// - we press enter to validate our entry
// - we click outside the field to validate our entry
@@ -609,34 +525,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(value, datetimeValue);
});
- QUnit.test(
- "datetime field with date widget: hit enter should update value",
- async function (assert) {
- /**
- * Don't think this test is usefull in the new system.
- */
- patchTimeZone(120);
-
- await makeView({
- serverData,
- type: "form",
- resModel: "partner",
- arch: '',
- resId: 1,
- });
-
- await editInput(target, ".o_field_widget input", "01/08/22");
- await triggerEvent(target, ".o_field_widget input", "keydown", { key: "Enter" });
-
- assert.strictEqual(target.querySelector(".o_field_widget input").value, "01/08/2022");
-
- // Click outside the field to check that the field is not changed
- await clickSave(target);
- assert.strictEqual(target.querySelector(".o_field_widget input").value, "01/08/2022");
- }
- );
-
- QUnit.test("DateTimeField with label opens datepicker on click", async function (assert) {
+ QUnit.test("DateTimeField with label opens datepicker on click", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
@@ -650,10 +539,6 @@ QUnit.module("Fields", (hooks) => {
});
await click(target.querySelector("label.o_form_label"));
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
});
});
diff --git a/addons/web/static/tests/views/fields/domain_field_tests.js b/addons/web/static/tests/views/fields/domain_field_tests.js
index 083f4e74c95..f8cd180964a 100644
--- a/addons/web/static/tests/views/fields/domain_field_tests.js
+++ b/addons/web/static/tests/views/fields/domain_field_tests.js
@@ -15,6 +15,7 @@ import {
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { fieldService } from "@web/core/field_service";
import { registry } from "@web/core/registry";
+import { getPickerCell } from "../../core/datetime/datetime_test_helpers";
let serverData;
let target;
@@ -145,26 +146,18 @@ QUnit.module("Fields", (hooks) => {
});
// The input field should display that the date is invalid
- assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime");
+ assert.equal(target.querySelector(".o_datetime_input").value, "Invalid DateTime");
// Change the date in the datepicker
- await click(target, ".o_datepicker_input");
+ await click(target, ".o_datetime_input");
// Select a date in the datepicker
- await click(
- document.body.querySelector(
- `.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]`
- )
- );
+ await click(getPickerCell("15"));
// Close the datepicker
- await click(
- document.body.querySelector(
- `.bootstrap-datetimepicker-widget a[data-action="close"]`
- )
- );
+ await click(target);
await clickDiscard(target);
// Open the datepicker again
- await click(target, ".o_datepicker_input");
+ await click(target, ".o_datetime_input");
}
);
@@ -825,19 +818,19 @@ QUnit.module("Fields", (hooks) => {
assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
- assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
+ assert.containsOnce(target, ".o_datetime_input", "there should be a datepicker input");
// Open and close the datepicker
- await click(target, ".o_datepicker_input");
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
+ await click(target, ".o_datetime_input");
+ assert.containsOnce(target, ".o_datetime_picker");
await triggerEvent(window, null, "scroll");
- assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
+ assert.containsOnce(target, ".o_datetime_picker");
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
assert.verifySteps([]);
// Manually input a date
rawDomain = `[("date", ">=", "2020-09-09")]`;
- await editInput(target, ".o_datepicker_input", "09/09/2020");
+ await editInput(target, ".o_datetime_input", "09/09/2020");
assert.verifySteps(["search_count"]);
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
diff --git a/addons/web/static/tests/views/fields/one2many_field_tests.js b/addons/web/static/tests/views/fields/one2many_field_tests.js
index c210296c628..74aa2994d0e 100644
--- a/addons/web/static/tests/views/fields/one2many_field_tests.js
+++ b/addons/web/static/tests/views/fields/one2many_field_tests.js
@@ -1,14 +1,15 @@
/** @odoo-module **/
+import { registerCleanup } from "@web/../tests/helpers/cleanup";
import {
addRow,
click,
clickCreate,
clickDiscard,
- clickSave,
clickM2OHighlightedItem,
- clickOpenedDropdownItem,
clickOpenM2ODropdown,
+ clickOpenedDropdownItem,
+ clickSave,
dragAndDrop,
editInput,
getFixture,
@@ -22,17 +23,17 @@ import {
triggerEvents,
triggerHotkey,
} from "@web/../tests/helpers/utils";
-import BasicModel from "web.BasicModel";
-import { browser } from "@web/core/browser/browser";
-import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
-import { getNextTabableElement } from "@web/core/utils/ui";
import { makeView, makeViewInDialog, setupViewRegistries } from "@web/../tests/views/helpers";
-import { registerCleanup } from "@web/../tests/helpers/cleanup";
+import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
+import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
+import { getNextTabableElement } from "@web/core/utils/ui";
import { session } from "@web/session";
import { RelationalModel } from "@web/views/relational_model";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { rpcService } from "@web/core/network/rpc_service";
+import BasicModel from "web.BasicModel";
+import { getPickerCell } from "../../core/datetime/datetime_test_helpers";
let serverData;
let target;
@@ -4395,19 +4396,8 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(td.textContent, "01/25/2017");
await click(td);
- await click(target.querySelector(".o_datepicker_input"));
- await nextTick();
- await click(document.body.querySelector(".bootstrap-datetimepicker-widget .picker-switch"));
- await click(
- document.body.querySelectorAll(".bootstrap-datetimepicker-widget .picker-switch")[1]
- );
- await click(
- [...document.body.querySelectorAll(".bootstrap-datetimepicker-widget .year")].filter(
- (el) => el.textContent === "2017"
- )[0]
- );
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .month")[1]);
- await click(document.body.querySelectorAll(".bootstrap-datetimepicker-widget .day")[22]);
+ await nextTick(); // wait for picker to open
+ await click(getPickerCell("1").at(0));
await clickSave(target);
assert.verifySteps(["get_views", "read", "read", "onchange", "write", "read", "read"]);
@@ -7003,7 +6993,7 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_data_row td:first-child");
assert.containsOnce(target, ".o_selected_row", "should have selected row");
- const { keydownEvent } = triggerHotkey("Shift+Tab");
+ const { keydownEvent } = await triggerHotkey("Shift+Tab");
await nextTick();
assert.containsNone(target, ".o_selected_row", "list should not be in edition");
// We also check the event is not default prevented, to make sure that the
@@ -7462,8 +7452,7 @@ QUnit.module("Fields", (hooks) => {
await addRow(target);
await editInput(target, ".o_data_row .o_field_widget input", "a name");
def = makeDeferred();
- await click(target, ".o_datepicker_input");
- await editInput(target, ".o_datepicker_input", "04/27/2022 14:08:52");
+ await editInput(target, ".o_field_datetime .o_input", "04/27/2022 14:08:52");
// resolve the onchange promise
def.resolve();
@@ -13093,7 +13082,7 @@ QUnit.module("Fields", (hooks) => {
if (method === "onchange") {
return {
value: {
- line_ids: [[0, 0, {date: "2020-01-01"}]],
+ line_ids: [[0, 0, { date: "2020-01-01" }]],
},
};
}
@@ -13109,7 +13098,7 @@ QUnit.module("Fields", (hooks) => {
rootType: "record",
activeFields: serverData.models.turlututu.fields,
},
- env.services,
+ env.services
);
await model.load({
@@ -13131,7 +13120,6 @@ QUnit.module("Fields", (hooks) => {
// The one2many orm commands should be parsed correctly and then, the date value is now a Datetime
// and not no longer a string.
const record = model.root.data.line_ids.records[0];
- assert.strictEqual(typeof(record.data.date), "object");
+ assert.strictEqual(typeof record.data.date, "object");
});
-
});
diff --git a/addons/web/static/tests/views/fields/properties_field_tests.js b/addons/web/static/tests/views/fields/properties_field_tests.js
index 96d5534be3a..753a131ce30 100644
--- a/addons/web/static/tests/views/fields/properties_field_tests.js
+++ b/addons/web/static/tests/views/fields/properties_field_tests.js
@@ -5,6 +5,7 @@ import {
clickDiscard,
clickSave,
editInput,
+ editSelect,
getFixture,
nextTick,
patchWithCleanup,
@@ -16,6 +17,11 @@ import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog
import { browser } from "@web/core/browser/browser";
import { fieldService } from "@web/core/field_service";
import { registry } from "@web/core/registry";
+import {
+ getPickerApplyButton,
+ getPickerCell,
+ getTimePickers,
+} from "../../core/datetime/datetime_test_helpers";
let serverData;
let target;
@@ -333,15 +339,16 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(type.value, "Date & Time", "Should have changed the property type");
// Choosing a date in the date picker should not close the definition popover
- await click(target, ".o_field_property_definition_value .o_datepicker_input");
- await click(document, ".datepicker-days tr:first-child .day:nth-child(3)");
- assert.ok(
- document.querySelector(".picker-switch .fa-check"),
- "Should not close the definition popover after selecting a date"
- );
- await click(document, ".picker-switch .fa-check");
- assert.ok(
- target.querySelector(".o_property_field_popover"),
+ await click(target, ".o_field_property_definition_value .o_datetime_input");
+ await click(getPickerCell("3").at(0));
+
+ assert.containsOnce(target, ".o_datetime_picker");
+
+ await click(getPickerApplyButton());
+
+ assert.containsOnce(
+ target,
+ ".o_property_field_popover",
"Should not close the definition popover after selecting a date"
);
@@ -354,7 +361,7 @@ QUnit.module("Fields", (hooks) => {
"My Datetime",
"Should have updated the property label"
);
- const datetimeComponent = field.querySelector(".o_property_field_value .o_datepicker");
+ const datetimeComponent = field.querySelector(".o_property_field_value .o_datetime_input");
assert.ok(datetimeComponent, "Should have changed the property type");
// Check that the value is reset (because the type changed)
@@ -1173,7 +1180,7 @@ QUnit.module("Fields", (hooks) => {
name: "property_2",
string: "My DateTime",
type: "datetime",
- value: "2018-12-31 11:01:01",
+ value: "2018-12-31 11:05:00",
},
]);
}
@@ -1193,20 +1200,18 @@ QUnit.module("Fields", (hooks) => {
// edit date property
await click(target, ".o_property_field[property-name=property_1] input");
- await click(document.body, ".datepicker [data-day='12/31/2018']");
+ await click(getPickerCell("31").at(0));
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]");
+ await click(getPickerCell("31").at(0));
+ const [hourSelect, minuteSelect] = getTimePickers().at(0);
+ await editSelect(hourSelect, null, "12");
+ await editSelect(minuteSelect, null, "5");
assert.equal(
target.querySelector("[property-name=property_2] input").value,
- "12/31/2018 12:01:01"
+ "12/31/2018 12:05:00"
);
// save
diff --git a/addons/web/static/tests/views/fields/remaining_days_field_tests.js b/addons/web/static/tests/views/fields/remaining_days_field_tests.js
index 890851d6cb2..558304e47f7 100644
--- a/addons/web/static/tests/views/fields/remaining_days_field_tests.js
+++ b/addons/web/static/tests/views/fields/remaining_days_field_tests.js
@@ -8,6 +8,7 @@ import {
patchTimeZone,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+import { getPickerCell } from "../../core/datetime/datetime_test_helpers";
let serverData;
let target;
@@ -116,22 +117,18 @@ QUnit.module("Fields", (hooks) => {
await click(rows[1], ".o_list_record_selector input");
await click(rows[0], ".o_data_cell");
- assert.containsOnce(
- target,
- "input.o_datepicker_input",
- "should have date picker input"
- );
+ assert.containsOnce(target, ".o_field_remaining_days input");
- await editInput(target, ".o_datepicker_input", "10/10/2017");
+ await editInput(target, ".o_field_remaining_days input", "10/10/2017");
await click(target);
- assert.containsOnce(document.body, ".modal");
+ assert.containsOnce(target, ".modal");
assert.strictEqual(
document.querySelector(".modal .o_field_widget").textContent,
"In 2 days",
"should have 'In 2 days' value to change"
);
- await click(document.body, ".modal .modal-footer .btn-primary");
+ await click(target, ".modal .modal-footer .btn-primary");
assert.strictEqual(
rows[0].querySelector(".o_data_cell").textContent,
@@ -177,15 +174,11 @@ QUnit.module("Fields", (hooks) => {
await click(rows[1], ".o_list_record_selector input");
await click(rows[0], ".o_data_cell");
- assert.containsOnce(
- target,
- "input.o_datepicker_input",
- "should have date picker input"
- );
+ assert.containsOnce(target, ".o_field_remaining_days input");
- await editInput(target, ".o_datepicker_input", "blabla");
+ await editInput(target, ".o_field_remaining_days input", "blabla");
await click(target);
- assert.containsNone(document.body, ".modal");
+ assert.containsNone(target, ".modal");
assert.strictEqual(cells[0].textContent, "Today");
assert.strictEqual(cells[1].textContent, "Tomorrow");
}
@@ -211,16 +204,12 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/08/2017");
assert.containsOnce(target, ".o_form_editable");
- assert.containsOnce(target, "div.o_field_widget[name='date'] .o_datepicker");
+ assert.containsOnce(target, "div.o_field_widget[name='date'] input");
- await click(target.querySelector(".o_datepicker .o_datepicker_input"));
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
+ await click(target, ".o_field_remaining_days input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
- await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
+ await click(getPickerCell("9").at(0));
await click(target, ".o_form_button_save");
assert.strictEqual(target.querySelector(".o_field_widget input").value, "10/09/2017");
});
@@ -238,12 +227,9 @@ QUnit.module("Fields", (hooks) => {
`,
});
- assert.containsOnce(
- target,
- ".o_form_editable .o_field_widget[name='date'] .o_datepicker"
- );
- await click(target.querySelector(".o_field_widget[name='date'] .o_datepicker input"));
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
+ assert.containsOnce(target, ".o_form_editable .o_field_widget[name='date'] input");
+ await click(target, ".o_field_widget[name='date'] input");
+ assert.containsOnce(target, ".o_datetime_picker");
}
);
@@ -303,17 +289,12 @@ QUnit.module("Fields", (hooks) => {
target.querySelector(".o_field_widget input").value,
"10/08/2017 11:00:00"
);
- assert.containsOnce(target, "div.o_field_widget[name='datetime'] .o_datepicker");
+ assert.containsOnce(target, "div.o_field_widget[name='datetime'] input");
- await click(target.querySelector(".o_datepicker .o_datepicker_input"));
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
+ await click(target, ".o_field_widget input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
- await click(document.body, ".bootstrap-datetimepicker-widget .day[data-day='10/09/2017']");
- await click(document.body, "a[data-action='close']");
+ await click(getPickerCell("9").at(0));
await click(target, ".o_form_button_save");
assert.strictEqual(
target.querySelector(".o_field_widget input").value,
diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js
index 6b6d9b6cacb..177f6d66b9a 100644
--- a/addons/web/static/tests/views/list_view_tests.js
+++ b/addons/web/static/tests/views/list_view_tests.js
@@ -1,5 +1,6 @@
/** @odoo-module **/
+import { Component, onWillStart, xml } from "@odoo/owl";
import { browser } from "@web/core/browser/browser";
import { Domain } from "@web/core/domain";
import { errorService } from "@web/core/errors/error_service";
@@ -10,18 +11,19 @@ import { uiService } from "@web/core/ui/ui_service";
import { getNextTabableElement } from "@web/core/utils/ui";
import { session } from "@web/session";
import { FloatField } from "@web/views/fields/float/float_field";
+import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { textField } from "@web/views/fields/text/text_field";
import { ListController } from "@web/views/list/list_controller";
-import { DynamicRecordList, DynamicGroupList } from "@web/views/relational_model";
+import { DynamicGroupList, DynamicRecordList } from "@web/views/relational_model";
import { actionService } from "@web/webclient/actions/action_service";
+import { getPickerApplyButton, getPickerCell } from "../core/datetime/datetime_test_helpers";
import { makeFakeLocalizationService, makeFakeUserService } from "../helpers/mock_services";
-import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import {
addRow,
click,
clickDiscard,
- clickOpenedDropdownItem,
clickOpenM2ODropdown,
+ clickOpenedDropdownItem,
clickSave,
drag,
dragAndDrop,
@@ -63,8 +65,6 @@ import {
import { createWebClient, doAction, loadState } from "../webclient/helpers";
import { makeView, makeViewInDialog, setupViewRegistries } from "./helpers";
-import { Component, onWillStart, xml } from "@odoo/owl";
-
const fieldRegistry = registry.category("fields");
const serviceRegistry = registry.category("services");
@@ -1611,13 +1611,8 @@ QUnit.module("Views", (hooks) => {
await click(target.querySelector(".o_data_cell"));
assert.containsOnce(target, ".o_selected_row");
- await click(target, ".o_datepicker input");
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
- triggerHotkey("Escape");
- await nextTick();
-
- assert.containsOnce(target, ".o_selected_row");
- assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
+ await click(target, ".o_field_date input");
+ assert.containsOnce(target, ".o_datetime_picker");
triggerHotkey("Escape");
await nextTick();
@@ -1639,20 +1634,8 @@ QUnit.module("Views", (hooks) => {
await click(target.querySelector(".o_list_button_add"));
assert.containsOnce(target, ".o_selected_row");
- await click(target, ".o_datepicker input");
- assert.containsOnce(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "datepicker should be opened"
- );
- await triggerEvent(document.activeElement, null, "keydown", { key: "Escape" });
-
- assert.containsOnce(target, ".o_selected_row", "the row is still in edition");
- assert.containsNone(
- document.body,
- ".bootstrap-datetimepicker-widget",
- "the datepicker is no longer visible"
- );
+ await click(target, ".o_field_date input");
+ assert.containsOnce(target, ".o_datetime_picker", "datepicker should be opened");
await triggerEvent(document.activeElement, null, "keydown", { key: "Escape" });
assert.containsNone(target, ".o_selected_row", "the row is no longer in edition");
@@ -7961,13 +7944,11 @@ QUnit.module("Views", (hooks) => {
document.activeElement,
target.querySelector(".o_field_widget[name=date] input")
);
- assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
+ assert.containsOnce(target, ".o_datetime_picker");
- await click(
- document.body,
- ".bootstrap-datetimepicker-widget [data-action=selectDay][data-day='02/15/2017']"
- );
- assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
+ await click(getPickerCell("15"));
+
+ assert.containsNone(target, ".o_datetime_picker");
assert.strictEqual(
target.querySelector(".o_field_widget[name=date] input").value,
"02/15/2017"
@@ -7976,8 +7957,6 @@ QUnit.module("Views", (hooks) => {
document.activeElement,
target.querySelector(".o_field_widget[name=date] input")
);
- assert.strictEqual(document.activeElement.selectionStart, 0);
- assert.strictEqual(document.activeElement.selectionEnd, 10);
});
QUnit.test("click on a button in a list view", async function (assert) {
@@ -10705,8 +10684,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
-
-
+
`,
mockRPC(route, args) {
if (args.method === "write") {
@@ -10720,23 +10698,14 @@ QUnit.module("Views", (hooks) => {
await click(target.querySelector(".o_list_record_selector input"));
await click(target.querySelector(".o_data_row .o_data_cell")); // edit first row
await click(target.querySelector(".o_data_row .o_data_cell .o_field_daterange input"));
+
// change dates via the daterangepicker
- const datepicker = document.querySelector(`.daterangepicker[data-name="date_start"]`);
- await triggerEvent(
- datepicker,
- ".drp-calendar.left .available[data-title='r3c1']",
- "mousedown"
- );
- await triggerEvent(
- datepicker,
- ".drp-calendar.right .available[data-title='r2c0']",
- "mousedown"
- );
- const applyBtn = datepicker.querySelector(".applyBtn");
- assert.notOk(applyBtn.disabled);
+ await click(getPickerCell("16").at(0));
+ await click(getPickerCell("12").at(1));
+ assert.notOk(getPickerApplyButton().disabled);
// Apply the changes
- await click(applyBtn);
+ await click(getPickerApplyButton());
assert.containsOnce(
target,
".modal",
@@ -10785,8 +10754,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
-
-
+
`,
mockRPC(route, args) {
if (args.method === "write") {
@@ -10802,9 +10770,11 @@ QUnit.module("Views", (hooks) => {
// Change the date in the first datetime
await editInput(
target,
- ".o_data_row .o_data_cell .o_field_daterange[name='date_start'] input",
+ ".o_data_row .o_data_cell .o_field_daterange[name='date_start'] input[data-field='date_start']",
"2021-04-01 11:00:00"
);
+ triggerHotkey("Enter");
+ await nextTick();
assert.containsOnce(
target,
".modal",
@@ -17867,8 +17837,8 @@ QUnit.module("Views", (hooks) => {
assert.containsN(target, ".o_field_cell.o_date_cell", 3);
await click(target.querySelector(".o_field_cell.o_date_cell"));
- await click(target, ".o_datepicker input");
- await click(document.querySelector(".datepicker-days td[data-day='12/19/2022']"));
+ await click(target, ".o_field_date input");
+ await click(getPickerCell("19"));
await clickSave(target);
assert.strictEqual(
@@ -17926,9 +17896,8 @@ QUnit.module("Views", (hooks) => {
assert.containsN(target, ".o_field_cell.o_datetime_cell", 3);
await click(target.querySelector(".o_field_cell.o_datetime_cell"));
- await click(target, ".o_datepicker input");
- await click(document.querySelector(".datepicker-days td[data-day='12/19/2022']"));
- await click(document, ".bootstrap-datetimepicker-widget a[data-action='close']");
+ await click(target, ".o_field_datetime input");
+ await click(getPickerCell("19"));
await clickSave(target);
assert.strictEqual(
diff --git a/addons/website_event/static/tests/tours/website_event.js b/addons/website_event/static/tests/tours/website_event.js
index c823ec03150..2255ac4410f 100644
--- a/addons/website_event/static/tests/tours/website_event.js
+++ b/addons/website_event/static/tests/tours/website_event.js
@@ -26,14 +26,10 @@
trigger: '.modal-dialog div[name=date_begin]',
content: _t("Open date range picker. Pick a Start date for your event"),
run: function () {
- $('input[id="date_begin_0"]').val('09/30/2020 08:00:00').change();
- $('input[id="date_end_0"]').val('10/02/2020 23:00:00').change();
- $('input[id="date_begin_0"]').click();
+ $('input[data-field="date_begin"]').val('09/30/2020 08:00:00').change();
+ $('input[data-field="date_end"]').val('10/02/2020 23:00:00').change();
+ $('input[data-field="date_begin"]').click();
}
- }, {
- content: _t("Apply change."),
- trigger: '.daterangepicker[data-name=date_begin] .applyBtn',
- in_modal: false,
}, {
trigger: '.modal-footer button.btn-primary',
extra_trigger: '.modal-dialog input[type=text][value!=""]',