diff --git a/addons/project/static/src/burndown_chart/burndown_chart_pivot_model.js b/addons/project/static/src/burndown_chart/burndown_chart_pivot_model.js
deleted file mode 100644
index 928b265eeb0..00000000000
--- a/addons/project/static/src/burndown_chart/burndown_chart_pivot_model.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/** @odoo-module **/
-
-import { PivotModel } from "@web/views/pivot/pivot_model";
-
-export class BurndownChartPivotModel extends PivotModel {
- /**
- * @protected
- * @override
- */
- async _loadData(config, prune = true) {
- config.metaData.measures.__count.string = '# of Tasks';
- await super._loadData(config, prune);
- }
-}
diff --git a/addons/project/static/src/burndown_chart/burndown_chart_pivot_view.js b/addons/project/static/src/burndown_chart/burndown_chart_pivot_view.js
deleted file mode 100644
index 52e4d96cf7d..00000000000
--- a/addons/project/static/src/burndown_chart/burndown_chart_pivot_view.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/** @odoo-module **/
-
-import { BurndownChartPivotModel } from "./burndown_chart_pivot_model";
-import { pivotView } from "@web/views/pivot/pivot_view";
-import { registry } from "@web/core/registry";
-
-const viewRegistry = registry.category("views");
-
-const burndownChartPivotView = {...pivotView, Model: BurndownChartPivotModel};
-viewRegistry.add("burndown_chart_pivot", burndownChartPivotView);
diff --git a/addons/project/static/src/burndown_chart/burndown_chart_search_model.js b/addons/project/static/src/burndown_chart/burndown_chart_search_model.js
new file mode 100644
index 00000000000..7a199af20ad
--- /dev/null
+++ b/addons/project/static/src/burndown_chart/burndown_chart_search_model.js
@@ -0,0 +1,135 @@
+/** @odoo-module */
+
+import { useService } from "@web/core/utils/hooks";
+import { SearchModel } from "@web/search/search_model";
+
+
+export class BurndownChartSearchModel extends SearchModel {
+
+ /**
+ * @override
+ */
+ setup(services) {
+ this.notificationService = useService("notification");
+ super.setup(...arguments);
+ }
+
+ /**
+ * @override
+ */
+ async load(config) {
+ await super.load(...arguments);
+ // Store date and stage_id searchItemId in the SearchModel for reuse in other functions.
+ for (const searchItem of Object.values(this.searchItems)) {
+ if (['dateGroupBy', 'groupBy'].includes(searchItem.type)) {
+ if (this.stageIdSearchItemId && this.dateSearchItemId) {
+ return;
+ }
+ switch (searchItem.fieldName) {
+ case 'date':
+ this.dateSearchItemId = searchItem.id;
+ break;
+ case 'stage_id':
+ this.stageIdSearchItemId = searchItem.id;
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * @override
+ */
+ deactivateGroup(groupId) {
+ // Prevent removing Date & Stage group by from the search
+ if (this.searchItems[this.stageIdSearchItemId].groupId == groupId && this.searchItems[this.dateSearchItemId].groupId) {
+ this._addGroupByNotification(this.env._t("Date and Stage"));
+ return;
+ }
+ super.deactivateGroup(groupId);
+ }
+
+ /**
+ * @override
+ */
+ toggleDateGroupBy(searchItemId, intervalId) {
+ // Ensure that there is always one and only one date group by selected.
+ if (searchItemId === this.dateSearchItemId) {
+ let filtered_query = [];
+ let triggerNotification = false;
+ for (const queryElem of this.query) {
+ if (queryElem.searchItemId !== searchItemId) {
+ filtered_query.push(queryElem);
+ } else if (queryElem.intervalId === intervalId) {
+ triggerNotification = true;
+ }
+ }
+ if (filtered_query.length !== this.query.length) {
+ this.query = filtered_query;
+ if (triggerNotification) {
+ this._addGroupByNotification(this.env._t("Date"));
+ }
+ }
+ }
+ super.toggleDateGroupBy(...arguments);
+ }
+
+ /**
+ * @override
+ */
+ toggleSearchItem(searchItemId) {
+ // Ensure that stage_id is always selected.
+ if (searchItemId === this.stageIdSearchItemId
+ && this.query.some(queryElem => queryElem.searchItemId === searchItemId)) {
+ this._addGroupByNotification(this.env._t("Stage"));
+ return;
+ }
+ super.toggleSearchItem(...arguments);
+ }
+
+ /**
+ * Adds a notification relative to the group by constraint of the Burndown Chart.
+ * @param fieldName The field name(s) the notification has to be related to.
+ * @private
+ */
+ _addGroupByNotification(fieldName) {
+ const notif = this.env._t("The Burndown Chart must be grouped by");
+ this.notificationService.add(
+ `${notif} ${fieldName}`,
+ { type: "danger" }
+ );
+ }
+
+ /**
+ * @override
+ */
+ async _notify() {
+ // Ensure that we always group by date firstly and by stage_id secondly
+ let stageIdIndex = -1;
+ let dateIndex = -1;
+ for (const [index, queryElem] of this.query.entries()) {
+ if (stageIdIndex !== -1 && dateIndex !== -1) {
+ break;
+ }
+ switch (queryElem.searchItemId) {
+ case this.dateSearchItemId:
+ dateIndex = index;
+ break;
+ case this.stageIdSearchItemId:
+ stageIdIndex = index;
+ break;
+ }
+ }
+ if (stageIdIndex > 0) {
+ if (stageIdIndex > dateIndex) {
+ dateIndex += 1;
+ }
+ this.query.splice(0, 0, this.query.splice(stageIdIndex, 1)[0]);
+ }
+ if (dateIndex > 0) {
+ this.query.splice(0, 0, this.query.splice(dateIndex, 1)[0]);
+ }
+ await super._notify(...arguments);
+ }
+
+}
diff --git a/addons/project/static/src/burndown_chart/burndown_chart_view.js b/addons/project/static/src/burndown_chart/burndown_chart_view.js
index e6d5c2b0f8d..0906550a881 100644
--- a/addons/project/static/src/burndown_chart/burndown_chart_view.js
+++ b/addons/project/static/src/burndown_chart/burndown_chart_view.js
@@ -4,6 +4,7 @@ import { BurndownChartModel } from "./burndown_chart_model";
import { BurndownChartRenderer } from "./burndown_chart_renderer";
import { graphView } from "@web/views/graph/graph_view";
import { registry } from "@web/core/registry";
+import { BurndownChartSearchModel } from "./burndown_chart_search_model";
const viewRegistry = registry.category("views");
@@ -11,7 +12,10 @@ const burndownChartGraphView = {
...graphView,
Renderer: BurndownChartRenderer,
buttonTemplate: "project.BurndownChartView.Buttons",
+ hideCustomGroupBy: true,
Model: BurndownChartModel,
+ searchMenuTypes: graphView.searchMenuTypes.filter(menuType => menuType !== "comparison"),
+ SearchModel: BurndownChartSearchModel,
};
viewRegistry.add("burndown_chart", burndownChartGraphView);
diff --git a/addons/project/static/src/burndown_chart/burndown_chart_view.xml b/addons/project/static/src/burndown_chart/burndown_chart_view.xml
index 75605aa1428..0925d1dd5c3 100644
--- a/addons/project/static/src/burndown_chart/burndown_chart_view.xml
+++ b/addons/project/static/src/burndown_chart/burndown_chart_view.xml
@@ -7,6 +7,8 @@
true
+
+
diff --git a/addons/project/static/tests/burndown_chart_tests.js b/addons/project/static/tests/burndown_chart_tests.js
index 5c8e13cbc31..467a1792d53 100644
--- a/addons/project/static/tests/burndown_chart_tests.js
+++ b/addons/project/static/tests/burndown_chart_tests.js
@@ -1,21 +1,23 @@
/** @odoo-module */
-import { click, getFixture } from "@web/../tests/helpers/utils";
+import { browser } from "@web/core/browser/browser";
+import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
+import { setupControlPanelServiceRegistry, toggleGroupByMenu, toggleMenuItem, toggleMenuItemOption } from "@web/../tests/search/helpers";
import { COLORS, hexToRGBA } from "@web/views/graph/colors";
import { dialogService } from "@web/core/dialog/dialog_service";
import { getGraphRenderer } from "@web/../tests/views/graph_view_tests";
import { makeView } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
-import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
+import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
const serviceRegistry = registry.category("services");
QUnit.module("Project", {}, () => {
QUnit.module("Views", (hooks) => {
- let serverData;
+ let makeViewParams;
let target;
- hooks.beforeEach(async () => {
+ hooks.beforeEach(async (assert) => {
target = getFixture();
- serverData = {
+ const serverData = {
models: {
burndown_chart: {
fields: {
@@ -55,9 +57,24 @@ QUnit.module("Project", {}, () => {
`,
+ "burndown_chart,false,search": `
+
+ `,
},
};
+ makeViewParams = {
+ serverData,
+ resModel: "burndown_chart",
+ type: "burndown_chart",
+ };
setupControlPanelServiceRegistry();
+ const notificationMock = () => {
+ assert.step("notification_triggered");
+ return () => {};
+ };
+ registry.category("services").add("notification", makeFakeNotificationService(notificationMock), {
+ force: true,
+ });
serviceRegistry.add("dialog", dialogService);
});
@@ -66,11 +83,7 @@ QUnit.module("Project", {}, () => {
QUnit.test("check if default mode is line chart and line chart is stacked for burndown chart", async function (assert) {
assert.expect(5);
- const burndownChart = await makeView({
- serverData,
- resModel: "burndown_chart",
- type: "burndown_chart",
- });
+ const burndownChart = await makeView(makeViewParams);
assert.strictEqual(burndownChart.model.metaData.mode, "line", "should be in line chart mode.");
assert.ok(burndownChart.model.metaData.stacked, "should be stacked by default.");
@@ -106,11 +119,7 @@ QUnit.module("Project", {}, () => {
QUnit.test("check if the stacked button is visible in the line chart", async function (assert) {
assert.expect(3);
- const burndownChart = await makeView({
- serverData,
- resModel: "burndown_chart",
- type: "burndown_chart",
- });
+ const burndownChart = await makeView(makeViewParams);
assert.ok(burndownChart.model.metaData.stacked, "graph should be a burndown chart.");
assert.containsOnce(target, `button.o_graph_button[data-tooltip="Stacked"]`);
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
@@ -121,11 +130,7 @@ QUnit.module("Project", {}, () => {
QUnit.test("check if it is classic line chart when stacked prop is false in line chart", async function (assert) {
assert.expect(4);
- const burndownChart = await makeView({
- serverData,
- resModel: "burndown_chart",
- type: "burndown_chart",
- });
+ const burndownChart = await makeView(makeViewParams);
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
await click(stackButton);
@@ -159,5 +164,161 @@ QUnit.module("Project", {}, () => {
assert.deepEqual(actualDatasets, expectedDatasets);
});
+
+ QUnit.test("check that the sort buttons are invisible", async function (assert) {
+ await makeView(makeViewParams);
+ assert.containsNone(target, '.o_cp_bottom_left:has(.btn-group[role=toolbar][aria-label="Sort graph"])', "The sort buttons are not rendered.");
+ });
+
+ async function makeBurnDownChartWithSearchView(makeViewOverwriteParams = { }) {
+ patchWithCleanup(browser, {
+ setTimeout: (fn) => fn(),
+ clearTimeout: () => {},
+ });
+ await makeView({
+ ...makeViewParams,
+ searchViewId: false,
+ searchViewArch: `
+
+
+
+
+ `,
+ searchViewFields: {
+ date: {
+ name: "date",
+ string: "Date",
+ type: "date",
+ store: true,
+ sortable: true,
+ searchable: true,
+ },
+ stage_id: {
+ name: "stage_id",
+ string: "Stage",
+ type: "many2one",
+ store: true,
+ sortable: true,
+ searchable: true,
+ },
+ },
+ context: { ...makeViewParams.context, 'search_default_date': 1, 'search_default_stage': 1 },
+ ...makeViewOverwriteParams,
+ });
+ }
+
+ async function testBurnDownChartWithSearchView(stepsTriggeringNotification, assert) {
+ await makeBurnDownChartWithSearchView();
+ await stepsTriggeringNotification();
+ assert.verifySteps(['notification_triggered']);
+ }
+
+ function getFirstElementForXpath(xpath) {
+ const xPathResult = document.evaluate(xpath, target, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
+ return xPathResult.singleNodeValue;
+ }
+
+ async function openGroupByMainMenu(target) {
+ await toggleGroupByMenu(target);
+ }
+
+ async function openGroupByDateMenu(target) {
+ await openGroupByMainMenu(target);
+ await toggleMenuItem(target, 'Date');
+ }
+
+ async function toggleGroupByStageMenu(target) {
+ await openGroupByMainMenu(target);
+ await toggleMenuItem(target, 'Stage');
+ }
+
+ async function toggleSelectedGroupByDateItem(target) {
+ await openGroupByDateMenu(target);
+ const selectedGroupByDateItemXpath = `//div
+ [contains(@class, 'o_group_by_menu')]
+ //button
+ [contains(@class, 'o_menu_item')]
+ [contains(., 'Date')]
+ /following-sibling::div
+ /span
+ [contains(@class, 'o_item_option')]
+ [contains(@class, 'selected')]`;
+ const selectedGroupByDateItemElement = getFirstElementForXpath(selectedGroupByDateItemXpath);
+ await toggleMenuItemOption(target, 'Date', selectedGroupByDateItemElement.innerText);
+ }
+
+ QUnit.test("check that removing the group by 'Date: Month > Stage' in the search bar triggers a notification", async function (assert) {
+
+ const stepsTriggeringNotification = async () => {
+ const removeFilterXpath = `//div[contains(@class, 'o_searchview_facet')]
+ [.//span[@class='o_facet_value']
+ [contains(., 'Date: Month')]]
+ /i[contains(@class, 'o_facet_remove')]`;
+ const removeFilterElement = getFirstElementForXpath(removeFilterXpath);
+ await click(removeFilterElement);
+ };
+ await testBurnDownChartWithSearchView(stepsTriggeringNotification, assert);
+ });
+
+ QUnit.test("check that removing the group by 'Date' triggers a notification", async function (assert) {
+ const stepsTriggeringNotification = async () => {
+ await toggleSelectedGroupByDateItem(target);
+ };
+ await testBurnDownChartWithSearchView(stepsTriggeringNotification, assert);
+ });
+
+ QUnit.test("check that removing the group by 'Stage' triggers a notification", async function (assert) {
+ const stepsTriggeringNotification = async () => {
+ await toggleGroupByStageMenu(target);
+ };
+ await testBurnDownChartWithSearchView(stepsTriggeringNotification, assert);
+ });
+
+ QUnit.test("check that adding a group by 'Date' actually toggle it", async function (assert) {
+ await makeBurnDownChartWithSearchView();
+ await openGroupByDateMenu(target);
+ const firstNotSelectedGroupByDateItemXpath = `//div
+ [contains(@class, 'o_group_by_menu')]
+ //button
+ [contains(@class, 'o_menu_item')]
+ [contains(., 'Date')]
+ /following-sibling::div
+ /span
+ [contains(@class, 'o_item_option')]
+ [not(contains(@class, 'selected'))]`;
+ const firstNotSelectedGroupByDateItemElement = getFirstElementForXpath(firstNotSelectedGroupByDateItemXpath);
+ await toggleMenuItemOption(target, 'Date', firstNotSelectedGroupByDateItemElement.innerText);
+ const groupByDateSubMenuXpath = `//div
+ [contains(@class, 'o_group_by_menu')]
+ //button
+ [contains(@class, 'o_menu_item')]
+ [contains(., 'Date')]
+ /following-sibling::div`;
+ const groupByDateSubMenuElement = getFirstElementForXpath(groupByDateSubMenuXpath);
+ const selectedGroupByDateItemElements = groupByDateSubMenuElement.querySelectorAll('span.o_item_option.selected');
+ assert.equal(selectedGroupByDateItemElements.length, 1, 'There is only one selected item.');
+ assert.equal(firstNotSelectedGroupByDateItemElement.innerText, selectedGroupByDateItemElements[0].innerText, 'The selected item is the one we clicked on.');
+ });
+
+ function checkGroupByOrder(assert) {
+ const dateSearchFacetXpath = `//div[contains(@class, 'o_searchview_facet')]
+ [.//span[@class='o_facet_value']
+ [contains(., 'Date: Month')]]`;
+ const dateSearchFacetElement = getFirstElementForXpath(dateSearchFacetXpath);
+ const dateSearchFacetParts = dateSearchFacetElement.querySelectorAll('.o_facet_value');
+ assert.equal(dateSearchFacetParts.length, 2);
+ assert.equal(dateSearchFacetParts[0].innerText, 'Date: Month');
+ assert.equal(dateSearchFacetParts[1].innerText, 'Stage');
+ }
+
+ QUnit.test("check that the group by is always sorted 'Date' first, 'Stage' second", async function (assert) {
+ await makeBurnDownChartWithSearchView({context: {...makeViewParams.context, 'search_default_date': 1, 'search_default_stage': 1}});
+ checkGroupByOrder(assert);
+ });
+
+ QUnit.test("check that the group by is always sorted 'Date' first, 'Stage' second", async function (assert) {
+ await makeBurnDownChartWithSearchView({context: {...makeViewParams.context, 'search_default_stage': 1, 'search_default_date': 1}});
+ checkGroupByOrder(assert);
+ });
});
});
diff --git a/addons/project/static/tests/tours/project_burndown_chart_tour.js b/addons/project/static/tests/tours/project_burndown_chart_tour.js
new file mode 100644
index 00000000000..ebc3d2e7c28
--- /dev/null
+++ b/addons/project/static/tests/tours/project_burndown_chart_tour.js
@@ -0,0 +1,85 @@
+/** @odoo-module */
+
+import tour from 'web_tour.tour';
+
+tour.register('burndown_chart_tour', {
+ test: true,
+ url: '/web',
+},
+[tour.stepUtils.showAppsMenuItem(), {
+ trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
+}, {
+ content: 'Open "Burndown Chart Test" project menu',
+ trigger: '.o_kanban_record:contains("Burndown Chart Test") .o_kanban_manage_toggle_button',
+}, {
+ content: `Open "Burndown Chart Test" project's "Burndown Chart" view`,
+ trigger: '.o_kanban_record:contains("Burndown Chart Test") .o_kanban_manage_reporting div[role="menuitem"] a:contains("Burndown Chart")',
+}, {
+ content: 'The sort buttons are not rendered',
+ trigger: '.o_cp_bottom_left:not(:has(.btn-group[role=toolbar][aria-label="Sort graph"]))',
+ extra_trigger: '.o_graph_renderer',
+}, {
+ content: 'Remove the project search "Burndown Chart Test"',
+ trigger: '.o_searchview_facet:contains("Burndown Chart Test") .o_facet_remove',
+}, {
+ content: 'Search Burndown Chart',
+ trigger: 'input.o_searchview_input',
+ run: `text Burndown`,
+}, {
+ content: 'Validate search',
+ trigger: '.o_searchview_autocomplete .o_menu_item:contains("Project")',
+}, {
+ content: 'Remove the group by "Date: Month > Stage"',
+ trigger: '.o_searchview_facet:contains("Date: Month") .o_facet_remove',
+}, {
+ content: 'A "The Burndown Chart must be grouped by Date and Stage" notification is shown when trying to remove the group by "Date: Month > Stage"',
+ trigger: '.o_notification_manager .o_notification:contains("The Burndown Chart must be grouped by Date and Stage") button.o_notification_close',
+}, {
+ content: 'Open the group by menu',
+ trigger: '.o_group_by_menu button',
+}, {
+ content: 'Click on the Stage group menu item',
+ trigger: '.o_group_by_menu .o_menu_item:contains("Stage")',
+}, {
+ content: 'A "The Burndown Chart must be grouped by Stage" notification is shown when trying to remove the group by "Date: Month > Stage"',
+ trigger: '.o_notification_manager .o_notification:contains("The Burndown Chart must be grouped by Stage") button.o_notification_close',
+}, {
+ content: 'Open the group by menu',
+ trigger: '.o_group_by_menu button',
+}, {
+ content: 'Open the Date group by sub menu',
+ trigger: '.o_group_by_menu button.o_menu_item:contains("Date")',
+ run: function () {
+ this.$anchor[0].dispatchEvent(new Event('mouseenter'));
+ },
+}, {
+ content: 'Click on the selected Date sub menu',
+ trigger: '.o_group_by_menu button.o_menu_item:contains("Date") + * .dropdown-item.selected',
+ run: function () {
+ this.$anchor[0].dispatchEvent(new Event('click'));
+ },
+}, {
+ content: 'A "The Burndown Chart must be grouped by Date" notification is shown when trying to remove the group by "Date: Month > Stage"',
+ trigger: '.o_notification_manager .o_notification:contains("The Burndown Chart must be grouped by Date") button.o_notification_close',
+}, {
+ content: 'Open the filter menu',
+ trigger: '.o_filter_menu button',
+}, {
+ content: 'Open the Date filter sub menu',
+ trigger: '.o_filter_menu button.o_menu_item:contains("Date")',
+ run: function () {
+ this.$anchor[0].dispatchEvent(new Event('mouseenter'));
+ },
+}, {
+ content: 'Click on the first Date filter sub menu',
+ trigger: '.o_filter_menu .o_menu_item:contains("Date") + * .dropdown-item:first-child',
+ run: function () {
+ this.$anchor[0].dispatchEvent(new Event('click'));
+ },
+}, {
+ content: 'Close the Date filter menu',
+ trigger: '.o_graph_renderer',
+}, {
+ content: 'The comparison menu is not rendered',
+ trigger: '.o_search_options:not(:has(.o_comparison_menu))',
+}]);
diff --git a/addons/project/tests/test_burndown_chart.py b/addons/project/tests/test_burndown_chart.py
index f22b6ec1667..dfcb50bf78e 100644
--- a/addons/project/tests/test_burndown_chart.py
+++ b/addons/project/tests/test_burndown_chart.py
@@ -5,174 +5,331 @@ from freezegun import freeze_time
from datetime import datetime
from odoo import Command
-from odoo.tests.common import TransactionCase
+from odoo.osv.expression import AND, OR
+from odoo.tests.common import tagged, HttpCase
+from .test_project_base import TestProjectCommon
-class TestBurndownChart(TransactionCase):
- def set_create_date(self, table, res_id, create_date):
- self.env.cr.execute("UPDATE {} SET create_date=%s WHERE id=%s".format(table), (create_date, res_id))
+class TestBurndownChartCommon(TestProjectCommon):
- def test_burndown_chart(self):
- current_year = datetime.now().year
- create_date = datetime(current_year - 1, 1, 1)
+ @classmethod
+ def set_create_date(cls, table, res_id, create_date):
+ cls.env.cr.execute("UPDATE {} SET create_date=%s WHERE id=%s".format(table), (create_date, res_id))
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.current_year = datetime.now().year
+ create_date = datetime(cls.current_year - 1, 1, 1)
kanban_state_vals = {
"legend_blocked": 'Blocked',
"legend_done": 'Ready',
"legend_normal": 'In Progress'
}
- Stage = self.env['project.task.type']
- todo_stage = Stage.create({
+ Stage = cls.env['project.task.type']
+ cls.todo_stage = Stage.create({
'sequence': 1,
'name': 'TODO',
**kanban_state_vals,
})
- self.set_create_date('project_task_type', todo_stage.id, create_date)
- in_progress_stage = Stage.create({
+ cls.set_create_date('project_task_type', cls.todo_stage.id, create_date)
+ cls.in_progress_stage = Stage.create({
'sequence': 10,
'name': 'In Progress',
**kanban_state_vals,
})
- self.set_create_date('project_task_type', in_progress_stage.id, create_date)
- testing_stage = Stage.create({
+ cls.set_create_date('project_task_type', cls.in_progress_stage.id, create_date)
+ cls.testing_stage = Stage.create({
'sequence': 20,
'name': 'Testing',
**kanban_state_vals,
})
- self.set_create_date('project_task_type', testing_stage.id, create_date)
- done_stage = Stage.create({
+ cls.set_create_date('project_task_type', cls.testing_stage.id, create_date)
+ cls.done_stage = Stage.create({
'sequence': 30,
'name': 'Done',
**kanban_state_vals,
})
- self.set_create_date('project_task_type', done_stage.id, create_date)
- stages = todo_stage + in_progress_stage + testing_stage + done_stage
- project = self.env['project.project'].create({
+ cls.set_create_date('project_task_type', cls.done_stage.id, create_date)
+ cls.stages = cls.todo_stage + cls.in_progress_stage + cls.testing_stage + cls.done_stage
+ cls.project = cls.env['project.project'].create({
'name': 'Burndown Chart Test',
'privacy_visibility': 'employees',
'alias_name': 'project+burndown_chart',
- 'type_ids': [Command.link(stage_id) for stage_id in stages.ids],
+ 'type_ids': [Command.link(stage_id) for stage_id in cls.stages.ids],
})
- self.set_create_date('project_project', project.id, create_date)
- project.invalidate_model()
- task_a = self.env['project.task'].create({
+ cls.set_create_date('project_project', cls.project.id, create_date)
+ cls.project.invalidate_model()
+ cls.milestone = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
+ 'name': 'Test Milestone',
+ 'project_id': cls.project_pigs.id,
+ })
+ cls.task_a = cls.env['project.task'].create({
'name': 'Task A',
'priority': 0,
- 'project_id': project.id,
- 'stage_id': todo_stage.id,
+ 'project_id': cls.project.id,
+ 'stage_id': cls.todo_stage.id,
})
- self.set_create_date('project_task', task_a.id, create_date)
- task_b = task_a.copy({
+ cls.set_create_date('project_task', cls.task_a.id, create_date)
+ cls.task_b = cls.task_a.copy({
'name': 'Task B',
+ 'user_ids': [Command.set([cls.user_projectuser.id, cls.user_projectmanager.id])],
})
- self.set_create_date('project_task', task_b.id, create_date)
- task_c = task_a.copy({
+ cls.set_create_date('project_task', cls.task_b.id, create_date)
+ cls.task_c = cls.task_a.copy({
'name': 'Task C',
+ 'partner_id': cls.partner_1.id,
+ 'user_ids': [Command.link(cls.user_projectuser.id)],
})
- self.set_create_date('project_task', task_c.id, create_date)
- task_d = task_a.copy({
+ cls.set_create_date('project_task', cls.task_c.id, create_date)
+ cls.task_d = cls.task_a.copy({
'name': 'Task D',
+ 'milestone_id': cls.milestone.id,
+ 'user_ids': [Command.link(cls.user_projectmanager.id)],
})
- self.set_create_date('project_task', task_d.id, create_date)
- task_e = task_a.copy({
+ cls.set_create_date('project_task', cls.task_d.id, create_date)
+ cls.task_e = cls.task_a.copy({
'name': 'Task E',
+ 'partner_id': cls.partner_1.id,
})
- self.set_create_date('project_task', task_e.id, create_date)
+ cls.set_create_date('project_task', cls.task_e.id, create_date)
# Create a new task to check if a task without changing its stage is taken into account
- task_f = self.env['project.task'].create({
+ task_f = cls.env['project.task'].create({
'name': 'Task F',
'priority': 0,
- 'project_id': project.id,
- 'stage_id': todo_stage.id,
+ 'project_id': cls.project.id,
+ 'milestone_id': cls.milestone.id,
+ 'stage_id': cls.todo_stage.id,
})
- self.set_create_date('project_task', task_f.id, datetime(current_year - 1, 12, 20))
+ cls.set_create_date('project_task', task_f.id, datetime(cls.current_year - 1, 12, 20))
+
+ cls.project_2 = cls.env['project.project'].create({
+ 'name': 'Burndown Chart Test 2 mySearchTag',
+ 'privacy_visibility': 'employees',
+ 'alias_name': 'project+burndown_chart+2',
+ 'type_ids': [Command.link(stage_id) for stage_id in cls.stages.ids],
+ })
+ cls.set_create_date('project_project', cls.project_2.id, create_date)
+ cls.project.invalidate_model()
+ cls.task_g = cls.env['project.task'].create({
+ 'name': 'Task G',
+ 'priority': 0,
+ 'project_id': cls.project_2.id,
+ 'stage_id': cls.todo_stage.id,
+ 'user_ids': [Command.link(cls.user_projectuser.id)],
+ })
+ cls.set_create_date('project_task', cls.task_g.id, create_date)
+ cls.task_h = cls.task_g.copy({
+ 'name': 'Task H',
+ 'user_ids': [Command.link(cls.user_projectmanager.id)],
+ })
+ cls.set_create_date('project_task', cls.task_h.id, create_date)
# Precommit to have the records in db and allow to rollback at the end of test
- self.env.cr.flush()
+ cls.env.cr.flush()
- with freeze_time('%s-02-10' % (current_year - 1)):
- (task_a + task_b).write({'stage_id': in_progress_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-02-10' % (cls.current_year - 1)):
+ (cls.task_a + cls.task_b).write({'stage_id': cls.in_progress_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-02-20' % (current_year - 1)):
- task_c.write({'stage_id': in_progress_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-02-20' % (cls.current_year - 1)):
+ cls.task_c.write({'stage_id': cls.in_progress_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-03-15' % (current_year - 1)):
- (task_d + task_e).write({'stage_id': in_progress_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-03-15' % (cls.current_year - 1)):
+ (cls.task_d + cls.task_e).write({'stage_id': cls.in_progress_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-04-10' % (current_year - 1)):
- (task_a + task_b).write({'stage_id': testing_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-04-10' % (cls.current_year - 1)):
+ (cls.task_a + cls.task_b).write({'stage_id': cls.testing_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-05-12' % (current_year - 1)):
- task_c.write({'stage_id': testing_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-05-12' % (cls.current_year - 1)):
+ cls.task_c.write({'stage_id': cls.testing_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-06-25' % (current_year - 1)):
- task_d.write({'stage_id': testing_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-06-25' % (cls.current_year - 1)):
+ cls.task_d.write({'stage_id': cls.testing_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-07-25' % (current_year - 1)):
- task_e.write({'stage_id': testing_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-07-25' % (cls.current_year - 1)):
+ cls.task_e.write({'stage_id': cls.testing_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-08-01' % (current_year - 1)):
- task_a.write({'stage_id': done_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-08-01' % (cls.current_year - 1)):
+ cls.task_a.write({'stage_id': cls.done_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-09-10' % (current_year - 1)):
- task_b.write({'stage_id': done_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-09-10' % (cls.current_year - 1)):
+ cls.task_b.write({'stage_id': cls.done_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-10-05' % (current_year - 1)):
- task_c.write({'stage_id': done_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-10-05' % (cls.current_year - 1)):
+ cls.task_c.write({'stage_id': cls.done_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-11-25' % (current_year - 1)):
- task_d.write({'stage_id': done_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-11-25' % (cls.current_year - 1)):
+ cls.task_d.write({'stage_id': cls.done_stage.id})
+ cls.env.cr.flush()
- with freeze_time('%s-12-12' % (current_year - 1)):
- task_e.write({'stage_id': done_stage.id})
- self.env.cr.flush()
+ with freeze_time('%s-12-12' % (cls.current_year - 1)):
+ cls.task_e.write({'stage_id': cls.done_stage.id})
+ cls.env.cr.flush()
- read_group_result = self.env['project.task.burndown.chart.report'].with_context(fill_temporal=True).read_group([('project_id', '=', project.id), ('display_project_id', '!=', False)], ['date', 'stage_id'], ['date:month', 'stage_id'], lazy=False)
- read_group_result_dict = {(res['date:month'], res['stage_id'][0]): res['__count'] for res in read_group_result}
- stages_dict = {stage.id: stage.name for stage in stages}
- expected_dict = {
- ('January %s' % (current_year - 1), todo_stage.id): 5,
- ('February %s' % (current_year - 1), todo_stage.id): 2,
- ('February %s' % (current_year - 1), in_progress_stage.id): 3,
- ('March %s' % (current_year - 1), in_progress_stage.id): 5,
- ('April %s' % (current_year - 1), in_progress_stage.id): 3,
- ('April %s' % (current_year - 1), testing_stage.id): 2,
- ('May %s' % (current_year - 1), in_progress_stage.id): 2,
- ('May %s' % (current_year - 1), testing_stage.id): 3,
- ('June %s' % (current_year - 1), in_progress_stage.id): 1,
- ('June %s' % (current_year - 1), testing_stage.id): 4,
- ('July %s' % (current_year - 1), testing_stage.id): 5,
- ('August %s' % (current_year - 1), testing_stage.id): 4,
- ('August %s' % (current_year - 1), done_stage.id): 1,
- ('September %s' % (current_year - 1), testing_stage.id): 3,
- ('September %s' % (current_year - 1), done_stage.id): 2,
- ('October %s' % (current_year - 1), testing_stage.id): 2,
- ('October %s' % (current_year - 1), done_stage.id): 3,
- ('November %s' % (current_year - 1), testing_stage.id): 1,
- ('November %s' % (current_year - 1), done_stage.id): 4,
- ('December %s' % (current_year - 1), done_stage.id): 5,
- ('December %s' % (current_year - 1), todo_stage.id): 1,
- ('January %s' % (current_year), done_stage.id): 5,
- ('January %s' % (current_year), todo_stage.id): 1,
- }
+
+class TestBurndownChart(TestBurndownChartCommon):
+
+ def map_read_group_result(self, read_group_result):
+ return {(res['date:month'], res['stage_id'][0]): res['__count'] for res in read_group_result if res['stage_id'][1]}
+
+ def check_read_group_results(self, domain, expected_results_dict):
+ stages_dict = {stage.id: stage.name for stage in self.stages}
+ read_group_result = self.env['project.task.burndown.chart.report'].read_group(
+ domain, ['date', 'stage_id'], ['date:month', 'stage_id'], lazy=False)
+ read_group_result_dict = self.map_read_group_result(read_group_result)
for (month, stage_id), __count in read_group_result_dict.items():
- # when we don't found any record in the dict then we are in the current_year
- # and the number of tasks should always be 5 in Done stage and 1 in Todo Stage
- # since we have created the last task without changing its stage.
- expected_count = expected_dict.get((month, stage_id), 5 if stage_id != todo_stage.id else 1)
+ expected_count = expected_results_dict.get((month, stage_id), 100000)
self.assertEqual(
__count,
expected_count,
'In %s, the number of tasks should be equal to %s in %s stage.' % (month, expected_count, stages_dict.get(stage_id, 'Unknown'))
)
+
+ def test_burndown_chart(self):
+ burndown_chart_domain = [('display_project_id', '!=', False)]
+ project_domain = [('project_id', '=', self.project.id)]
+
+ # Check that we get the expected results for the complete data of `self.project`.
+ project_expected_dict = {
+ ('January %s' % (self.current_year - 1), self.todo_stage.id): 5,
+ ('January %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
+ ('January %s' % (self.current_year - 1), self.testing_stage.id): 0,
+ ('January %s' % (self.current_year - 1), self.done_stage.id): 0,
+ ('February %s' % (self.current_year - 1), self.todo_stage.id): 2,
+ ('February %s' % (self.current_year - 1), self.in_progress_stage.id): 3,
+ ('February %s' % (self.current_year - 1), self.testing_stage.id): 0,
+ ('February %s' % (self.current_year - 1), self.done_stage.id): 0,
+ ('March %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('March %s' % (self.current_year - 1), self.in_progress_stage.id): 5,
+ ('March %s' % (self.current_year - 1), self.testing_stage.id): 0,
+ ('March %s' % (self.current_year - 1), self.done_stage.id): 0,
+ ('April %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('April %s' % (self.current_year - 1), self.in_progress_stage.id): 3,
+ ('April %s' % (self.current_year - 1), self.testing_stage.id): 2,
+ ('April %s' % (self.current_year - 1), self.done_stage.id): 0,
+ ('May %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('May %s' % (self.current_year - 1), self.in_progress_stage.id): 2,
+ ('May %s' % (self.current_year - 1), self.testing_stage.id): 3,
+ ('May %s' % (self.current_year - 1), self.done_stage.id): 0,
+ ('June %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('June %s' % (self.current_year - 1), self.in_progress_stage.id): 1,
+ ('June %s' % (self.current_year - 1), self.testing_stage.id): 4,
+ ('June %s' % (self.current_year - 1), self.done_stage.id): 0,
+ ('July %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('July %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
+ ('July %s' % (self.current_year - 1), self.testing_stage.id): 5,
+ ('July %s' % (self.current_year - 1), self.done_stage.id): 0,
+ ('August %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('August %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
+ ('August %s' % (self.current_year - 1), self.testing_stage.id): 4,
+ ('August %s' % (self.current_year - 1), self.done_stage.id): 1,
+ ('September %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('September %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
+ ('September %s' % (self.current_year - 1), self.testing_stage.id): 3,
+ ('September %s' % (self.current_year - 1), self.done_stage.id): 2,
+ ('October %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('October %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
+ ('October %s' % (self.current_year - 1), self.testing_stage.id): 2,
+ ('October %s' % (self.current_year - 1), self.done_stage.id): 3,
+ ('November %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('November %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
+ ('November %s' % (self.current_year - 1), self.testing_stage.id): 1,
+ ('November %s' % (self.current_year - 1), self.done_stage.id): 4,
+ ('December %s' % (self.current_year - 1), self.todo_stage.id): 0,
+ ('December %s' % (self.current_year - 1), self.in_progress_stage.id): 0,
+ ('December %s' % (self.current_year - 1), self.done_stage.id): 5,
+ ('December %s' % (self.current_year - 1), self.todo_stage.id): 1,
+ ('January %s' % (self.current_year), self.todo_stage.id): 0,
+ ('January %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('January %s' % (self.current_year), self.done_stage.id): 5,
+ ('January %s' % (self.current_year), self.todo_stage.id): 1,
+ ('February %s' % (self.current_year), self.todo_stage.id): 0,
+ ('February %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('February %s' % (self.current_year), self.done_stage.id): 5,
+ ('February %s' % (self.current_year), self.todo_stage.id): 1,
+ ('March %s' % (self.current_year), self.todo_stage.id): 0,
+ ('March %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('March %s' % (self.current_year), self.done_stage.id): 5,
+ ('March %s' % (self.current_year), self.todo_stage.id): 1,
+ ('April %s' % (self.current_year), self.todo_stage.id): 0,
+ ('April %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('April %s' % (self.current_year), self.done_stage.id): 5,
+ ('April %s' % (self.current_year), self.todo_stage.id): 1,
+ ('May %s' % (self.current_year), self.todo_stage.id): 0,
+ ('May %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('May %s' % (self.current_year), self.done_stage.id): 5,
+ ('May %s' % (self.current_year), self.todo_stage.id): 1,
+ ('June %s' % (self.current_year), self.todo_stage.id): 0,
+ ('June %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('June %s' % (self.current_year), self.done_stage.id): 5,
+ ('June %s' % (self.current_year), self.todo_stage.id): 1,
+ ('July %s' % (self.current_year), self.todo_stage.id): 0,
+ ('July %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('July %s' % (self.current_year), self.done_stage.id): 5,
+ ('July %s' % (self.current_year), self.todo_stage.id): 1,
+ ('August %s' % (self.current_year), self.todo_stage.id): 0,
+ ('August %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('August %s' % (self.current_year), self.done_stage.id): 5,
+ ('August %s' % (self.current_year), self.todo_stage.id): 1,
+ ('September %s' % (self.current_year), self.todo_stage.id): 0,
+ ('September %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('September %s' % (self.current_year), self.done_stage.id): 5,
+ ('September %s' % (self.current_year), self.todo_stage.id): 1,
+ ('October %s' % (self.current_year), self.todo_stage.id): 0,
+ ('October %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('October %s' % (self.current_year), self.done_stage.id): 5,
+ ('October %s' % (self.current_year), self.todo_stage.id): 1,
+ ('November %s' % (self.current_year), self.todo_stage.id): 0,
+ ('November %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('November %s' % (self.current_year), self.done_stage.id): 5,
+ ('November %s' % (self.current_year), self.todo_stage.id): 1,
+ ('December %s' % (self.current_year), self.todo_stage.id): 0,
+ ('December %s' % (self.current_year), self.in_progress_stage.id): 0,
+ ('December %s' % (self.current_year), self.done_stage.id): 5,
+ ('December %s' % (self.current_year), self.todo_stage.id): 1,
+ }
+ self.check_read_group_results(AND([burndown_chart_domain, project_domain]), project_expected_dict)
+
+ # Check that we get the expected results for the complete data of `self.project` & `self.project_2` using an
+ # `ilike` in the domain.
+ all_projects_domain_with_ilike = OR([project_domain, [('project_id', 'ilike', 'mySearchTag')]])
+ project_expected_dict = {key: val if key[1] != self.todo_stage.id else val + 2 for key, val in project_expected_dict.items()}
+ self.check_read_group_results(AND([burndown_chart_domain, all_projects_domain_with_ilike]), project_expected_dict)
+
+ date_from, date_to = ('%s-01-01' % (self.current_year - 1), '%s-02-01' % (self.current_year - 1))
+ date_and_user_domain = [('date', '>=', date_from), ('date', '<', date_to), ('user_ids', 'ilike', 'ProjectUser')]
+ complex_domain_expected_dict = {
+ ('January %s' % (self.current_year - 1), self.todo_stage.id): 3,
+ ('February %s' % (self.current_year - 1), self.todo_stage.id): 1,
+ ('February %s' % (self.current_year - 1), self.in_progress_stage.id): 2,
+ }
+ complex_domain = AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain])
+ self.check_read_group_results(complex_domain, complex_domain_expected_dict)
+
+ date_and_user_domain = [('date', '>=', date_from), ('date', '<', date_to), ('user_ids', 'ilike', 'ProjectManager')]
+ milestone_domain = [('milestone_id', 'ilike', 'Test')]
+ complex_domain = AND([burndown_chart_domain, all_projects_domain_with_ilike, date_and_user_domain, milestone_domain])
+ complex_domain_expected_dict = {
+ ('January %s' % (self.current_year - 1), self.todo_stage.id): 1,
+ ('February %s' % (self.current_year - 1), self.todo_stage.id): 1,
+ }
+ self.check_read_group_results(complex_domain, complex_domain_expected_dict)
+
+
+@tagged('-at_install', 'post_install')
+class TestBurndownChartTour(HttpCase, TestBurndownChartCommon):
+
+ def test_burndown_chart_tour(self):
+ # Test customizing personal stages as a project user
+ self.start_tour('/web', 'burndown_chart_tour', login="admin")
diff --git a/addons/project/tests/test_project_base.py b/addons/project/tests/test_project_base.py
index 394e8fe8925..c83db73de51 100644
--- a/addons/project/tests/test_project_base.py
+++ b/addons/project/tests/test_project_base.py
@@ -3,6 +3,7 @@
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
+
class TestProjectCommon(TransactionCase):
@classmethod
@@ -94,6 +95,9 @@ class TestProjectCommon(TransactionCase):
self.env['mail.thread'].message_process(model, mail)
return self.env[target_model].search([(target_field, '=', subject)])
+
+class TestProjectBase(TestProjectCommon):
+
def test_delete_project_with_tasks(self):
"""User should never be able to delete a project with tasks"""
diff --git a/addons/web/static/src/search/control_panel/control_panel.js b/addons/web/static/src/search/control_panel/control_panel.js
index 9db06294bdf..f4cb99b373b 100644
--- a/addons/web/static/src/search/control_panel/control_panel.js
+++ b/addons/web/static/src/search/control_panel/control_panel.js
@@ -95,10 +95,7 @@ export class ControlPanel extends Component {
}
ControlPanel.components = {
- ComparisonMenu,
- FavoriteMenu,
- FilterMenu,
- GroupByMenu,
+ ...Object.values(MAPPING),
Pager,
SearchBar,
};
diff --git a/addons/web/static/src/search/group_by_menu/group_by_menu.js b/addons/web/static/src/search/group_by_menu/group_by_menu.js
index a99b034cdf2..4c5d8223561 100644
--- a/addons/web/static/src/search/group_by_menu/group_by_menu.js
+++ b/addons/web/static/src/search/group_by_menu/group_by_menu.js
@@ -26,6 +26,13 @@ export class GroupByMenu extends Component {
useBus(this.env.searchModel, "update", this.render);
}
+ /**
+ * @returns {boolean}
+ */
+ get hideCustomGroupBy() {
+ return this.env.searchModel.hideCustomGroupBy || false;
+ }
+
/**
* @returns {Object[]}
*/
diff --git a/addons/web/static/src/search/group_by_menu/group_by_menu.xml b/addons/web/static/src/search/group_by_menu/group_by_menu.xml
index b2911a39b41..d6e90e5e0a4 100644
--- a/addons/web/static/src/search/group_by_menu/group_by_menu.xml
+++ b/addons/web/static/src/search/group_by_menu/group_by_menu.xml
@@ -43,7 +43,7 @@
-
+
diff --git a/addons/web/static/src/search/search_model.js b/addons/web/static/src/search/search_model.js
index 3396494808f..e3b17b43d0a 100644
--- a/addons/web/static/src/search/search_model.js
+++ b/addons/web/static/src/search/search_model.js
@@ -212,13 +212,14 @@ export class SearchModel extends EventBus {
// used to avoid useless recomputations
this._reset();
- const { comparison, context, domain, groupBy, orderBy } = config;
+ const { comparison, context, domain, groupBy, hideCustomGroupBy, orderBy } = config;
this.globalComparison = comparison;
this.globalContext = toRaw(context || {});
this.globalDomain = domain || [];
this.globalGroupBy = groupBy || [];
this.globalOrderBy = orderBy || [];
+ this.hideCustomGroupBy = hideCustomGroupBy;
this.searchMenuTypes = new Set(config.searchMenuTypes || ["filter", "groupBy", "favorite"]);
diff --git a/addons/web/static/src/search/with_search/with_search.js b/addons/web/static/src/search/with_search/with_search.js
index 1c7b3367d19..5eded123282 100644
--- a/addons/web/static/src/search/with_search/with_search.js
+++ b/addons/web/static/src/search/with_search/with_search.js
@@ -86,5 +86,6 @@ WithSearch.props = {
// extra options
activateFavorite: { type: Boolean, optional: true },
dynamicFilters: { type: Array, element: Object, optional: true },
+ hideCustomGroupBy: { type: Boolean, optional: true },
searchMenuTypes: { type: Array, element: String, optional: true },
};
diff --git a/addons/web/static/src/views/view.js b/addons/web/static/src/views/view.js
index e4c467a8437..dbaad5586f8 100644
--- a/addons/web/static/src/views/view.js
+++ b/addons/web/static/src/views/view.js
@@ -85,6 +85,7 @@ export function getDefaultConfig() {
*
* @property {boolean} [activateFavorite]
* @property {Object[]} [dynamicFilters]
+ * @property {boolean} [hideCustomGroupBy]
* @property {string[]} [searchMenuTypes]
* @property {Object} [globalState]
*/
@@ -123,6 +124,7 @@ const STANDARD_PROPS = [
"activateFavorite",
"dynamicFilters",
+ "hideCustomGroupBy",
"searchMenuTypes",
// LEGACY: remove this later (clean when mappings old state <-> new state are established)
@@ -321,6 +323,7 @@ export class View extends Component {
this.componentProps = finalProps;
this.withSearchProps = {
...toRaw(props),
+ hideCustomGroupBy: props.hideCustomGroupBy || descr.hideCustomGroupBy,
searchMenuTypes,
SearchModel: descr.SearchModel,
};
diff --git a/addons/web/static/tests/search/group_by_menu_tests.js b/addons/web/static/tests/search/group_by_menu_tests.js
index f9fe8f2ef61..d8eb350c2f0 100644
--- a/addons/web/static/tests/search/group_by_menu_tests.js
+++ b/addons/web/static/tests/search/group_by_menu_tests.js
@@ -457,4 +457,69 @@ QUnit.module("Search", (hooks) => {
assert.deepEqual(controlPanel.env.searchModel.groupBy, []);
assert.deepEqual(getFacetTexts(target), []);
});
+
+ QUnit.test("Custom group by menu is displayed when hideCustomGroupBy is not set", async function (assert) {
+
+ await makeWithSearch({
+ serverData,
+ resModel: "foo",
+ Component: ControlPanel,
+ searchViewId: false,
+ searchViewArch: `
+
+
+
+
+ `,
+ searchMenuTypes: ["groupBy"],
+ });
+
+ await toggleGroupByMenu(target);
+
+ assert.containsOnce(target, ".o_add_custom_group_menu");
+ });
+
+ QUnit.test("Custom group by menu is displayed when hideCustomGroupBy is false", async function (assert) {
+
+ await makeWithSearch({
+ serverData,
+ resModel: "foo",
+ Component: ControlPanel,
+ searchViewId: false,
+ searchViewArch: `
+
+
+
+
+ `,
+ hideCustomGroupBy: false,
+ searchMenuTypes: ["groupBy"],
+ });
+
+ await toggleGroupByMenu(target);
+
+ assert.containsOnce(target, ".o_add_custom_group_menu");
+ });
+
+ QUnit.test("Custom group by menu is displayed when hideCustomGroupBy is true", async function (assert) {
+
+ await makeWithSearch({
+ serverData,
+ resModel: "foo",
+ Component: ControlPanel,
+ searchViewId: false,
+ searchViewArch: `
+
+
+
+
+ `,
+ hideCustomGroupBy: true,
+ searchMenuTypes: ["groupBy"],
+ });
+
+ await toggleGroupByMenu(target);
+
+ assert.containsNone(target, ".o_add_custom_group_menu");
+ });
});