diff --git a/eslint.config.cjs b/eslint.config.cjs index 0d5731f89a8b..de7798a21c11 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -191,7 +191,7 @@ const config = [{ }, }, { - files: ["**/*.esm.js"], + files: ["**/*.esm.js", "**/*.test.js"], languageOptions: { ecmaVersion: 2024, diff --git a/web_datetime_picker_default_time/README.rst b/web_datetime_picker_default_time/README.rst index c86cd7a52eaf..aa2f0a3c6566 100644 --- a/web_datetime_picker_default_time/README.rst +++ b/web_datetime_picker_default_time/README.rst @@ -17,13 +17,13 @@ Web Datetime Picker Default Time :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github - :target: https://github.com/OCA/web/tree/16.0/web_datetime_picker_default_time + :target: https://github.com/OCA/web/tree/18.0/web_datetime_picker_default_time :alt: OCA/web .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/web-16-0/web-16-0-web_datetime_picker_default_time + :target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_datetime_picker_default_time :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -45,14 +45,22 @@ browser. Usage ===== -You can define the default time as follows for a static value: +**Static Default Time** You can define the default time as follows for a +static value For ``widget="datetime"``: .. code:: xml - + -Otherwise you can also use a JSON field to make it dynamic through a -compute function, and reference this field in the view: +For ``widget="daterange"``: + +.. code:: xml + + + +**Dynamic Default Time** Otherwise you can also use a JSON field to make +it dynamic through a compute function, and reference this field in the +view: .. code:: python @@ -70,7 +78,7 @@ compute function, and reference this field in the view: Known issues / Roadmap ====================== -- Handle Timezone related to the default time +- Handle Timezone related to the default time Bug Tracker =========== @@ -78,7 +86,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -93,8 +101,18 @@ Authors Contributors ------------ -- Akim Juillerat akim.juillerat@camptocamp.com -- Iván Todorovich ivan.todorovich@camptocamp.com +- Akim Juillerat akim.juillerat@camptocamp.com +- Iván Todorovich ivan.todorovich@camptocamp.com + +- `Trobz `__: + + - Tuan Nguyen + +Other credits +------------- + +The migration of this module from 16.0 to 18.0 was financially supported +by Camptocamp. Maintainers ----------- @@ -117,6 +135,6 @@ Current `maintainer `__: |maintainer-grindtildeath| -This module is part of the `OCA/web `_ project on GitHub. +This module is part of the `OCA/web `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_datetime_picker_default_time/__manifest__.py b/web_datetime_picker_default_time/__manifest__.py index 98d289a2b32e..26fb657eac0d 100644 --- a/web_datetime_picker_default_time/__manifest__.py +++ b/web_datetime_picker_default_time/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Web Datetime Picker Default Time", "summary": "Allows to define a default time on datetime picker", - "version": "16.0.1.0.0", + "version": "18.0.1.0.0", "category": "web", "website": "https://github.com/OCA/web", "author": "Camptocamp, Odoo Community Association (OCA)", @@ -15,7 +15,9 @@ "assets": { "web.assets_backend": [ "/web_datetime_picker_default_time/static/src/js/*.js", - "/web_datetime_picker_default_time/static/src/xml/*.xml", + ], + "web.assets_unit_tests": [ + "web_datetime_picker_default_time/static/tests/web_datetime_picker_default_time.test.js", ], }, } diff --git a/web_datetime_picker_default_time/readme/CONTRIBUTORS.md b/web_datetime_picker_default_time/readme/CONTRIBUTORS.md index 6168e901d843..1dcb597d37d7 100644 --- a/web_datetime_picker_default_time/readme/CONTRIBUTORS.md +++ b/web_datetime_picker_default_time/readme/CONTRIBUTORS.md @@ -1,2 +1,4 @@ * Akim Juillerat * Iván Todorovich +- [Trobz](https://trobz.com): + - Tuan Nguyen \<\> \ No newline at end of file diff --git a/web_datetime_picker_default_time/readme/CREDITS.md b/web_datetime_picker_default_time/readme/CREDITS.md new file mode 100644 index 000000000000..57e03a9fe7a4 --- /dev/null +++ b/web_datetime_picker_default_time/readme/CREDITS.md @@ -0,0 +1 @@ +The migration of this module from 16.0 to 18.0 was financially supported by Camptocamp. diff --git a/web_datetime_picker_default_time/readme/USAGE.md b/web_datetime_picker_default_time/readme/USAGE.md index 03e2fc174e39..dfafdae45657 100644 --- a/web_datetime_picker_default_time/readme/USAGE.md +++ b/web_datetime_picker_default_time/readme/USAGE.md @@ -1,9 +1,16 @@ -You can define the default time as follows for a static value: +**Static Default Time** +You can define the default time as follows for a static value +For `widget="datetime"`: +```xml + +``` +For `widget="daterange"`: ```xml - + ``` +**Dynamic Default Time** Otherwise you can also use a JSON field to make it dynamic through a compute function, and reference this field in the view: diff --git a/web_datetime_picker_default_time/static/description/index.html b/web_datetime_picker_default_time/static/description/index.html index 8ff4371146e6..64191e63f8c0 100644 --- a/web_datetime_picker_default_time/static/description/index.html +++ b/web_datetime_picker_default_time/static/description/index.html @@ -369,7 +369,7 @@

Web Datetime Picker Default Time

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:a5ffb697bdf4c26817212f783a9d4d617e91fcdc912a7750382d3eddaff05f7b !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runboat

This module customizes the datetime picker widget and allows to define a default time to be applied in case the user selects only a Date.

For example, if a user wants to define a commitment date without having @@ -386,19 +386,26 @@

Web Datetime Picker Default Time

  • Credits
  • Usage

    -

    You can define the default time as follows for a static value:

    +

    Static Default Time You can define the default time as follows for a +static value For widget="datetime":

    -<field name="your_datetime_field" options="{'defaultTime': {'hour': 8, 'minute': 30, 'second': 15 }}"/>
    +<field name="your_datetime_field" widget="datetime" options="{'defaultTime': {'hour': 8, 'minute': 30, 'second': 15 }}"/>
     
    -

    Otherwise you can also use a JSON field to make it dynamic through a -compute function, and reference this field in the view:

    +

    For widget="daterange":

    +
    +<field name="your_start_datetime_field" widget="datetime" options="{'end_date_field': 'your_end_datetime_field', 'defaultStartTime': {'hour': 2, 'minute': 22, 'second': 22,}, 'defaultEndTime': {'hour': 3, 'minute': 33, 'second': 33,}}"/>
    +
    +

    Dynamic Default Time Otherwise you can also use a JSON field to make +it dynamic through a compute function, and reference this field in the +view:

     start_time = field.Json(compute="_compute_start_time")
     
    @@ -422,7 +429,7 @@ 

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

    +feedback.

    Do not contact contributors directly about support or help with technical issues.

    @@ -438,10 +445,19 @@

    Contributors

    +
    +
    +

    Other credits

    +

    The migration of this module from 16.0 to 18.0 was financially supported +by Camptocamp.

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -451,7 +467,7 @@

    Maintainers

    promote its widespread use.

    Current maintainer:

    grindtildeath

    -

    This module is part of the OCA/web project on GitHub.

    +

    This module is part of the OCA/web project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    diff --git a/web_datetime_picker_default_time/static/src/js/datepicker.esm.js b/web_datetime_picker_default_time/static/src/js/datepicker.esm.js index f453cbf7cc5e..3872f6494b39 100644 --- a/web_datetime_picker_default_time/static/src/js/datepicker.esm.js +++ b/web_datetime_picker_default_time/static/src/js/datepicker.esm.js @@ -1,51 +1,58 @@ -/** @odoo-module **/ /* Copyright 2024 Camptocamp * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) */ - -import {DateTimePicker} from "@web/core/datepicker/datepicker"; +import {DateTimePicker} from "@web/core/datetime/datetime_picker"; +import {DateTimePickerPopover} from "@web/core/datetime/datetime_picker_popover"; import {patch} from "@web/core/utils/patch"; -import {localization} from "@web/core/l10n/localization"; - -patch(DateTimePicker.prototype, "DateTimePickerDefaultTime", { - onMounted() { - this._super.apply(this, arguments); - this.addPickerListener("change", ({date, oldDate}) => { - const default_time = this.props.defaultTime; - if (date && !oldDate && default_time) { - // FIXME: Consider TZ - date.set({ - hour: default_time.hour, - minute: default_time.minute, - second: default_time.second, - }); - window.$(this.rootRef.el).datetimepicker("date", date); - } - }); - }, - isStrDate(input_string) { - return input_string.trim().length == localization.dateFormat.length; - }, - customParseValue(input_value, options) { - const default_time = this.props.defaultTime; - let [res, error] = this.parseValueOriginal(input_value, options); - if (default_time && this.isStrDate(input_value)) { - const new_value = res.set({ - hour: default_time.hour, - minute: default_time.minute, - second: default_time.second, - }); - res = new_value; +const {DateTime} = luxon; + +/** + * @typedef {import("@web/core/datetime/datetime_picker").DateTimePickerProps & { + * defaultTime?: { hour: number, minute: number, second: number }, + * defaultStartTime?: { hour: number, minute: number, second: number }, + * defaultEndTime?: { hour: number, minute: number, second: number }, + * }} DateTimePickerProps + */ + +patch(DateTimePicker.prototype, { + /** + * @param {DateTimePickerProps} props + */ + onPropsUpdated(props) { + super.onPropsUpdated(props); + + const timeValues = this.values.map((val, index) => + this.getCustomTimeValues(val, index) + ); + + if (props.range) { + this.state.timeValues = timeValues; + } else { + this.state.timeValues = []; + this.state.timeValues[props.focusedDateIndex] = + timeValues[props.focusedDateIndex]; } - return [res, error]; + + this.adjustFocus(this.values, props.focusedDateIndex); + this.handle12HourSystem(); + this.state.timeValues = this.state.timeValues.map((timeValue) => + timeValue.map(String) + ); }, - initFormat() { - this._super.apply(this, arguments); - this.parseValueOriginal = this.parseValue; - this.parseValue = this.customParseValue; + + getCustomTimeValues(val, index) { + const defaultTime = + this.props.defaultTime || this.props.defaultStartTime || DateTime.local(); + const defaultEndTime = + this.props.defaultEndTime || DateTime.local().plus({hour: 1}); + + const timeSource = index === 1 ? val || defaultEndTime : val || defaultTime; + + return [timeSource.hour, timeSource.minute || 0, timeSource.second || 0]; }, }); -DateTimePicker.props = _.extend({}, DateTimePicker.props, { +DateTimePicker.props = { + ...DateTimePicker.props, defaultTime: { type: Object, shape: { @@ -55,4 +62,24 @@ DateTimePicker.props = _.extend({}, DateTimePicker.props, { }, optional: true, }, -}); + defaultStartTime: { + type: Object, + shape: { + hour: Number, + minute: Number, + second: Number, + }, + optional: true, + }, + defaultEndTime: { + type: Object, + shape: { + hour: Number, + minute: Number, + second: Number, + }, + optional: true, + }, +}; + +DateTimePickerPopover.props.pickerProps.shape = DateTimePicker.props; diff --git a/web_datetime_picker_default_time/static/src/js/datetime_field.esm.js b/web_datetime_picker_default_time/static/src/js/datetime_field.esm.js index 58f46609f94e..73e60477eb4e 100644 --- a/web_datetime_picker_default_time/static/src/js/datetime_field.esm.js +++ b/web_datetime_picker_default_time/static/src/js/datetime_field.esm.js @@ -1,20 +1,84 @@ -/** @odoo-module **/ /* Copyright 2024 Camptocamp * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) */ -import {DateTimeField} from "@web/views/fields/datetime/datetime_field"; import {patch} from "@web/core/utils/patch"; +import { + DateTimeField, + dateRangeField, + dateTimeField, +} from "@web/views/fields/datetime/datetime_field"; +import { + listDateRangeField, + listDateTimeField, +} from "@web/views/fields/datetime/list_datetime_field"; -patch(DateTimeField.prototype, "DateTimeFieldDefaultTime", { +/** + * @typedef {import("./datepicker.esm").DateTimePickerProps} DateTimePickerProps + */ + +patch(DateTimeField.prototype, { + setup() { + super.setup(); + + this.state.defaultTime = this.defaultTime; + this.state.defaultStartTime = this.defaultStartTime; + this.state.defaultEndTime = this.defaultEndTime; + }, + + // Getter get defaultTime() { if (typeof this.props.defaultTime === "string") { + if (!this.props.record.data[this.props.defaultTime]) { + return ""; + } + if (typeof this.props.record.data[this.props.defaultTime] === "string") { + return JSON.parse(this.props.record.data[this.props.defaultTime]); + } return this.props.record.data[this.props.defaultTime]; } return this.props.defaultTime; }, + + get defaultStartTime() { + if (typeof this.props.defaultStartTime === "string") { + if (!this.props.record.data[this.props.defaultStartTime]) { + return ""; + } + if ( + typeof this.props.record.data[this.props.defaultStartTime] === "string" + ) { + return JSON.parse(this.props.record.data[this.props.defaultStartTime]); + } + return this.props.record.data[this.props.defaultStartTime]; + } + return this.props.defaultStartTime; + }, + + get defaultEndTime() { + if (typeof this.props.defaultEndTime === "string") { + if (!this.props.record.data[this.props.defaultEndTime]) { + return ""; + } + if (typeof this.props.record.data[this.props.defaultEndTime] === "string") { + return JSON.parse(this.props.record.data[this.props.defaultEndTime]); + } + return this.props.record.data[this.props.defaultEndTime]; + } + return this.props.defaultEndTime; + }, + + // OVERRIDE:remove automatic date calculation + async addDate(valueIndex) { + this.state.focusedDateIndex = valueIndex; + this.state.value = this.values; + this.state.range = true; + + this.openPicker(valueIndex); + }, }); -DateTimeField.props = _.extend({}, DateTimeField.props, { +DateTimeField.props = { + ...DateTimeField.props, defaultTime: { type: [ String, @@ -30,13 +94,60 @@ DateTimeField.props = _.extend({}, DateTimeField.props, { ], optional: true, }, + defaultStartTime: { + type: [ + String, + { + type: Object, + shape: { + hour: Number, + minute: Number, + second: Number, + }, + optional: true, + }, + ], + optional: true, + }, + defaultEndTime: { + type: [ + String, + { + type: Object, + shape: { + hour: Number, + minute: Number, + second: Number, + }, + optional: true, + }, + ], + optional: true, + }, +}; + +const superDateTimeExtractProps = dateTimeField.extractProps; +dateTimeField.extractProps = ({attrs, options}, dynamicInfo) => ({ + ...superDateTimeExtractProps({attrs, options}, dynamicInfo), + defaultTime: options.defaultTime, }); -const super_extractProps = DateTimeField.extractProps; +const superDateRangeExtractProps = dateRangeField.extractProps; +dateRangeField.extractProps = ({attrs, options}, dynamicInfo) => ({ + ...superDateRangeExtractProps({attrs, options}, dynamicInfo), + defaultStartTime: options.defaultStartTime, + defaultEndTime: options.defaultEndTime, +}); -DateTimeField.extractProps = ({attrs}) => { - return { - ...super_extractProps({attrs}), - defaultTime: attrs.options.defaultTime, - }; -}; +const superListDateTimeExtractProps = listDateTimeField.extractProps; +listDateTimeField.extractProps = ({attrs, options}, dynamicInfo) => ({ + ...superListDateTimeExtractProps({attrs, options}, dynamicInfo), + defaultTime: options.defaultTime, +}); + +const superListDateRangeExtractProps = listDateRangeField.extractProps; +listDateRangeField.extractProps = ({attrs, options}, dynamicInfo) => ({ + ...superListDateRangeExtractProps({attrs, options}, dynamicInfo), + defaultStartTime: options.defaultStartTime, + defaultEndTime: options.defaultEndTime, +}); diff --git a/web_datetime_picker_default_time/static/src/xml/datetime_field.xml b/web_datetime_picker_default_time/static/src/xml/datetime_field.xml deleted file mode 100644 index df9affad0de1..000000000000 --- a/web_datetime_picker_default_time/static/src/xml/datetime_field.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - defaultTime - - - diff --git a/web_datetime_picker_default_time/static/tests/web_datetime_picker_default_time.test.js b/web_datetime_picker_default_time/static/tests/web_datetime_picker_default_time.test.js new file mode 100644 index 000000000000..f11696a9f206 --- /dev/null +++ b/web_datetime_picker_default_time/static/tests/web_datetime_picker_default_time.test.js @@ -0,0 +1,166 @@ +import {expect, test} from "@odoo/hoot"; +import {click, hover, queryOne, waitFor} from "@odoo/hoot-dom"; + +import { + contains, + defineModels, + fields, + models, + mountView, + onRpc, +} from "@web/../tests/web_test_helpers"; + +class ProductPricelistItem extends models.Model { + _name = "product.pricelist.item"; + _inherit = []; + + default_date = fields.Json(); + date_start = fields.Datetime(); + date_end = fields.Datetime(); + datetime_field = fields.Datetime({string: "Datetime Field"}); + + _records = [{id: 1, default_date: '{"hour": 8, "minute": 30, "second": 15}'}]; +} + +defineModels([ProductPricelistItem]); + +test("Default time is applied correctly for datetime field", async () => { + await mountView({ + type: "form", + resModel: "product.pricelist.item", + arch: ` +
    + + `, + }); + + const dateTimeFieldSelector = "input[data-field='datetime_field']"; + await click(dateTimeFieldSelector); + await contains(".o_date_picker .o_datetime_button:first").click(); + const dateTimeFieldElement = queryOne(dateTimeFieldSelector); + const date = new Date(dateTimeFieldElement.value); + + expect(date.getHours()).toBe(5); + expect(date.getMinutes()).toBe(5); + expect(date.getSeconds()).toBe(5); +}); + +test("Default time is applied correctly for daterange field", async () => { + await mountView({ + type: "form", + resModel: "product.pricelist.item", + arch: ` +
    + + `, + }); + + // Test defaultStartTime + const dateStartFieldSelector = "input[data-field='date_start']"; + await click(dateStartFieldSelector); + await contains(".o_date_picker .o_datetime_button:first").click(); + const dateStartFieldElement = queryOne(dateStartFieldSelector); + const dateStart = new Date(dateStartFieldElement.value); + + expect(dateStart.getHours()).toBe(2); + expect(dateStart.getMinutes()).toBe(22); + expect(dateStart.getSeconds()).toBe(22); + + // Test defaultEndTime + await hover("div[name='date_start']"); + await contains(".o_add_end_date").click(); + await contains(".o_date_picker:nth-of-type(2) .o_datetime_button:last").click(); + await waitFor("input[data-field='date_end']"); + const dateEndFieldElement = queryOne("input[data-field='date_end']"); + const dateEnd = new Date(dateEndFieldElement.value); + + expect(dateEnd.getHours()).toBe(3); + expect(dateEnd.getMinutes()).toBe(33); + expect(dateEnd.getSeconds()).toBe(33); +}); + +onRpc("has_group", () => true); +test("Default time is applied correctly for list.datetime field", async () => { + await mountView({ + type: "list", + resModel: "product.pricelist.item", + arch: ` + + + `, + }); + + await contains(".o_control_panel_main_buttons .o_list_button_add").click(); + const dateTimeFieldSelector = "input[data-field='datetime_field']"; + await contains(dateTimeFieldSelector).click(); + await contains(".o_date_picker .o_datetime_button:first").click(); + const dateTimeFieldElement = queryOne(dateTimeFieldSelector); + const date = new Date(dateTimeFieldElement.value); + + expect(date.getHours()).toBe(5); + expect(date.getMinutes()).toBe(5); + expect(date.getSeconds()).toBe(5); +}); + +test("Default time is applied correctly for list.daterange field", async () => { + await mountView({ + type: "list", + resModel: "product.pricelist.item", + arch: ` + + + `, + }); + + await contains(".o_control_panel_main_buttons .o_list_button_add").click(); + + // Test defaultStartTime + const dateStartFieldSelector = "input[data-field='date_start']"; + await contains(dateStartFieldSelector).click(); + await contains(".o_date_picker .o_datetime_button:first").click(); + const dateStartFieldElement = queryOne(dateStartFieldSelector); + const dateStart = new Date(dateStartFieldElement.value); + + expect(dateStart.getHours()).toBe(2); + expect(dateStart.getMinutes()).toBe(22); + expect(dateStart.getSeconds()).toBe(22); + + // Test defaultEndTime + await contains(".o_add_end_date").click(); + await contains(".o_date_picker .o_datetime_button:first").click(); + await contains(".o_date_picker .o_datetime_button:last").click(); + await contains(".o_date_picker .o_datetime_button:last").click(); + await contains("button.o_apply").click(); + await waitFor("input[data-field='date_end']", {timeout: 1500}); + + const dateEndFieldElement = queryOne("input[data-field='date_end']"); + const dateEnd = new Date(dateEndFieldElement.value); + + expect(dateEnd.getHours()).toBe(3); + expect(dateEnd.getMinutes()).toBe(33); + expect(dateEnd.getSeconds()).toBe(33); +}); + +test("Dynamic default time is applied correctly", async () => { + await mountView({ + type: "form", + resId: 1, + resModel: "product.pricelist.item", + arch: ` +
    + + + `, + }); + + const dateTimeFieldSelector = "input[data-field='datetime_field']"; + await click(dateTimeFieldSelector); + await waitFor(".o_date_picker .o_datetime_button"); + await click(".o_date_picker .o_datetime_button:first"); + const dateTimeFieldElement = queryOne(dateTimeFieldSelector); + const date = new Date(dateTimeFieldElement.value); + + expect(date.getHours()).toBe(8); + expect(date.getMinutes()).toBe(30); + expect(date.getSeconds()).toBe(15); +});