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 da33de41b58..ed76d2c3e11 100644
--- a/addons/web/static/src/views/fields/datetime/datetime_field.js
+++ b/addons/web/static/src/views/fields/datetime/datetime_field.js
@@ -26,6 +26,7 @@ import { standardFieldProps } from "../standard_field_props";
* placeholder?: string;
* required?: boolean;
* rounding?: number;
+ * startDateField?: string;
* warnFuture?: boolean;
* }} DateTimeFieldProps
*
@@ -42,6 +43,7 @@ export class DateTimeField extends Component {
placeholder: { type: String, optional: true },
required: { type: Boolean, optional: true },
rounding: { type: Number, optional: true },
+ startDateField: { type: String, optional: true },
warnFuture: { type: Boolean, optional: true },
};
@@ -51,6 +53,10 @@ export class DateTimeField extends Component {
// Getters
//-------------------------------------------------------------------------
+ get endDateField() {
+ return this.props.endDateField || this.props.name;
+ }
+
get hasEmptyField() {
return this.values.length < 2 || !this.values.every(Boolean);
}
@@ -60,8 +66,16 @@ export class DateTimeField extends Component {
return this.inputRefs.map((ref) => ref.el?.value);
}
- get showEndDateInput() {
- return this.props.endDateField && this.values.filter(Boolean).length;
+ get relatedField() {
+ return this.props.startDateField || this.props.endDateField;
+ }
+
+ get showRange() {
+ return this.relatedField && this.values.filter(Boolean).length;
+ }
+
+ get startDateField() {
+ return this.props.startDateField || this.props.name;
}
get values() {
@@ -73,7 +87,7 @@ export class DateTimeField extends Component {
//-------------------------------------------------------------------------
setup() {
- const { endDateField, name } = this.props; // should not change
+ const { name } = this.props; // should not change
this.rootRef = useRef("root");
this.inputRefs = [useRef("start-date"), useRef("end-date")];
@@ -84,9 +98,9 @@ export class DateTimeField extends Component {
onChange: (value) => {
if (Array.isArray(value)) {
if (value.every(Boolean)) {
- this.emptyField = false;
+ this.emptyField = null;
} else {
- this.emptyField = value[0] ? endDateField : name;
+ this.emptyField = value[1] ? this.startDateField : this.endDateField;
this.state.value = value.find(Boolean);
}
}
@@ -96,10 +110,10 @@ export class DateTimeField extends Component {
const toUpdate = {};
if (Array.isArray(value)) {
// Value is already a range
- [toUpdate[name], toUpdate[endDateField]] = value;
+ [toUpdate[this.startDateField], toUpdate[this.endDateField]] = value;
} else {
- toUpdate[this.emptyField === name ? endDateField : name] = value;
- if (endDateField && this.emptyField) {
+ toUpdate[this.emptyField === name ? this.relatedField : name] = value;
+ if (this.relatedField && this.emptyField) {
toUpdate[this.emptyField] = false;
}
}
@@ -124,7 +138,7 @@ export class DateTimeField extends Component {
async addDate() {
const [value] = this.values;
- this.state.focusedDateIndex = this.emptyField === this.props.name ? 0 : 1;
+ this.state.focusedDateIndex = this.emptyField === this.startDateField ? 0 : 1;
this.state.value = [value, value];
}
@@ -139,14 +153,15 @@ export class DateTimeField extends Component {
* @param {DateTimeFieldProps} props
*/
getPickerProps(props) {
- const { endDateField, record, name, maxDate, minDate, rounding } = props;
+ const { record, name, maxDate, minDate, rounding } = props;
const value = this.getValueFromProps(props);
// Compute own props
this.type = record.fields[name].type;
- if (endDateField) {
+ if (this.relatedField) {
this.emptyField =
- !Array.isArray(value) && (record.data[endDateField] ? name : endDateField);
+ !Array.isArray(value) &&
+ (record.data[this.relatedField] ? name : this.relatedField);
}
// Compute picker props
@@ -168,14 +183,17 @@ export class DateTimeField extends Component {
* @param {DateTimeFieldProps} props
* @returns {DateTimePickerProps["value"]}
*/
- getValueFromProps({ endDateField, record, required, name }) {
+ getValueFromProps({ endDateField, name, record, required, startDateField }) {
const value = record.data[name];
- if (endDateField) {
- const endValue = record.data[endDateField];
- if (required || (value && endValue)) {
- return [value, endValue];
+ const relatedField = startDateField || endDateField;
+ if (relatedField) {
+ const relatedValue = record.data[relatedField];
+ if (required || (value && relatedValue)) {
+ const range = [value, value];
+ range[startDateField ? 0 : 1] = relatedValue;
+ return range;
} else if (!value) {
- return endValue;
+ return relatedValue;
}
}
return value;
@@ -217,6 +235,7 @@ export class DateTimeField extends Component {
}
}
+const START_DATE_FIELD_OPTION = "start_date_field";
const END_DATE_FIELD_OPTION = "end_date_field";
export const dateField = {
@@ -250,12 +269,23 @@ export const dateField = {
placeholder: attrs.placeholder,
required: Boolean(modifiers.required),
rounding: parseInt(options.rounding),
+ startDateField: options[START_DATE_FIELD_OPTION],
warnFuture: archParseBoolean(options.warn_future),
}),
- fieldDependencies: ({ type, modifiers, options }) =>
- options[END_DATE_FIELD_OPTION] && [
- { name: options[END_DATE_FIELD_OPTION], type, modifiers },
- ],
+ fieldDependencies: ({ type, modifiers, options }) => {
+ const deps = [];
+ if (options[START_DATE_FIELD_OPTION]) {
+ deps.push({ name: options[START_DATE_FIELD_OPTION], type, modifiers });
+ if (options[END_DATE_FIELD_OPTION]) {
+ console.warn(
+ `A field cannot have both ${START_DATE_FIELD_OPTION} and ${END_DATE_FIELD_OPTION} options at the same time`
+ );
+ }
+ } else if (options[END_DATE_FIELD_OPTION]) {
+ deps.push({ name: options[END_DATE_FIELD_OPTION], type, modifiers });
+ }
+ return deps;
+ },
};
export const dateTimeField = {
@@ -279,9 +309,15 @@ export const dateRangeField = {
displayName: _lt("Date Range"),
supportedOptions: [
...dateTimeField.supportedOptions,
+ {
+ label: _lt("Start date field"),
+ name: START_DATE_FIELD_OPTION,
+ type: "field",
+ availableTypes: ["date", "datetime"],
+ },
{
label: _lt("End date field"),
- name: "end_date_field",
+ name: END_DATE_FIELD_OPTION,
type: "field",
availableTypes: ["date", "datetime"],
},
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 3b6648aa45f..25792497dff 100644
--- a/addons/web/static/src/views/fields/datetime/datetime_field.xml
+++ b/addons/web/static/src/views/fields/datetime/datetime_field.xml
@@ -10,11 +10,11 @@
-
+
@@ -47,7 +47,7 @@
class="o_input cursor-pointer"
autocomplete="off"
t-att-placeholder="props.placeholder"
- t-att-data-field="props.endDateField"
+ t-att-data-field="endDateField"
t-on-input="onInput"
/>
{
);
});
- QUnit.test("initial empty start date", async (assert) => {
+ QUnit.test("Render with initial empty value and optional start date", async (assert) => {
+ patchDate(2014, 7, 14, 12, 34, 56);
+
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ serverData,
+ arch: `
+ `,
+ });
+
+ await click(target, "input[data-field=datetime_end]");
+
+ 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",
+ "end date should be set properly"
+ );
+ assert.notOk(isHiddenByCSS(target.querySelector(".o_add_date")));
+ assert.strictEqual(
+ target.querySelector(".o_add_date").innerText.trim().toLowerCase(),
+ "add start 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("initial empty date with optional start date", async (assert) => {
+ patchDate(2014, 7, 14, 12, 34, 56);
+
+ serverData.models.partner.records[0].datetime = "2017-03-13 00:00:00";
+ serverData.models.partner.records[0].datetime_end = false;
+
+ await makeView({
+ type: "form",
+ resModel: "partner",
+ 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_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("initial empty date with optional end 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);
@@ -609,14 +691,14 @@ QUnit.module("Fields", (hooks) => {
"add start date"
);
- // Add an start date
+ // Add a start 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"
+ "the start date should be set to the same value as the end date"
);
});
diff --git a/odoo/tests/form.py b/odoo/tests/form.py
index 70f37b48b98..c7d265c7075 100644
--- a/odoo/tests/form.py
+++ b/odoo/tests/form.py
@@ -233,15 +233,16 @@ class Form:
field_info['type'] = node.get('widget')
elif node.get('widget') == 'daterange':
options = ast.literal_eval(node.get('options', '{}'))
- daterange_field_names[options.get('end_date_field')] = field_name
+ related_field = options.get('start_date_field') or options.get('end_date_field')
+ daterange_field_names[related_field] = field_name
# determine subview to use for edition
if level and field_info['type'] == 'one2many':
field_info['invisible'] = field_modifiers.get('invisible') == [TRUE_LEAF]
field_info['edition_view'] = self._get_one2many_edition_view(field_info, node, level)
- for end_field, start_field in daterange_field_names.items():
- modifiers[end_field]['invisible'] = modifiers[start_field].get('invisible', False)
+ for related_field, start_field in daterange_field_names.items():
+ modifiers[related_field]['invisible'] = modifiers[start_field].get('invisible', False)
view['onchange'] = model._onchange_spec({'arch': etree.tostring(tree)})