[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) <xbo@odoo.com>
This commit is contained in:
Bastien (bvdn)
2023-04-07 15:31:52 +02:00
parent d2e5969e3e
commit 0c2060eab9
5 changed files with 112 additions and 8 deletions
+17
View File
@@ -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,
@@ -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);
}
}
@@ -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": `
+9 -2
View File
@@ -493,8 +493,15 @@
<field name="arch" type="xml">
<form class="o_form_project_tasks">
<group>
<field name="display_name" string = "Task Title" placeholder="e.g. Send Invitations" required="True"/>
<field name="project_id" widget="project_private_task" invisible="context.get('default_project_id', False)" class="o_project_task_project_field"/>
<field name="display_name" string= "Task Title" placeholder="e.g. Send Invitations"/>
<field name="project_id"
widget="project_private_task"
invisible="context.get('default_project_id', False)"
placeholder="Private"
class="o_project_task_project_field"
domain="[('type_ids', 'in', context['default_stage_id'])] if context.get('default_stage_id') else []"
context="{'default_type_ids': [(4, context.get('default_stage_id', False))]}"
/>
<field name="user_ids" options="{'no_open': True, 'no_quick_create': True}" domain="[('share', '=', False), ('active', '=', True)]"
widget="many2many_avatar_user"/>
<field name="company_id" invisible="1"/>
@@ -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;