diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py
index ea6d9ca9248..e0c820458f9 100644
--- a/addons/project/__manifest__.py
+++ b/addons/project/__manifest__.py
@@ -70,6 +70,7 @@
'project/static/src/components/**/*',
'project/static/src/views/**/*',
'project/static/src/js/project_graph_view.js',
+ 'project/static/src/js/project_pivot_model.js',
'project/static/src/js/project_pivot_view.js',
'project/static/src/js/project_rating_graph_view.js',
'project/static/src/js/project_rating_pivot_view.js',
diff --git a/addons/project/static/src/js/project_pivot_model.js b/addons/project/static/src/js/project_pivot_model.js
new file mode 100644
index 00000000000..3cb7b55320a
--- /dev/null
+++ b/addons/project/static/src/js/project_pivot_model.js
@@ -0,0 +1,18 @@
+/** @odoo-module **/
+
+import { PivotModel } from "@web/views/pivot/pivot_model";
+
+export class ProjectTaskPivotModel extends PivotModel {
+ /**
+ * @override
+ */
+ _getEmptyGroupLabel(fieldName) {
+ if (fieldName === "project_id") {
+ return this.env._t("Private");
+ } else if (fieldName === "user_ids") {
+ return this.env._t("Unassigned");
+ } else {
+ return super._getEmptyGroupLabel(fieldName);
+ }
+ }
+}
diff --git a/addons/project/static/src/js/project_pivot_view.js b/addons/project/static/src/js/project_pivot_view.js
index e821b6fe0c2..ce1e1954e5c 100644
--- a/addons/project/static/src/js/project_pivot_view.js
+++ b/addons/project/static/src/js/project_pivot_view.js
@@ -3,7 +3,12 @@
import { ProjectControlPanel } from "@project/components/project_control_panel/project_control_panel";
import { registry } from "@web/core/registry";
import { pivotView } from "@web/views/pivot/pivot_view";
+import { ProjectTaskPivotModel } from "./project_pivot_model";
-const projectPivotView = {...pivotView, ControlPanel: ProjectControlPanel};
+const projectPivotView = {
+ ...pivotView,
+ ControlPanel: ProjectControlPanel,
+ Model: ProjectTaskPivotModel,
+};
registry.category("views").add("project_pivot", projectPivotView);
diff --git a/addons/project/static/src/views/project_task_kanban/project_task_kanban_header.js b/addons/project/static/src/views/project_task_kanban/project_task_kanban_header.js
index 1bae963c11e..e3e84099a2f 100644
--- a/addons/project/static/src/views/project_task_kanban/project_task_kanban_header.js
+++ b/addons/project/static/src/views/project_task_kanban/project_task_kanban_header.js
@@ -63,4 +63,17 @@ export class ProjectTaskKanbanHeader extends KanbanHeader {
canDeleteGroup(group) {
return super.canDeleteGroup(group) && (!this.props.list.isGroupedByStage || this.isProjectManager) || this.props.list.isGroupedByPersonalStages;
}
+
+ /**
+ * @override
+ */
+ _getEmptyGroupLabel(fieldName) {
+ if (fieldName === "project_id") {
+ return this.env._t("🔒 Private");
+ } else if (fieldName === "user_ids") {
+ return this.env._t("👤 Unassigned");
+ } else {
+ return super._getEmptyGroupLabel(fieldName);
+ }
+ }
}
diff --git a/addons/project/static/src/views/project_task_list/project_task_list_renderer.js b/addons/project/static/src/views/project_task_list/project_task_list_renderer.js
index 8ff3e9d0552..84f3ac19e76 100644
--- a/addons/project/static/src/views/project_task_list/project_task_list_renderer.js
+++ b/addons/project/static/src/views/project_task_list/project_task_list_renderer.js
@@ -15,4 +15,15 @@ export class ProjectTaskListRenderer extends ListRenderer {
}
return readonly || super.isCellReadonly(column, record);
}
+
+ getGroupDisplayName(group) {
+ const { _t } = this.env;
+ if (group.groupByField.name === "project_id" && !group.value) {
+ return _t("🔒 Private");
+ } else if (group.groupByField.name === "user_ids" && !group.value) {
+ return _t("👤 Unassigned");
+ } else {
+ return super.getGroupDisplayName(group);
+ }
+ }
}
diff --git a/addons/project/static/tests/project_task_groupby_tests.js b/addons/project/static/tests/project_task_groupby_tests.js
new file mode 100644
index 00000000000..79d8d643260
--- /dev/null
+++ b/addons/project/static/tests/project_task_groupby_tests.js
@@ -0,0 +1,95 @@
+/** @odoo-module */
+
+import { getFixture } from "@web/../tests/helpers/utils";
+import { setupViewRegistries } from "@web/../tests/views/helpers";
+import { start, startServer } from "@mail/../tests/helpers/test_utils";
+
+let target;
+
+QUnit.module("Project", (hooks) => {
+ hooks.beforeEach(async () => {
+ const pyEnv = await startServer();
+ pyEnv.mockServer.models['project.task'] = {
+ fields: {
+ id: { string: "Id", type: "integer" },
+ display_name: { string: "Name", type: "char" },
+ project_id: {
+ string: "Project",
+ type: "many2one",
+ relation: "project.project",
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "My task",
+ project_id: false,
+ }],
+ };
+ target = getFixture();
+ setupViewRegistries();
+ });
+
+ QUnit.test("Test group label for empty project in tree", async function (assert) {
+ assert.expect(1);
+
+ const views = {
+ "project.task,false,list":
+ ``,
+ };
+ const { openView } = await start({
+ serverData: { views },
+ });
+ await openView({
+ res_model: "project.task",
+ views: [[false, "tree"]],
+ context: { group_by: ["project_id"] },
+ });
+
+ assert.strictEqual(target.querySelector(".o_group_name").innerText, "🔒 Private (1)");
+ });
+
+ QUnit.test("Test group label for empty project in kanban", async function (assert) {
+ assert.expect(1);
+
+ const views = {
+ "project.task,false,kanban":
+ `
+
+
+
+ `,
+ };
+ const { openView } = await start({
+ serverData: { views },
+ });
+ await openView({
+ res_model: "project.task",
+ views: [[false, "kanban"]],
+ });
+
+ assert.strictEqual(target.querySelector(".o_column_title").innerText, "🔒 Private (1)");
+ });
+
+ QUnit.test("Test group label for empty project in pivot", async function (assert) {
+ assert.expect(1);
+
+ const views = {
+ "project.task,false,pivot":
+ `
+
+ `,
+ };
+ const { openView } = await start({
+ serverData: { views },
+ });
+ await openView({
+ res_model: "project.task",
+ views: [[false, "pivot"]],
+ });
+
+ assert.strictEqual(
+ target.querySelector("tr:nth-of-type(2) .o_pivot_header_cell_closed").innerText,
+ "Private",
+ );
+ });
+});
diff --git a/addons/project/views/project_task_views.xml b/addons/project/views/project_task_views.xml
index 8600137dcf5..0a02f6a3913 100644
--- a/addons/project/views/project_task_views.xml
+++ b/addons/project/views/project_task_views.xml
@@ -19,6 +19,7 @@
+
diff --git a/addons/web/static/src/views/kanban/kanban_header.js b/addons/web/static/src/views/kanban/kanban_header.js
index 597201dbfd7..6d20a40138a 100644
--- a/addons/web/static/src/views/kanban/kanban_header.js
+++ b/addons/web/static/src/views/kanban/kanban_header.js
@@ -75,6 +75,10 @@ export class KanbanHeader extends Component {
return this.props.group;
}
+ _getEmptyGroupLabel(fieldName) {
+ return this.env._t("None");
+ }
+
get groupName() {
const { groupByField, displayName } = this.group;
let name = displayName;
@@ -87,7 +91,7 @@ export class KanbanHeader extends Component {
groupByField.type === "datetime" ||
isNull(name)
) {
- name = this.env._t("None");
+ name = this._getEmptyGroupLabel(groupByField.name);
}
}
return name;
diff --git a/addons/web/static/src/views/pivot/pivot_model.js b/addons/web/static/src/views/pivot/pivot_model.js
index b833589624b..5e68fe5e31e 100644
--- a/addons/web/static/src/views/pivot/pivot_model.js
+++ b/addons/web/static/src/views/pivot/pivot_model.js
@@ -1525,6 +1525,11 @@ export class PivotModel extends Model {
}
});
}
+
+ _getEmptyGroupLabel(fieldName) {
+ return this.env._t("None");
+ }
+
/**
* Extract from a groupBy value a label.
*
@@ -1549,7 +1554,7 @@ export class PivotModel extends Model {
: this.env._t("No");
}
if (value === false) {
- return this.env._t("None");
+ return this._getEmptyGroupLabel(groupBy);
}
if (value instanceof Array) {
return this._getNumberedLabel(value, fieldName, config);