From 0c2060eab987ea21a00d7f4dd89bf01d656557e9 Mon Sep 17 00:00:00 2001 From: "Bastien (bvdn)" Date: Thu, 23 Mar 2023 10:26:28 +0000 Subject: [PATCH] [IMP] project, web: sort burndown chart legend Before this commit: - The burndown chart displayed the stage in order of the data fetching which wasn't logical especial for a chart that supposed to represent the evolution of the tasks in the project - When sorting all/my tasks by stage, It was possible to select any project in any quickcreate of the stages which didn't make much sense After this commit: The burndown chart legend is now ordered according to the stage sequence (previously was ordered randomly by comming data) Modified the burndownChartModel, simply makes a RPC to get the stages and sequences then sort the legend elements (one by stage) with it Display only the projects which uses the stage in the dropdown menu of the task kanban quickcreate (when grouping by stage) When sorting by all/my tasks by stage, the quickcreate now only display the projects which contains the stage selected done by adding a domain in the quickcreate form (shoutout to LTU and AUON who actually found the fix) Task-3067445 closes odoo/odoo#105694 Signed-off-by: Xavier Bol (xbo) --- addons/project/models/project_project.py | 17 +++++ .../burndown_chart/burndown_chart_model.js | 72 ++++++++++++++++++- .../static/tests/burndown_chart_tests.js | 8 ++- addons/project/views/project_task_views.xml | 11 ++- .../web/static/src/views/graph/graph_model.js | 12 ++-- 5 files changed, 112 insertions(+), 8 deletions(-) diff --git a/addons/project/models/project_project.py b/addons/project/models/project_project.py index 55f5e28d628..9b2fa22c956 100644 --- a/addons/project/models/project_project.py +++ b/addons/project/models/project_project.py @@ -555,6 +555,17 @@ class Project(models.Model): def action_project_task_burndown_chart_report(self): action = self.env['ir.actions.act_window']._for_xml_id('project.action_project_task_burndown_chart_report') action['display_name'] = _("%(name)s's Burndown Chart", name=self.name) + context = action['context'].replace('active_id', str(self.id)) + context = ast.literal_eval(context) + context.update({ + 'stage_name_and_sequence_per_id': { + stage.id: { + 'sequence': stage.sequence, + 'name': stage.name + } for stage in self.type_ids + } + }) + action['context'] = context return action def action_project_timesheets(self): @@ -764,6 +775,12 @@ class Project(models.Model): 'action': 'project.action_project_task_burndown_chart_report', 'additional_context': json.dumps({ 'active_id': self.id, + 'stage_name_and_sequence_per_id': { + stage.id: { + 'sequence': stage.sequence, + 'name': stage.name + } for stage in self.type_ids + }, }), 'show': True, 'sequence': 60, diff --git a/addons/project/static/src/views/burndown_chart/burndown_chart_model.js b/addons/project/static/src/views/burndown_chart/burndown_chart_model.js index 7850ffa5849..ec088acdb2a 100644 --- a/addons/project/static/src/views/burndown_chart/burndown_chart_model.js +++ b/addons/project/static/src/views/burndown_chart/burndown_chart_model.js @@ -1,14 +1,84 @@ /** @odoo-module **/ import { GraphModel } from "@web/views/graph/graph_model"; +import { sortBy } from "@web/core/utils/arrays"; export class BurndownChartModel extends GraphModel { + /** + * @override + */ + setup(params) { + super.setup(params); + this.stageSeqAndNamePerId = {}; + } + + /** + * Fetch the sequence of each stage in the project. This function alters this.stageSeqAndNamePerId + * @protected + * @param {Object} context + */ + async _fetchStageInfo(context) { + const searchDomain = + !context.active_id || !context.default_project_id + ? [] + : [["project_ids", "in", context.active_id]]; + const data = await this.orm.webSearchRead("project.task.type", searchDomain, [ + "name", + "sequence", + "id", + ]); + const stageSeqAndNamePerId = {}; + for (const { id, name, sequence } of data.records) { + stageSeqAndNamePerId[id] = { name, sequence }; + } + return stageSeqAndNamePerId; + } + + /** + * @param {SearchParams} searchParams + */ + async load(searchParams) { + const { context, groupBy } = searchParams; + + if (groupBy.includes("stage_id")) { + if (context.stage_name_and_sequence_per_id && context.default_project_id) { + this.stageSeqAndNamePerId = context.stage_name_and_sequence_per_id; + } else { + // if the stage_name_and_sequence_per_id wasn't given by the action (for example if the page is simply reloaded) + this.stageSeqAndNamePerId = await this._fetchStageInfo(context); + } + } else { + this.stageSeqAndNamePerId = {}; + } + await super.load(searchParams); + } + + /** + * @override + */ + _prepareData() { + super._prepareData(); + const { groupBy } = this.searchParams; + const { mode } = this.metaData; + if (mode === "line" && groupBy.includes("stage_id")) { + this.data.datasets = sortBy(this.data.datasets, (dataSet) => { + const firstIdentifier = [...dataSet.identifiers][0]; + const group = Object.assign(...JSON.parse(firstIdentifier)); + const val = group.stage_id; + if (Array.isArray(val)) { + return this.stageSeqAndNamePerId[val[0]]?.sequence || -1; + } + return -1; + }); + } + } + /** * @protected * @override */ async _loadDataPoints(metaData) { - metaData.measures.__count.string = this.env._t('# of Tasks'); + metaData.measures.__count.string = this.env._t("# of Tasks"); return super._loadDataPoints(metaData); } } diff --git a/addons/project/static/tests/burndown_chart_tests.js b/addons/project/static/tests/burndown_chart_tests.js index 94fb5e716f1..a4d658dec98 100644 --- a/addons/project/static/tests/burndown_chart_tests.js +++ b/addons/project/static/tests/burndown_chart_tests.js @@ -45,7 +45,13 @@ QUnit.module("Project", {}, () => { { id: 2, name: "In Progress" }, { id: 3, name: "Done" }, ], - } + }, + "project.task.type": { + fields: { + name: { string: "Name", type: "char" }, + sequence: { type: "integer" }, + }, + }, }, views: { "burndown_chart,false,graph": ` diff --git a/addons/project/views/project_task_views.xml b/addons/project/views/project_task_views.xml index a26d77f4bfa..06c285b8840 100644 --- a/addons/project/views/project_task_views.xml +++ b/addons/project/views/project_task_views.xml @@ -493,8 +493,15 @@
- - + + diff --git a/addons/web/static/src/views/graph/graph_model.js b/addons/web/static/src/views/graph/graph_model.js index 9ffddafe61b..47a58a01e7c 100644 --- a/addons/web/static/src/views/graph/graph_model.js +++ b/addons/web/static/src/views/graph/graph_model.js @@ -116,7 +116,7 @@ export class GraphModel extends Model { this.initialGroupBy = searchParams.context.graph_groupbys || this.metaData.groupBy; // = arch groupBy --> change that } const metaData = this._buildMetaData(); - return this._fetchDataPoints(metaData); + await this._fetchDataPoints(metaData); } /** @@ -243,7 +243,7 @@ export class GraphModel extends Model { // dataPoints + labels --> datasetsTmp --> datasets const datasetsTmp = {}; for (const dataPt of dataPoints) { - const { domain, labelIndex, originIndex, trueLabel, value } = dataPt; + const { domain, labelIndex, originIndex, trueLabel, value, identifier } = dataPt; const datasetLabel = this._getDatasetLabel(dataPt); if (!(datasetLabel in datasetsTmp)) { let dataLength = labels.length; @@ -256,11 +256,13 @@ export class GraphModel extends Model { domains: new Array(dataLength).fill([]), label: datasetLabel, originIndex: originIndex, + identifiers: new Set(), }; } datasetsTmp[datasetLabel].data[labelIndex] = value; datasetsTmp[datasetLabel].domains[labelIndex] = domain; datasetsTmp[datasetLabel].trueLabels[labelIndex] = trueLabel; + datasetsTmp[datasetLabel].identifiers.add(identifier); } // sort by origin let datasets = sortBy(Object.values(datasetsTmp), "originIndex"); @@ -446,10 +448,11 @@ export class GraphModel extends Model { for (const group of data.groups) { const { __domain, __count } = group; const labels = []; - + const rawValues = []; for (const gb of groupBy) { let label; const val = group[gb.spec]; + rawValues.push({ [gb.spec]: val }); const fieldName = gb.fieldName; const { type } = fields[fieldName]; if (type === "boolean") { @@ -493,6 +496,7 @@ export class GraphModel extends Model { value, labels, originIndex, + identifier: JSON.stringify(rawValues), }); } return dataPoints; @@ -553,7 +557,7 @@ export class GraphModel extends Model { /** * @protected */ - async _prepareData() { + _prepareData() { const processedDataPoints = this._getProcessedDataPoints(); this.data = this._getData(processedDataPoints); this.lineOverlayDataset = null;