[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:
@@ -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": `
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user