diff --git a/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_model.js b/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_model.js
index 040238f5cd2..d5497e38723 100644
--- a/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_model.js
+++ b/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_model.js
@@ -1,13 +1,13 @@
/** @odoo-module **/
-import { GraphModel } from "@web/views/graph/graph_model";
+import { ProjectTaskGraphModel } from "@project/views/project_task_graph/project_task_graph_model";
const FIELDS = [
'unit_amount', 'effective_hours', 'planned_hours', 'remaining_hours', 'total_hours_spent', 'subtask_effective_hours',
'overtime', 'number_hours', 'difference', 'timesheet_unit_amount'
];
-export class hrTimesheetGraphModel extends GraphModel {
+export class hrTimesheetGraphModel extends ProjectTaskGraphModel {
/**
* @override
*/
@@ -36,4 +36,4 @@ export class hrTimesheetGraphModel extends GraphModel {
return super._getProcessedDataPoints(...arguments);
}
}
-hrTimesheetGraphModel.services = [...GraphModel.services, "company"];
+hrTimesheetGraphModel.services = [...ProjectTaskGraphModel.services, "company"];
diff --git a/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_view.js b/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_view.js
index 79224d96edb..09e6b321d04 100644
--- a/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_view.js
+++ b/addons/hr_timesheet/static/src/views/timesheet_graph/timesheet_graph_view.js
@@ -1,13 +1,13 @@
/** @odoo-module **/
-import { projectGraphView } from "@project/views/project_task_graph/project_graph_view";
+import { projectTaskGraphView } from "@project/views/project_task_graph/project_task_graph_view";
import { hrTimesheetGraphModel } from "./timesheet_graph_model";
import { registry } from "@web/core/registry";
const viewRegistry = registry.category("views");
export const hrTimesheetGraphView = {
- ...projectGraphView,
+ ...projectTaskGraphView,
Model: hrTimesheetGraphModel,
};
diff --git a/addons/hr_timesheet/static/tests/timesheet_graph_tests.js b/addons/hr_timesheet/static/tests/timesheet_graph_tests.js
index 74198eef6cf..28fc94cf3b6 100644
--- a/addons/hr_timesheet/static/tests/timesheet_graph_tests.js
+++ b/addons/hr_timesheet/static/tests/timesheet_graph_tests.js
@@ -1,9 +1,14 @@
/** @odoo-module **/
import { companyService } from "@web/webclient/company_service";
-import { getGraphRenderer } from "@web/../tests/views/graph_view_tests";
+import {
+ checkLabels,
+ checkLegend,
+ getGraphRenderer,
+ selectMode
+} from "@web/../tests/views/graph_view_tests";
import { makeView } from "@web/../tests/views/helpers";
-import { patchWithCleanup } from "@web/../tests/helpers/utils";
+import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { session } from "@web/session";
import { registry } from "@web/core/registry";
import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
@@ -11,16 +16,65 @@ import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
const serviceRegistry = registry.category("services");
QUnit.module('hr_timesheet', function (hooks) {
- let serverData;
+ let serverData, target;
hooks.beforeEach(() => {
serverData = {
models: {
'account.analytic.line': {
fields: {
unit_amount: { string: "Unit Amount", type: "float", group_operator: "sum", store: true },
+ project_id: {
+ string: "Project",
+ type: "many2one",
+ relation: "project.project",
+ store: true,
+ sortable: true,
+ },
},
records: [
- { id: 1, unit_amount: 8 }
+ { id: 1, unit_amount: 8, project_id: false },
+ ],
+ },
+ "project.task": {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ name: { string: "Name", type: "char" },
+ milestone_id: {
+ string: "Milestone",
+ type: "many2one",
+ relation: "project.milestone",
+ store: true,
+ sortable: true,
+ },
+ project_id: {
+ string: "Project",
+ type: "many2one",
+ relation: "project.project",
+ store: true,
+ sortable: true,
+ },
+ },
+ records: [
+ { id: 1, name: "Task 1", project_id: 1, milestone_id: 1 },
+ { id: 2, name: "Task 2", project_id: false, milestone_id: false },
+ ],
+ },
+ "project.milestone": {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ name: { string: "Name", "type": "char" },
+ },
+ records: [
+ { id: 1, name: "Milestone 1" },
+ ]
+ },
+ "project.project": {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ name: { string: "Name", "type": "char" },
+ },
+ records: [
+ { id: 1, name: "Project 1" },
],
},
},
@@ -35,6 +89,7 @@ QUnit.module('hr_timesheet', function (hooks) {
}
}
setupControlPanelServiceRegistry();
+ target = getFixture();
serviceRegistry.add("company", companyService, { force: true });
});
@@ -73,4 +128,63 @@ QUnit.module('hr_timesheet', function (hooks) {
const renderedData = getGraphRenderer(graph).chart.data.datasets[0].data;
assert.deepEqual(renderedData, [1], 'The timesheet graph view is taking the timesheet_uom_factor into account (factor !== 1)');
});
+
+ QUnit.test("check custom default label", async function (assert) {
+ const graph = await makeView({
+ serverData,
+ type: "graph",
+ resModel: "project.task",
+ arch: `
+
+
+
+ `,
+ });
+
+ checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
+ checkLegend(assert, graph, ["Count"]);
+
+ await selectMode(target, "line");
+
+ checkLabels(assert, graph, ["", "Project 1", ""]);
+ checkLegend(assert, graph, ["Count"]);
+
+ await selectMode(target, "pie");
+
+ checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
+ checkLegend(assert, graph, ["🔒 Private", "Project 1"]);
+ });
+
+ QUnit.test("check default label with 2 fields in groupby", async function (assert) {
+ const graph = await makeView({
+ serverData,
+ type: "graph",
+ resModel: "project.task",
+ arch: `
+
+
+
+
+ `,
+ });
+
+ checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
+ checkLegend(assert, graph, ["Undefined", "Milestone 1", "Sum"]);
+
+ await selectMode(target, "line");
+
+ checkLabels(assert, graph, ["", "Project 1", ""]);
+ checkLegend(assert, graph, ["Milestone 1"]);
+
+ await selectMode(target, "pie");
+
+ checkLabels(assert, graph, [
+ "🔒 Private / Undefined",
+ "Project 1 / Milestone 1"
+ ]);
+ checkLegend(assert, graph, [
+ "🔒 Private / Undefined",
+ "Project 1 / Milestone 1"
+ ]);
+ });
});
diff --git a/addons/hr_timesheet/views/project_task_views.xml b/addons/hr_timesheet/views/project_task_views.xml
index 211fefa5d83..507cbb2ea31 100644
--- a/addons/hr_timesheet/views/project_task_views.xml
+++ b/addons/hr_timesheet/views/project_task_views.xml
@@ -243,7 +243,7 @@
project.task
-
+
diff --git a/addons/project/models/project_task.py b/addons/project/models/project_task.py
index b87e57f5976..195ed879cb4 100644
--- a/addons/project/models/project_task.py
+++ b/addons/project/models/project_task.py
@@ -167,17 +167,17 @@ class Task(models.Model):
# See project_task_stage_personal.py for the model defininition
personal_stage_type_ids = fields.Many2many('project.task.type', 'project_task_user_rel', column1='task_id', column2='stage_id',
ondelete='restrict', group_expand='_read_group_personal_stage_type_ids', copy=False,
- domain="[('user_id', '=', user.id)]", depends=['user_ids'], string='Personal Stage')
+ domain="[('user_id', '=', user.id)]", depends=['user_ids'], string='Personal Stages')
# Personal Stage computed from the user
personal_stage_id = fields.Many2one('project.task.stage.personal', string='Personal Stage State', compute_sudo=False,
compute='_compute_personal_stage_id', help="The current user's personal stage.")
# This field is actually a related field on personal_stage_id.stage_id
# However due to the fact that personal_stage_id is computed, the orm throws out errors
# saying the field cannot be searched.
- personal_stage_type_id = fields.Many2one('project.task.type', string='Personal User Stage',
+ personal_stage_type_id = fields.Many2one('project.task.type', string='Personal Stage',
compute='_compute_personal_stage_type_id', inverse='_inverse_personal_stage_type_id', store=False,
search='_search_personal_stage_type_id', default=_default_personal_stage_type_id,
- help="The current user's personal task stage.")
+ help="The current user's personal task stage.", domain="[('user_id', '=', uid)]")
partner_id = fields.Many2one('res.partner',
string='Customer', recursive=True, tracking=True, compute='_compute_partner_id', store=True, readonly=False,
domain="['|', ('company_id', '=?', company_id), ('company_id', '=', False)]", )
diff --git a/addons/project/static/src/views/project_task_graph/project_task_graph_model.js b/addons/project/static/src/views/project_task_graph/project_task_graph_model.js
new file mode 100644
index 00000000000..ec076d1f69f
--- /dev/null
+++ b/addons/project/static/src/views/project_task_graph/project_task_graph_model.js
@@ -0,0 +1,12 @@
+/** @odoo-module **/
+
+import { GraphModel } from "@web/views/graph/graph_model";
+
+export class ProjectTaskGraphModel extends GraphModel {
+ _getDefaultFilterLabel(field) {
+ if (field.fieldName === "project_id") {
+ return this.env._t("🔒 Private");
+ }
+ return super._getDefaultFilterLabel(field);
+ }
+}
diff --git a/addons/project/static/src/views/project_task_graph/project_graph_view.js b/addons/project/static/src/views/project_task_graph/project_task_graph_view.js
similarity index 51%
rename from addons/project/static/src/views/project_task_graph/project_graph_view.js
rename to addons/project/static/src/views/project_task_graph/project_task_graph_view.js
index 5cd8faa1ca8..fead3dd9953 100644
--- a/addons/project/static/src/views/project_task_graph/project_graph_view.js
+++ b/addons/project/static/src/views/project_task_graph/project_task_graph_view.js
@@ -3,9 +3,14 @@
import { ProjectControlPanel } from "@project/components/project_control_panel/project_control_panel";
import { registry } from "@web/core/registry";
import { graphView } from "@web/views/graph/graph_view";
+import { ProjectTaskGraphModel } from "./project_task_graph_model";
const viewRegistry = registry.category("views");
-export const projectGraphView = {...graphView, ControlPanel: ProjectControlPanel};
+export const projectTaskGraphView = {
+ ...graphView,
+ ControlPanel: ProjectControlPanel,
+ Model: ProjectTaskGraphModel,
+};
-viewRegistry.add("project_graph", projectGraphView);
+viewRegistry.add("project_task_graph", projectTaskGraphView);
diff --git a/addons/project/static/tests/views/project_task_graph/project_task_graph_view_tests.js b/addons/project/static/tests/views/project_task_graph/project_task_graph_view_tests.js
new file mode 100644
index 00000000000..9a1838a9bda
--- /dev/null
+++ b/addons/project/static/tests/views/project_task_graph/project_task_graph_view_tests.js
@@ -0,0 +1,127 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+
+import { fakeCookieService } from "@web/../tests/helpers/mock_services";
+import { getFixture } from "@web/../tests/helpers/utils";
+import { checkLabels, checkLegend, selectMode } from "@web/../tests/views/graph_view_tests";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+
+let serverData, target;
+
+QUnit.module("Project", (hooks) => {
+ hooks.beforeEach(async () => {
+ serverData = {
+ models: {
+ "project.task": {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ name: { string: "Name", type: "char" },
+ milestone_id: {
+ string: "Milestone",
+ type: "many2one",
+ relation: "project.milestone",
+ store: true,
+ sortable: true,
+ },
+ project_id: {
+ string: "Project",
+ type: "many2one",
+ relation: "project.project",
+ store: true,
+ sortable: true,
+ },
+ },
+ records: [
+ { id: 1, name: "Task 1", project_id: 1, milestone_id: 1 },
+ { id: 2, name: "Task 2", project_id: false, milestone_id: false },
+ ],
+ },
+ "project.milestone": {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ name: { string: "Name", "type": "char" },
+ },
+ records: [
+ { id: 1, name: "Milestone 1" },
+ ]
+ },
+ "project.project": {
+ fields: {
+ id: { string: "ID", type: "integer" },
+ name: { string: "Name", "type": "char" },
+ },
+ records: [
+ { id: 1, name: "Project 1" },
+ ],
+ },
+ },
+ views: {},
+ };
+ setupViewRegistries();
+
+ target = getFixture();
+ registry.category("services").add("cookie", fakeCookieService);
+ });
+
+ QUnit.module("ProjectTaskGraphView");
+
+ QUnit.test("check custom default label", async function (assert) {
+ const graph = await makeView({
+ serverData,
+ type: "graph",
+ resModel: "project.task",
+ arch: `
+
+
+
+ `,
+ });
+
+ checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
+ checkLegend(assert, graph, ["Count"]);
+
+ await selectMode(target, "line");
+
+ checkLabels(assert, graph, ["", "Project 1", ""]);
+ checkLegend(assert, graph, ["Count"]);
+
+ await selectMode(target, "pie");
+
+ checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
+ checkLegend(assert, graph, ["🔒 Private", "Project 1"]);
+ });
+
+ QUnit.test("check default label with 2 fields in groupby", async function (assert) {
+ const graph = await makeView({
+ serverData,
+ type: "graph",
+ resModel: "project.task",
+ arch: `
+
+
+
+
+ `,
+ });
+
+ checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
+ checkLegend(assert, graph, ["Undefined", "Milestone 1", "Sum"]);
+
+ await selectMode(target, "line");
+
+ checkLabels(assert, graph, ["", "Project 1", ""]);
+ checkLegend(assert, graph, ["Milestone 1"]);
+
+ await selectMode(target, "pie");
+
+ checkLabels(assert, graph, [
+ "🔒 Private / Undefined",
+ "Project 1 / Milestone 1"
+ ]);
+ checkLegend(assert, graph, [
+ "🔒 Private / Undefined",
+ "Project 1 / Milestone 1"
+ ]);
+ });
+});
diff --git a/addons/project/views/project_task_views.xml b/addons/project/views/project_task_views.xml
index f1943ab7730..294433cbcb0 100644
--- a/addons/project/views/project_task_views.xml
+++ b/addons/project/views/project_task_views.xml
@@ -119,7 +119,6 @@
-
@@ -138,6 +137,12 @@
+
+
+
+
+ True
+
@@ -683,6 +688,9 @@
+
+
+
@@ -746,7 +754,7 @@
project.task.graph
project.task
-
+
diff --git a/addons/web/static/src/views/graph/graph_model.js b/addons/web/static/src/views/graph/graph_model.js
index b3a1f3173e4..037b9126fd4 100644
--- a/addons/web/static/src/views/graph/graph_model.js
+++ b/addons/web/static/src/views/graph/graph_model.js
@@ -373,6 +373,14 @@ export class GraphModel extends Model {
return new DateClasses(arrays);
}
+ /**
+ * @protected
+ * @returns {string}
+ */
+ _getDefaultFilterLabel(field) {
+ return this.env._t("Undefined");
+ }
+
/**
* Eventually filters and sort data points.
* @protected
@@ -383,7 +391,7 @@ export class GraphModel extends Model {
let processedDataPoints = [];
if (mode === "line") {
processedDataPoints = this.dataPoints.filter(
- (dataPoint) => dataPoint.labels[0] !== this.env._t("Undefined")
+ (dataPoint) => dataPoint.labels[0] !== this._getDefaultFilterLabel(groupBy[0]),
);
} else if (mode === "pie") {
processedDataPoints = this.dataPoints.filter(
@@ -505,7 +513,7 @@ export class GraphModel extends Model {
if (type === "boolean") {
label = `${val}`; // toUpperCase?
} else if (val === false) {
- label = this.env._t("Undefined");
+ label = this._getDefaultFilterLabel(gb);
} else if (["many2many", "many2one"].includes(type)) {
const [id, name] = val;
const key = JSON.stringify([fieldName, name]);
diff --git a/addons/web/static/tests/views/graph_view_tests.js b/addons/web/static/tests/views/graph_view_tests.js
index 44995cc8a7f..a99bf33d7bf 100644
--- a/addons/web/static/tests/views/graph_view_tests.js
+++ b/addons/web/static/tests/views/graph_view_tests.js
@@ -65,12 +65,12 @@ function checkDatasets(assert, graph, keys, expectedDatasets) {
assert.deepEqual(actualValues, expectedDatasets);
}
-function checkLabels(assert, graph, expectedLabels) {
+export function checkLabels(assert, graph, expectedLabels) {
const labels = getChart(graph).data.labels.map((l) => l.toString());
assert.deepEqual(labels, expectedLabels);
}
-function checkLegend(assert, graph, expectedLegendLabels) {
+export function checkLegend(assert, graph, expectedLegendLabels) {
expectedLegendLabels =
expectedLegendLabels instanceof Array ? expectedLegendLabels : [expectedLegendLabels];
const chart = getChart(graph);
@@ -124,7 +124,7 @@ function getModeButton(el, mode) {
return el.querySelector(`.o_graph_button[data-mode="${mode}"`);
}
-async function selectMode(el, mode) {
+export async function selectMode(el, mode) {
await click(getModeButton(el, mode));
}
@@ -4239,4 +4239,50 @@ QUnit.module("Views", (hooks) => {
await validateSearch(target);
assert.verifySteps(["rendering"]);
});
+
+ QUnit.test("apply default filter label", async function (assert) {
+ const graphView = registry.category("views").get("graph");
+ class CustomGraphModel extends graphView.Model {
+ _getDefaultFilterLabel(fields) {
+ return "None";
+ }
+ }
+ registry.category("views").add("custom_graph", {
+ ...graphView,
+ Model: CustomGraphModel,
+ });
+
+ const graph = await makeView({
+ serverData,
+ type: "graph",
+ resModel: "foo",
+ arch: `
+
+
+
+
+ `,
+ });
+
+ checkLabels(assert, graph, ["xphone", "xpad"]);
+ checkLegend(assert, graph, ["None", "red", "Sum"]);
+
+ await selectMode(target, "line");
+
+ checkLabels(assert, graph, ["xphone", "xpad"]);
+ checkLegend(assert, graph, ["None", "red"]);
+
+ await selectMode(target, "pie");
+
+ checkLabels(assert, graph, [
+ "xphone / None",
+ "xphone / red",
+ "xpad / None",
+ ]);
+ checkLegend(assert, graph, [
+ "xphone / None",
+ "xphone / red",
+ "xpad / None",
+ ]);
+ });
});