[IMP] web: Optional start date in daterange fields

Before this commit, the date/datetime/daterange fields only allowed for
an optional end date field. This meant that the primary date was always
the start date.

This commit allows the field to do the opposite: with the primary date
being the end date, and having a `start_date_field` option for an
optional start date.

closes odoo/odoo#120695

Signed-off-by: Michaël Mattiello <mcm@odoo.com>
This commit is contained in:
Julien Mougenot
2023-05-11 09:36:24 +02:00
parent 3952fbd15f
commit 805197daf9
4 changed files with 153 additions and 34 deletions
@@ -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"],
},
@@ -10,11 +10,11 @@
<input
t-ref="start-date"
type="text"
t-att-id="props.id"
t-att-id="showRange ? props.endDateField and props.id : props.id"
class="o_input cursor-pointer"
autocomplete="off"
t-att-placeholder="props.placeholder"
t-att-data-field="props.name"
t-att-data-field="emptyField !== startDateField ? startDateField : endDateField"
t-on-input="onInput"
/>
<span
@@ -23,12 +23,12 @@
title="This date is on the future. Make sure it is what you expected."
/>
</t>
<t t-if="showEndDateInput">
<t t-if="showRange">
<t t-if="emptyField and !props.readonly and !props.required">
<!-- Add date button -->
<button class="o_add_date btn btn-secondary btn-sm w-100" t-on-click="addDate">
<i class="fa fa-plus me-2" />
<t t-if="emptyField === props.name">Add start date</t>
<t t-if="emptyField === startDateField">Add start date</t>
<t t-else="">Add end date</t>
</button>
</t>
@@ -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"
/>
<span
@@ -580,7 +580,89 @@ QUnit.module("Fields", (hooks) => {
);
});
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: `
<form>
<field name="datetime_end" widget="daterange" options="{'start_date_field': 'datetime'}"/>
</form>`,
});
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: `
<form>
<field name="datetime_end" widget="daterange" options="{'start_date_field': 'datetime'}"/>
</form>`,
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"
);
});
+4 -3
View File
@@ -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)})