From 496c0a275aaf9c357df5c70d0eeadc2b6880041f Mon Sep 17 00:00:00 2001 From: Pratik Awasthi Date: Tue, 25 Apr 2023 07:19:00 +0000 Subject: [PATCH] [IMP] project: improve the list view by adding personal stage This will improve the following generic UX - rename 'personal user stage' into 'personal stage' for private task - added the 'personal stage' field to the right of the stage optional=hide - earlier gantt view open with the user now it will open with groupby project - removed groupby stage in pivot view - rename 'undefined' into 'private' for graph view of private task task-3251648 closes odoo/odoo#120094 Related: odoo/upgrade#4607 Related: odoo/enterprise#40515 Signed-off-by: Xavier Bol (xbo) --- .../timesheet_graph/timesheet_graph_model.js | 6 +- .../timesheet_graph/timesheet_graph_view.js | 4 +- .../static/tests/timesheet_graph_tests.js | 122 ++++++++++++++++- .../hr_timesheet/views/project_task_views.xml | 2 +- addons/project/models/project_task.py | 6 +- .../project_task_graph_model.js | 12 ++ ...aph_view.js => project_task_graph_view.js} | 9 +- .../project_task_graph_view_tests.js | 127 ++++++++++++++++++ addons/project/views/project_task_views.xml | 12 +- .../web/static/src/views/graph/graph_model.js | 12 +- .../static/tests/views/graph_view_tests.js | 52 ++++++- 11 files changed, 342 insertions(+), 22 deletions(-) create mode 100644 addons/project/static/src/views/project_task_graph/project_task_graph_model.js rename addons/project/static/src/views/project_task_graph/{project_graph_view.js => project_task_graph_view.js} (51%) create mode 100644 addons/project/static/tests/views/project_task_graph/project_task_graph_view_tests.js 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", + ]); + }); });