diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py
index 110ba18354d..986e5835b8b 100644
--- a/addons/web/__manifest__.py
+++ b/addons/web/__manifest__.py
@@ -36,6 +36,7 @@ This module provides the core of the Odoo Web Client.
"static/src/xml/search_panel.xml",
"static/src/xml/web_calendar.xml",
"static/src/xml/graph.xml",
+ "static/src/xml/week_days.xml",
],
'bootstrap': True, # load translations for login screen
}
diff --git a/addons/web/static/src/js/widgets/week_days.js b/addons/web/static/src/js/widgets/week_days.js
new file mode 100644
index 00000000000..0ba69c9227f
--- /dev/null
+++ b/addons/web/static/src/js/widgets/week_days.js
@@ -0,0 +1,90 @@
+odoo.define('web.WeekDays', function (require) {
+ 'use strict';
+
+ const CustomCheckbox = require('web.CustomCheckbox');
+ const Registry = require('web.widgetRegistry');
+ const utils = require('web.utils');
+ const { useState } = owl.hooks;
+
+
+ class WeekDays extends owl.Component {
+ constructor(parent) {
+ super(...arguments);
+ this.parent = parent;
+ this.weekdaysShort = [];
+ this._sortWeekdays();
+ this.state = useState({ days: this._prepareData(this.props.record.data) });
+ this.mode = this.props.options.mode;
+ }
+
+ /**
+ * @override
+ */
+ updateState(state) {
+ this.state.days = this._prepareData(state.data);
+ }
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prepares weekdays data for state
+ * @private
+ * @param {Object} data
+ */
+ _prepareData(data) {
+ const weekdays = {};
+ this.weekdaysShort.forEach((day) => {
+ weekdays[day] = {
+ id: `${day}-${utils.generateID()}`,
+ value: data[day],
+ };
+ });
+ return weekdays;
+ }
+ /**
+ * Generates short week days as per start of the week of language
+ * @private
+ */
+ _sortWeekdays() {
+ const weekStart = this.env._t.database.parameters.week_start;
+ const weekdaysShort = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
+ const slicedArray1 = weekdaysShort.slice(weekStart - 1, weekdaysShort.length);
+ const slicedArray2 = weekdaysShort.slice(0, weekStart - 1);
+ this.weekdaysShort = slicedArray1.concat(slicedArray2);
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onChange(ev) {
+ const field = ev.target.id.split("-")[0];
+ this.trigger('field-changed', {
+ dataPointID: this.props.record.id,
+ changes: { [field]: ev.target.checked },
+ });
+ }
+ }
+
+ WeekDays.components = { CustomCheckbox };
+ WeekDays.template = "web.RecurrentTask";
+ WeekDays.fieldDependencies = {
+ sun: {type: 'boolean'},
+ mon: {type: 'boolean'},
+ tue: {type: 'boolean'},
+ wed: {type: 'boolean'},
+ thu: {type: 'boolean'},
+ fri: {type: 'boolean'},
+ sat: {type: 'boolean'},
+ };
+
+ Registry.add('week_days', WeekDays);
+
+ return WeekDays;
+});
diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss
index b0419de7b29..c87a5d505a7 100644
--- a/addons/web/static/src/scss/form_view.scss
+++ b/addons/web/static/src/scss/form_view.scss
@@ -825,6 +825,27 @@ $o-form-label-margin-right: 0px;
margin-top: 5px;
}
+ // for weekly recurrent
+ .o_recurrent_weekdays {
+ > table {
+ width: 100%;
+ table-layout: fixed;
+ display: table;
+ td {
+ border: 1px solid #dee2e6;
+ }
+ th, td {
+ padding: 0.75rem 0 0.75rem 0;
+ }
+ .o_recurrent_weekday_label {
+ @include o-text-overflow;
+ }
+ .custom-control-label {
+ left: 25%;
+ }
+ }
+ }
+
// One2Many Kanban views
.o_field_widget .o_kanban_view.o_kanban_ungrouped {
padding: 0;
@@ -1051,3 +1072,4 @@ $o-form-label-margin-right: 0px;
}
}
}
+
diff --git a/addons/web/static/src/xml/week_days.xml b/addons/web/static/src/xml/week_days.xml
new file mode 100644
index 00000000000..9d4b2870364
--- /dev/null
+++ b/addons/web/static/src/xml/week_days.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+ |
+
+
+
+
+
diff --git a/addons/web/static/tests/widgets/week_days_tests.js b/addons/web/static/tests/widgets/week_days_tests.js
new file mode 100644
index 00000000000..fdda354a949
--- /dev/null
+++ b/addons/web/static/tests/widgets/week_days_tests.js
@@ -0,0 +1,157 @@
+odoo.define('web.week_days_tests', function (require) {
+ "use strict";
+
+ const FormView = require('web.FormView');
+ const testUtils = require('web.test_utils');
+ const { _t } = require('web.core');
+
+ QUnit.module('WeeklyRecurrence', {
+ beforeEach() {
+ this.data = {
+ partner: {
+ fields: {
+ id: {strin: "id", type:"integer"},
+ mon: {string: "Mon", type: "boolean"},
+ tue: {string: "Tue", type: "boolean"},
+ wed: {string: "Wed", type: "boolean"},
+ thu: {string: "Thu", type: "boolean"},
+ fri: {string: "Fri", type: "boolean"},
+ sat: {string: "Sat", type: "boolean"},
+ sun: {string: "Sun", type: "boolean"},
+
+ },
+ records: [
+ {
+ id: 1,
+ mon: false,
+ tue: false,
+ wed: false,
+ thu: false,
+ fri: false,
+ sat: false,
+ sun: false,
+ },
+ ],
+ },
+ };
+ },
+ }, function () {
+ QUnit.module('WeeklyRecurrenceWidget');
+
+ QUnit.test('simple week recurrence widget', async function (assert) {
+ assert.expect(14);
+
+ let writeCall = 0;
+ const form = await testUtils.createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ res_id: 1,
+ arch:
+ `
`,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ writeCall++;
+ if (writeCall === 1) {
+ assert.ok(args.args[1].sun,
+ "value of sunday should be true");
+ this.data.partner.records[0].sun = args.args[1].sun;
+ }
+ if (writeCall === 2) {
+ assert.notOk(args.args[1].sun,
+ "value of sunday should be false");
+ assert.ok(args.args[1].mon,
+ "value of monday should be true");
+ assert.ok(args.args[1].tue,
+ "value of tuesday should be true");
+ this.data.partner.records[0].sun = args.args[1].sun;
+ this.data.partner.records[0].mon = args.args[1].mon;
+ this.data.partner.records[0].tue = args.args[1].tue;
+ }
+ return Promise.resolve();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsN(form, '.custom-control input:disabled', 7,
+ "all inputs should be disabled in readonly mode");
+ const labelsTexts = [...form.el.querySelectorAll('.o_recurrent_weekday_label')].map(el => el.innerText.trim());
+ assert.deepEqual(labelsTexts, ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+ "labels should be short week names");
+
+ await testUtils.form.clickEdit(form);
+ assert.containsNone(form, '.custom-control input:disabled', 7,
+ "all inputs should be enabled in readonly mode");
+
+ await testUtils.dom.click(form.el.querySelector('input[id^="sun"'));
+ assert.ok(form.el.querySelector('input[id^="sun"').checked,
+ "sunday checkbox should be checked");
+ await testUtils.form.clickSave(form);
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.el.querySelector('input[id^="mon"'));
+ assert.ok(form.el.querySelector('input[id^="mon"').checked,
+ "monday checkbox should be checked");
+
+ await testUtils.dom.click(form.el.querySelector('input[id^="tue"'));
+ assert.ok(form.el.querySelector('input[id^="tue"').checked,
+ "tuesday checkbox should be checked");
+
+ // uncheck Sunday checkbox and check write call
+ await testUtils.dom.click(form.el.querySelector('input[id^="sun"'));
+ assert.notOk(form.el.querySelector('input[id^="sun"').checked,
+ "sunday checkbox should be unchecked");
+
+ await testUtils.form.clickSave(form);
+ assert.notOk(form.el.querySelector('input[id^="sun"').checked,
+ "sunday checkbox should be unchecked");
+ assert.ok(form.el.querySelector('input[id^="mon"').checked,
+ "monday checkbox should be checked");
+ assert.ok(form.el.querySelector('input[id^="tue"').checked,
+ "tuesday checkbox should be checked");
+
+ form.destroy();
+ });
+
+ QUnit.test('week recurrence widget show week start as per language configuration', async function (assert) {
+ assert.expect(1);
+
+ const form = await testUtils.createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ res_id: 1,
+ arch:
+ `
`,
+ env: {
+ _t: {
+ database: {
+ parameters: Object.assign(_t.database.parameters,{
+ week_start: 5,
+ }),
+ }
+ }
+ },
+ });
+
+ const labelsTexts = [...form.el.querySelectorAll('.o_recurrent_weekday_label')].map(el => el.innerText.trim());
+ assert.deepEqual(labelsTexts, ['Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu'],
+ "labels should be short week names");
+
+ form.destroy();
+ });
+ });
+});
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index a7b0487ad5a..ed3433ebefa 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -289,6 +289,7 @@
+
@@ -780,6 +781,7 @@
+
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 35932910c26..70c4ca5777d 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -2533,6 +2533,15 @@ reference (FieldReference)
- model_field: name of a FieldMany2One('ir.model') containing the model of the records that can be selected.
When this option is set, the select part of the FieldReference isn't displayed.
+Widgets
+-------
+week_days (WeekDays)
+ This field displays a list of checkboxes for week days, 1 checkbox for each day
+ and allow the user to select a subset of the choices.
+
+ .. code-block:: xml
+
+
Client actions
==============