Skip to content

Commit

Permalink
[MERGE] project: improve burndown chart performance
Browse files Browse the repository at this point in the history
Purpose of this PR
===============

The Burndown Chart report was very slow on big databases as it was not
possible for Postgresql to optimize the query as it was based on a view
that was using several generate series.

This commit aims to improve the performance by injecting the constraints
at a lower level than it was in the past, lowering the amount of data
processed in the higher level of the query.

/!\ Important note
------------------

Overwriting the `read_group_raw` is really not a good practice and should
be avoided in most case. If you fall on this implementation by grepping
the source code, please be advised that this is not the right way of doing
things.

Implementation details
----------------------

- The report is now run by generating the `SQL` that is executed by the
  `read_group_raw`. This allows inserting `SQL` constraints at a lower
  level and simnifically improves performance. As there is no other way
  to do it, the code is unfortunately a modified copy of the actual
  `read_group_raw`.
- The pivot view has been removed as it had no meaning and was creating
  confusing data.
- The `Group By` menu has been limited to `stage_id` and `date` as bringing
  more data trough the different `GROUP BY` statements up to the higher level
  is costly. Further more, additional `Group By` did not bring added value
  as the Chart was less readable.
- The JS code has been adapted in order to force a group by both `stage_id`
  and `date` so that the date displayed is always making sense.
- The sort ascending and descending options have been removed as creating
  confusing data.
- The compare with previous period has also been removed as the chart only
  really make sense when seen chronologically.
- A lot of tests have been added in order to ensure that changes that would
  be harmful for the report will trigger test fails.

task-2845729

--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

closes odoo#93225

Signed-off-by: Yannick Tivisse (yti) <[email protected]>
  • Loading branch information
robodoo authored Jul 20, 2022
2 parents 57a1776 + ac7cd0e commit cc788e0
Show file tree
Hide file tree
Showing 19 changed files with 1,192 additions and 372 deletions.
2 changes: 1 addition & 1 deletion addons/mail/models/mail_tracking_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MailTracking(models.Model):
_rec_name = 'field'
_order = 'tracking_sequence asc'

field = fields.Many2one('ir.model.fields', required=True, readonly=1, ondelete='cascade')
field = fields.Many2one('ir.model.fields', required=True, readonly=1, index=True, ondelete='cascade')
field_desc = fields.Char('Field Description', required=True, readonly=1)
field_type = fields.Char('Field Type')
field_groups = fields.Char(compute='_compute_field_groups')
Expand Down
606 changes: 420 additions & 186 deletions addons/project/report/project_task_burndown_chart_report.py

Large diffs are not rendered by default.

20 changes: 4 additions & 16 deletions addons/project/report/project_task_burndown_chart_report_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<field name="arch" type="xml">
<search string="Burndown Chart">
<field name="project_id" />
<field name="user_ids" />
<field name="milestone_id" groups="project.group_project_milestone"/>
<field name="date_assign"/>
<field name="date_deadline"/>
Expand All @@ -18,12 +19,10 @@
<filter name="filter_date_assign" date="date_assign"/>
<filter string="Last Month" invisible="1" name="last_month" domain="[('date','&gt;=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
<filter string="Open tasks" name="open_tasks" domain="[('is_closed', '=', False)]"/>
<filter string="Late Milestones" name="late_milestone" domain="[('is_closed', '=', False), ('task_id.has_late_and_unreached_milestone', '=', True)]" groups="project.group_project_milestone"/>
<filter string="Late Milestones" name="late_milestone" domain="[('is_closed', '=', False), ('has_late_and_unreached_milestone', '=', True)]" groups="project.group_project_milestone"/>
<group expand="0" string="Group By">
<filter string="Date" name="date" context="{'group_by': 'date'}" />
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}" />
<filter string="Project" name="project" context="{'group_by': 'project_id'}" />
<filter string="Milestone" name="milestone" context="{'group_by': 'milestone_id'}" groups="project.group_project_milestone" />
</group>
</search>
</field>
Expand All @@ -40,23 +39,12 @@
</field>
</record>

<record id="project_task_burndown_chart_report_view_pivot" model="ir.ui.view">
<field name="name">project.task.burndown.chart.report.view.pivot</field>
<field name="model">project.task.burndown.chart.report</field>
<field name="arch" type="xml">
<pivot string="Burndown Chart" display_quantity="1" disable_linking="1" sample="1" js_class="burndown_chart_pivot">
<field name="date" type="row"/>
<field name="stage_id" type="row"/>
</pivot>
</field>
</record>

<record id="action_project_task_burndown_chart_report" model="ir.actions.act_window">
<field name="name">Burndown Chart</field>
<field name="res_model">project.task.burndown.chart.report</field>
<field name="view_mode">graph,pivot</field>
<field name="view_mode">graph</field>
<field name="search_view_id" ref="project_task_burndown_chart_report_view_search"/>
<field name="context">{'search_default_project_id': active_id}</field>
<field name="context">{'search_default_project_id': active_id, 'search_default_date': 1, 'search_default_stage': 1}</field>
<field name="domain">[('display_project_id', '!=', False)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/** @odoo-module */

import { useService } from "@web/core/utils/hooks";
import { SearchModel } from "@web/search/search_model";


export class BurndownChartSearchModel extends SearchModel {

/**
* @override
*/
setup(services) {
this.notificationService = useService("notification");
super.setup(...arguments);
}

/**
* @override
*/
async load(config) {
await super.load(...arguments);
// Store date and stage_id searchItemId in the SearchModel for reuse in other functions.
for (const searchItem of Object.values(this.searchItems)) {
if (['dateGroupBy', 'groupBy'].includes(searchItem.type)) {
if (this.stageIdSearchItemId && this.dateSearchItemId) {
return;
}
switch (searchItem.fieldName) {
case 'date':
this.dateSearchItemId = searchItem.id;
break;
case 'stage_id':
this.stageIdSearchItemId = searchItem.id;
break;
}
}
}
}

/**
* @override
*/
deactivateGroup(groupId) {
// Prevent removing Date & Stage group by from the search
if (this.searchItems[this.stageIdSearchItemId].groupId == groupId && this.searchItems[this.dateSearchItemId].groupId) {
this._addGroupByNotification(this.env._t("Date and Stage"));
return;
}
super.deactivateGroup(groupId);
}

/**
* @override
*/
toggleDateGroupBy(searchItemId, intervalId) {
// Ensure that there is always one and only one date group by selected.
if (searchItemId === this.dateSearchItemId) {
let filtered_query = [];
let triggerNotification = false;
for (const queryElem of this.query) {
if (queryElem.searchItemId !== searchItemId) {
filtered_query.push(queryElem);
} else if (queryElem.intervalId === intervalId) {
triggerNotification = true;
}
}
if (filtered_query.length !== this.query.length) {
this.query = filtered_query;
if (triggerNotification) {
this._addGroupByNotification(this.env._t("Date"));
}
}
}
super.toggleDateGroupBy(...arguments);
}

/**
* @override
*/
toggleSearchItem(searchItemId) {
// Ensure that stage_id is always selected.
if (searchItemId === this.stageIdSearchItemId
&& this.query.some(queryElem => queryElem.searchItemId === searchItemId)) {
this._addGroupByNotification(this.env._t("Stage"));
return;
}
super.toggleSearchItem(...arguments);
}

/**
* Adds a notification relative to the group by constraint of the Burndown Chart.
* @param fieldName The field name(s) the notification has to be related to.
* @private
*/
_addGroupByNotification(fieldName) {
const notif = this.env._t("The Burndown Chart must be grouped by");
this.notificationService.add(
`${notif} ${fieldName}`,
{ type: "danger" }
);
}

/**
* @override
*/
async _notify() {
// Ensure that we always group by date firstly and by stage_id secondly
let stageIdIndex = -1;
let dateIndex = -1;
for (const [index, queryElem] of this.query.entries()) {
if (stageIdIndex !== -1 && dateIndex !== -1) {
break;
}
switch (queryElem.searchItemId) {
case this.dateSearchItemId:
dateIndex = index;
break;
case this.stageIdSearchItemId:
stageIdIndex = index;
break;
}
}
if (stageIdIndex > 0) {
if (stageIdIndex > dateIndex) {
dateIndex += 1;
}
this.query.splice(0, 0, this.query.splice(stageIdIndex, 1)[0]);
}
if (dateIndex > 0) {
this.query.splice(0, 0, this.query.splice(dateIndex, 1)[0]);
}
await super._notify(...arguments);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { BurndownChartModel } from "./burndown_chart_model";
import { BurndownChartRenderer } from "./burndown_chart_renderer";
import { graphView } from "@web/views/graph/graph_view";
import { registry } from "@web/core/registry";
import { BurndownChartSearchModel } from "./burndown_chart_search_model";

const viewRegistry = registry.category("views");

const burndownChartGraphView = {
...graphView,
Renderer: BurndownChartRenderer,
buttonTemplate: "project.BurndownChartView.Buttons",
hideCustomGroupBy: true,
Model: BurndownChartModel,
searchMenuTypes: graphView.searchMenuTypes.filter(menuType => menuType !== "comparison"),
SearchModel: BurndownChartSearchModel,
};

viewRegistry.add("burndown_chart", burndownChartGraphView);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<xpath expr="//div[@role='toolbar'][3]" position="attributes">
<attribute name="t-if">true</attribute>
</xpath>
<xpath expr="//div[@role='toolbar'][4]" position="replace">
</xpath>
</t>

</templates>
Loading

0 comments on commit cc788e0

Please sign in to comment.