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)})