mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[ADD] Web Gantt Chart
This commit is contained in:
committed by
Parthiv Patel
parent
0088cf48d6
commit
7d98387f0d
1
addons/project_gantt/__init__.py
Normal file
1
addons/project_gantt/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
18
addons/project_gantt/__manifest__.py
Normal file
18
addons/project_gantt/__manifest__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
'name': 'Project Gantt Chart Demo',
|
||||
'category': 'Project',
|
||||
'version': '2.0',
|
||||
'summary': 'Project Gantt Chart Demo',
|
||||
'depends': [
|
||||
'web_gantt',
|
||||
'project'
|
||||
],
|
||||
'data': [
|
||||
'views/project_task_inherit.xml',
|
||||
],
|
||||
'demo': ['data/project_task_demo.xml'],
|
||||
'qweb': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
}
|
||||
119
addons/project_gantt/data/project_task_demo.xml
Normal file
119
addons/project_gantt/data/project_task_demo.xml
Normal file
@@ -0,0 +1,119 @@
|
||||
<flectra>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="project.project_task_1" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">9</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_2" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today())"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">7</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_3" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=2))"/>
|
||||
<field name="end_date" eval="(DateTime.today())"/>
|
||||
<field name="progress">5</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_4" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=2))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=2))"/>
|
||||
<field name="progress">5</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_5" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today())"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=2))"/>
|
||||
<field name="progress">4</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_6" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=3))"/>
|
||||
<field name="progress">9</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_7" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=2))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">8</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_8" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=2))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=3))"/>
|
||||
<field name="progress">10</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_9" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today())"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">0</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_10" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">7</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_11" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=3))"/>
|
||||
<field name="progress">1</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_12" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">2</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_19" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=2))"/>
|
||||
<field name="progress">3</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_20" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today())"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=3))"/>
|
||||
<field name="progress">3</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_21" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=2))"/>
|
||||
<field name="end_date" eval="(DateTime.today())"/>
|
||||
<field name="progress">4</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_22" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=2))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">7</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_24" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=1))"/>
|
||||
<field name="progress">4</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_25" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today() + relativedelta(days=2))"/>
|
||||
<field name="progress">2</field>
|
||||
</record>
|
||||
|
||||
<record id="project.project_task_26" model="project.task">
|
||||
<field name="start_date" eval="(DateTime.today() - relativedelta(days=1))"/>
|
||||
<field name="end_date" eval="(DateTime.today())"/>
|
||||
<field name="progress">10</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</flectra>
|
||||
1
addons/project_gantt/models/__init__.py
Normal file
1
addons/project_gantt/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import project_task
|
||||
9
addons/project_gantt/models/project_task.py
Normal file
9
addons/project_gantt/models/project_task.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from flectra import fields, models
|
||||
|
||||
|
||||
class ProjectTaskInherit(models.Model):
|
||||
_inherit = 'project.task'
|
||||
|
||||
start_date = fields.Date(string='Start Date')
|
||||
end_date = fields.Date(string='End Date')
|
||||
progress = fields.Integer(string='Progress')
|
||||
66
addons/project_gantt/views/project_task_inherit.xml
Normal file
66
addons/project_gantt/views/project_task_inherit.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<flectra>
|
||||
|
||||
<record id="project_task_view_gantt_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.form.view</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="progress"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_task_gantt" model="ir.ui.view">
|
||||
<field name="name">project.task.gantt</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<gantt string="Project Tasks" mode="day">
|
||||
<field name="progress" type="progress" scale="10"/>
|
||||
<field name="start_date" type="start"/>
|
||||
<field name="end_date" type="end"/>
|
||||
<!-- <field name="child_task_ids" type="child" start="sd" end="ed"/>-->
|
||||
</gantt>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_all_task_tree_action_inherit" model="ir.actions.act_window.view">
|
||||
<field eval="1" name="sequence"/>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="project.view_task_tree2"/>
|
||||
<field name="act_window_id" ref="project.action_view_all_task"/>
|
||||
</record>
|
||||
|
||||
<record id="action_view_all_task_kanban_action_inherit" model="ir.actions.act_window.view">
|
||||
<field eval="2" name="sequence"/>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="view_id" ref="project.view_task_kanban"/>
|
||||
<field name="act_window_id" ref="project.action_view_all_task"/>
|
||||
</record>
|
||||
|
||||
<record id="action_view_all_task_form_action_inherit" model="ir.actions.act_window.view">
|
||||
<field eval="3" name="sequence"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="project.view_task_form2"/>
|
||||
<field name="act_window_id" ref="project.action_view_all_task"/>
|
||||
</record>
|
||||
|
||||
<record id="action_view_all_task_calendar_action_inherit" model="ir.actions.act_window.view">
|
||||
<field eval="4" name="sequence"/>
|
||||
<field name="view_mode">calendar</field>
|
||||
<field name="view_id" ref="project.view_task_calendar"/>
|
||||
<field name="act_window_id" ref="project.action_view_all_task"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="action_view_all_task_gantt_chart_inherit" model="ir.actions.act_window.view">
|
||||
<field eval="5" name="sequence"/>
|
||||
<field name="view_mode">gantt</field>
|
||||
<field name="view_id" ref="project_gantt.view_project_task_gantt"/>
|
||||
<field name="act_window_id" ref="project.action_view_all_task"/>
|
||||
</record>
|
||||
|
||||
|
||||
</flectra>
|
||||
0
addons/web_gantt/__init__.py
Normal file
0
addons/web_gantt/__init__.py
Normal file
18
addons/web_gantt/__manifest__.py
Normal file
18
addons/web_gantt/__manifest__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
'name': 'Web Gantt Chart',
|
||||
'category': 'Project',
|
||||
'version': '2.0',
|
||||
'summary': 'Gantt Chart',
|
||||
'depends': [
|
||||
'web'
|
||||
],
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/*.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
}
|
||||
12
addons/web_gantt/static/src/css/style.css
Normal file
12
addons/web_gantt/static/src/css/style.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.o_gantt_renderer{
|
||||
height: 100%;
|
||||
}
|
||||
.gantt_grid_head_add, .gantt_add{
|
||||
display: none;
|
||||
}
|
||||
.gantt_task_progress_drag{
|
||||
display: none !important;
|
||||
}
|
||||
.gantt_scale_line > .gantt_scale_cell, .gantt_grid_scale > .gantt_grid_head_cell{
|
||||
font-weight: bold !important;
|
||||
}
|
||||
1
addons/web_gantt/static/src/dhtmlx/css/dhtmlxgantt.css
Normal file
1
addons/web_gantt/static/src/dhtmlx/css/dhtmlxgantt.css
Normal file
File diff suppressed because one or more lines are too long
14
addons/web_gantt/static/src/dhtmlx/js/dhtmlxgantt.js
Normal file
14
addons/web_gantt/static/src/dhtmlx/js/dhtmlxgantt.js
Normal file
File diff suppressed because one or more lines are too long
147
addons/web_gantt/static/src/js/gantt_controller.js
Normal file
147
addons/web_gantt/static/src/js/gantt_controller.js
Normal file
@@ -0,0 +1,147 @@
|
||||
flectra.define('web_gantt.GanttController', function (require) {
|
||||
"use strict";
|
||||
|
||||
|
||||
const AbstractController = require('web.AbstractController');
|
||||
const { ComponentWrapper } = require('web.OwlCompatibility');
|
||||
const DropdownMenu = require('web.DropdownMenu');
|
||||
const { DEFAULT_INTERVAL, INTERVAL_OPTIONS } = require('web.searchUtils');
|
||||
const { qweb } = require('web.core');
|
||||
var view_dialogs = require('web.view_dialogs');
|
||||
|
||||
class CarretDropdownMenu extends DropdownMenu {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get displayCaret() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var GanttChartController = AbstractController.extend({
|
||||
events: _.extend({}, AbstractController.prototype.events, {
|
||||
'click .gantt_bar_task': '_onTaskClicked',
|
||||
}),
|
||||
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
|
||||
item_selected: '_onItemSelected',
|
||||
open_view: '_onOpenView',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
* @param {GraphModel} model
|
||||
* @param {GraphRenderer} renderer
|
||||
* @param {Object} params
|
||||
* @param {string[]} params.measures
|
||||
* @param {boolean} params.isEmbedded
|
||||
* @param {string[]} params.groupableFields,
|
||||
*/
|
||||
init: function (parent, model, renderer, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this.measures = params.measures;
|
||||
this.child = params.child;
|
||||
this.childExist = params.childExist;
|
||||
// this parameter condition the appearance of a 'Group By'
|
||||
// button in the control panel owned by the graph view.
|
||||
this.isEmbedded = params.isEmbedded;
|
||||
this.withButtons = params.withButtons;
|
||||
// views to use in the action triggered when the graph is clicked
|
||||
this.views = params.views;
|
||||
this.title = params.title;
|
||||
|
||||
// this parameter determines what is the list of fields
|
||||
// that may be used within the groupby menu available when
|
||||
// the view is embedded
|
||||
this.groupableFields = params.groupableFields;
|
||||
this.buttonDropdownPromises = [];
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$el.addClass('o_gantt_controller');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
getOwnedQueryParams: function () {
|
||||
var state = this.model.get();
|
||||
return {
|
||||
context: {
|
||||
gantt_mode: state.mode,
|
||||
}
|
||||
};
|
||||
},
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
_onTaskClicked:function(ev){
|
||||
var $target = $(ev.currentTarget);
|
||||
if($target.attr('task_id').startsWith('child_')){
|
||||
let id = $target.attr('task_id').substring("child_".length);
|
||||
|
||||
return new view_dialogs.FormViewDialog(this, {
|
||||
title: $target.find('.gantt_task_content').text(),
|
||||
res_model: this.child.model,
|
||||
view_id: false,
|
||||
res_id: parseInt(id),
|
||||
on_saved: this.reload.bind(this, {}),
|
||||
}).open();
|
||||
|
||||
}else{
|
||||
return new view_dialogs.FormViewDialog(this, {
|
||||
title: $target.find('.gantt_task_content').text(),
|
||||
res_model: this.modelName,
|
||||
view_id: false,
|
||||
res_id: parseInt($target.attr('task_id')),
|
||||
on_saved: this.reload.bind(this, {}),
|
||||
}).open();
|
||||
}
|
||||
},
|
||||
_onButtonClick:function(ev){
|
||||
var $target = $(ev.target);
|
||||
if($target.hasClass('o_gantt_button')){
|
||||
this.update({ mode: $target.data('mode') });
|
||||
}else{
|
||||
return new view_dialogs.FormViewDialog(this, {
|
||||
res_model: this.modelName,
|
||||
view_id: false,
|
||||
res_id: false,
|
||||
on_saved: this.reload.bind(this, {}),
|
||||
}).open();
|
||||
}
|
||||
},
|
||||
updateButtons: function () {
|
||||
if (!this.$buttons) {
|
||||
return;
|
||||
}
|
||||
var state = this.model.get();
|
||||
this.$buttons.find('.o_gantt_button').removeClass('active');
|
||||
this.$buttons
|
||||
.find('.o_gantt_button[data-mode="' + state.mode + '"]')
|
||||
.addClass('active');
|
||||
},
|
||||
formatDate: function(date){
|
||||
var d = new Date(date),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
year = d.getFullYear();
|
||||
|
||||
if (month.length < 2)
|
||||
month = '0' + month;
|
||||
if (day.length < 2)
|
||||
day = '0' + day;
|
||||
|
||||
return [day, month, year].join('-');
|
||||
},
|
||||
reload: async function () {
|
||||
const promises = [this._super(...arguments)];
|
||||
return Promise.all(promises);
|
||||
},
|
||||
renderButtons: function ($node) {
|
||||
this.$buttons = $(qweb.render('GanttView.buttons'));
|
||||
this.$buttons.click(ev => this._onButtonClick(ev));
|
||||
},
|
||||
|
||||
});
|
||||
return GanttChartController;
|
||||
});
|
||||
232
addons/web_gantt/static/src/js/gantt_model.js
Normal file
232
addons/web_gantt/static/src/js/gantt_model.js
Normal file
@@ -0,0 +1,232 @@
|
||||
flectra.define('web_gantt.GanttModel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const { DEFAULT_INTERVAL, rankInterval } = require('web.searchUtils');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var AbstractModel = require('web.AbstractModel');
|
||||
|
||||
return AbstractModel.extend({
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.chart = null;
|
||||
this.GanttData = [];
|
||||
},
|
||||
|
||||
__get: function () {
|
||||
return Object.assign({ isSample: this.isSampleModel }, this.chart);
|
||||
},
|
||||
__load: function (params) {
|
||||
var groupBys = params.context.graph_groupbys || params.groupBys;
|
||||
this.initialGroupBys = groupBys;
|
||||
this.fields = params.fields;
|
||||
this.modelName = params.modelName;
|
||||
this.chart = Object.assign({
|
||||
context: params.context,
|
||||
dataPoints: [],
|
||||
domain: params.domain,
|
||||
groupBy: params.groupedBy.length ? params.groupedBy : groupBys,
|
||||
measure: params.context.graph_measure || params.measure,
|
||||
mode: params.context.graph_mode || params.mode,
|
||||
origins: [],
|
||||
stacked: params.stacked,
|
||||
timeRanges: params.timeRanges,
|
||||
orderBy: params.orderBy,
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate,
|
||||
child: params.child,
|
||||
childExist: params.childExist,
|
||||
progress: params.progress,
|
||||
titleField: params.titleField,
|
||||
});
|
||||
|
||||
this._computeDerivedParams();
|
||||
|
||||
return this._loadGraph();
|
||||
},
|
||||
__reload: function (handle, params) {
|
||||
if ('context' in params) {
|
||||
this.chart.context = params.context;
|
||||
this.chart.groupBy = params.context.graph_groupbys || this.chart.groupBy;
|
||||
this.chart.measure = params.context.graph_measure || this.chart.measure;
|
||||
this.chart.mode = params.context.graph_mode || this.chart.mode;
|
||||
}
|
||||
if ('domain' in params) {
|
||||
this.chart.domain = params.domain;
|
||||
}
|
||||
if ('groupBy' in params) {
|
||||
this.chart.groupBy = params.groupBy.length ? params.groupBy : this.initialGroupBys;
|
||||
}
|
||||
if ('measure' in params) {
|
||||
this.chart.measure = params.measure;
|
||||
}
|
||||
if ('timeRanges' in params) {
|
||||
this.chart.timeRanges = params.timeRanges;
|
||||
}
|
||||
|
||||
this._computeDerivedParams();
|
||||
|
||||
if ('mode' in params) {
|
||||
this.chart.mode = params.mode;
|
||||
return Promise.resolve();
|
||||
}
|
||||
if ('stacked' in params) {
|
||||
this.chart.stacked = params.stacked;
|
||||
return Promise.resolve();
|
||||
}
|
||||
if ('orderBy' in params) {
|
||||
this.chart.orderBy = params.orderBy;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._loadGraph();
|
||||
},
|
||||
_computeDerivedParams: function () {
|
||||
this.chart.processedGroupBy = this._processGroupBy(this.chart.groupBy);
|
||||
|
||||
const { range, rangeDescription, comparisonRange, comparisonRangeDescription, fieldName } = this.chart.timeRanges;
|
||||
if (range) {
|
||||
this.chart.domains = [
|
||||
this.chart.domain.concat(range),
|
||||
this.chart.domain.concat(comparisonRange),
|
||||
];
|
||||
this.chart.origins = [rangeDescription, comparisonRangeDescription];
|
||||
const groupBys = this.chart.processedGroupBy.map(function (gb) {
|
||||
return gb.split(":")[0];
|
||||
});
|
||||
this.chart.comparisonFieldIndex = groupBys.indexOf(fieldName);
|
||||
} else {
|
||||
this.chart.domains = [this.chart.domain];
|
||||
this.chart.origins = [""];
|
||||
this.chart.comparisonFieldIndex = -1;
|
||||
}
|
||||
},
|
||||
_loadGraph: function () {
|
||||
var self = this;
|
||||
this.chart.dataPoints = [];
|
||||
this.chart.GanttData = [];
|
||||
var groupBy = this.chart.processedGroupBy;
|
||||
var fields = _.map(groupBy, function (groupBy) {
|
||||
return groupBy.split(':')[0];
|
||||
});
|
||||
if (this.chart.measure !== '__count__') {
|
||||
if (this.fields[this.chart.measure].type === 'many2one') {
|
||||
fields = fields.concat(this.chart.measure + ":count_distinct");
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
fields = fields.concat(this.chart.startDate);
|
||||
fields = fields.concat(this.chart.endDate);
|
||||
fields = fields.concat(this.chart.titleField);
|
||||
fields = fields.concat(this.chart.progress);
|
||||
if(this.chart.childExist){
|
||||
fields = fields.concat(this.chart.child.name);
|
||||
}
|
||||
var context = _.extend({fill_temporal: true}, this.chart.context);
|
||||
|
||||
var proms = [];
|
||||
proms.push(this._rpc({
|
||||
route: '/web/dataset/search_read',
|
||||
model: this.modelName,
|
||||
fields: fields,
|
||||
context: context,
|
||||
domain: this.chart.domain,
|
||||
}).then(self._processGanttData.bind(self)));
|
||||
return Promise.all(proms);
|
||||
},
|
||||
_processGanttData: async function(rawData) {
|
||||
var self = this;
|
||||
var childFields = ['display_name'];
|
||||
var proms = [];
|
||||
if(this.chart.childExist){
|
||||
childFields = childFields.concat(this.chart.child.startDate);
|
||||
childFields = childFields.concat(this.chart.child.endDate);
|
||||
}
|
||||
rawData.records.forEach(function (dataPt){
|
||||
if(dataPt[self.chart.startDate] != false && dataPt[self.chart.endDate] != false){
|
||||
var start_date = self.formatDate(dataPt[self.chart.startDate]);
|
||||
var end_date = self.formatDate(dataPt[self.chart.endDate]);
|
||||
var type = '';
|
||||
if(self.chart.childExist){
|
||||
type = dataPt[self.chart.child.name].length != 0 ? "project" : ''
|
||||
}
|
||||
self.chart.GanttData.push({
|
||||
resId: dataPt.id,
|
||||
startDate: start_date,
|
||||
type: type,
|
||||
endDate: end_date,
|
||||
titleField: dataPt[self.chart.titleField],
|
||||
progress: dataPt[self.chart.progress],
|
||||
});
|
||||
}
|
||||
if(self.chart.childExist && dataPt[self.chart.child.name].length != 0){
|
||||
proms.push(self._rpc({
|
||||
route: '/web/dataset/search_read',
|
||||
model: self.chart.child.model,
|
||||
fields: childFields,
|
||||
domain: [['id', 'in', dataPt[self.chart.child.name]]],
|
||||
}).then(self._processGanttChildData.bind(self,dataPt)));
|
||||
}
|
||||
});
|
||||
return Promise.all(proms);
|
||||
},
|
||||
_processGanttChildData: async function(data,rawData){
|
||||
var self = this;
|
||||
rawData.records.forEach(function (dataPt){
|
||||
var start_date = self.formatDate(dataPt[self.chart.child.startDate]);
|
||||
var end_date = self.formatDate(dataPt[self.chart.child.endDate]);
|
||||
self.chart.GanttData.push({
|
||||
resId: 'child_' + dataPt.id,
|
||||
startDate: start_date,
|
||||
endDate: end_date,
|
||||
titleField: dataPt['display_name'],
|
||||
parent: data.id,
|
||||
});
|
||||
});
|
||||
},
|
||||
formatDate: function(date){
|
||||
var d = new Date(date),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
year = d.getFullYear();
|
||||
|
||||
if (month.length < 2)
|
||||
month = '0' + month;
|
||||
if (day.length < 2)
|
||||
day = '0' + day;
|
||||
|
||||
return [day, month, year].join('-');
|
||||
},
|
||||
_processGroupBy: function(groupBy) {
|
||||
const groupBysMap = new Map();
|
||||
for (const gb of groupBy) {
|
||||
let [fieldName, interval] = gb.split(':');
|
||||
const field = this.fields[fieldName];
|
||||
if (['date', 'datetime'].includes(field.type)) {
|
||||
interval = interval || DEFAULT_INTERVAL;
|
||||
}
|
||||
if (groupBysMap.has(fieldName)) {
|
||||
const registeredInterval = groupBysMap.get(fieldName);
|
||||
if (rankInterval(registeredInterval) < rankInterval(interval)) {
|
||||
groupBysMap.set(fieldName, interval);
|
||||
}
|
||||
} else {
|
||||
groupBysMap.set(fieldName, interval);
|
||||
}
|
||||
}
|
||||
return [...groupBysMap].map(([fieldName, interval]) => {
|
||||
if (interval) {
|
||||
return `${fieldName}:${interval}`;
|
||||
}
|
||||
return fieldName;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
167
addons/web_gantt/static/src/js/gantt_renderer.js
Normal file
167
addons/web_gantt/static/src/js/gantt_renderer.js
Normal file
@@ -0,0 +1,167 @@
|
||||
flectra.define('web_gantt.GanttRenderer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractRenderer = require('web.AbstractRenderer');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var dataComparisonUtils = require('web.dataComparisonUtils');
|
||||
var fieldUtils = require('web.field_utils');
|
||||
|
||||
var _t = core._t;
|
||||
var DateClasses = dataComparisonUtils.DateClasses;
|
||||
var qweb = core.qweb;
|
||||
|
||||
return AbstractRenderer.extend({
|
||||
className: "o_gantt_renderer",
|
||||
id: "o_gantt_renderer",
|
||||
sampleDataTargets: ['.o_graph_canvas_container'],
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
* @param {Object} state
|
||||
* @param {Object} params
|
||||
* @param {boolean} [params.isEmbedded]
|
||||
* @param {Object} [params.fields]
|
||||
* @param {string} [params.title]
|
||||
*/
|
||||
init: function (parent, state, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this.isEmbedded = params.isEmbedded || false;
|
||||
this.title = params.title || '';
|
||||
this.fields = params.fields || {};
|
||||
this.modelName = params.modelName;
|
||||
this.startDate = params.startDate;
|
||||
this.endDate = params.endDate;
|
||||
this.titleField = params.titleField;
|
||||
this.progress_scale = params.progress_scale;
|
||||
this.child = params.child;
|
||||
this.childExist = params.childExist;
|
||||
this.disableLinking = params.disableLinking;
|
||||
this.chart = null;
|
||||
this.ganttId = _.uniqueId('gantt');
|
||||
this.$legendTooltip = null;
|
||||
this.$tooltip = null;
|
||||
this.rtlEnable = _t.database.parameters.direction == "rtl" ? true : false;
|
||||
},
|
||||
on_attach_callback: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.isInDOM = true;
|
||||
this._render();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
on_detach_callback: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.isInDOM = false;
|
||||
},
|
||||
_prepareGanttData: function(){
|
||||
var self = this;
|
||||
this.GanttChartData = {"data": [],"links": []};
|
||||
this.state.GanttData.forEach(function (data){
|
||||
var progress_bar = data.progress;
|
||||
if(self.progress_scale){
|
||||
progress_bar = (data.progress) / self.progress_scale;
|
||||
}
|
||||
self.GanttChartData.data.push({
|
||||
id: data.resId,
|
||||
start_date: data.startDate,
|
||||
end_date: data.endDate,
|
||||
text: data.titleField,
|
||||
type: data.type || '',
|
||||
progress: progress_bar ? progress_bar : 0,
|
||||
parent: data.parent ? data.parent : false,
|
||||
});
|
||||
});
|
||||
},
|
||||
async _renderView() {
|
||||
var self = this;
|
||||
this._prepareGanttData();
|
||||
if (this.chart) {
|
||||
|
||||
this.chart.clearAll();
|
||||
this.chart.parse(this.GanttChartData);
|
||||
this._changeState();
|
||||
}else{
|
||||
if (this.isInDOM) {
|
||||
this._renderTitle();
|
||||
gantt.plugins({
|
||||
tooltip: true
|
||||
});
|
||||
gantt.config.show_links = false;
|
||||
if(this.rtlEnable){
|
||||
gantt.config.rtl = true;
|
||||
}
|
||||
gantt.config.date_grid = _t.database.parameters.date_format;
|
||||
gantt.config.auto_scheduling = true;
|
||||
gantt.config.auto_scheduling_strict = true;
|
||||
gantt.i18n.setLocale(moment.locale());
|
||||
gantt.init('o_gantt_renderer');
|
||||
gantt.parse(this.GanttChartData);
|
||||
this.chart = gantt;
|
||||
this.chart.attachEvent("onBeforeLightbox", function(){
|
||||
return false;
|
||||
});
|
||||
this.chart.attachEvent("onAfterTaskDrag", function(id, mode, e){
|
||||
var currentTask = this.getTask(id);
|
||||
self._updateData(currentTask);
|
||||
});
|
||||
this._changeState();
|
||||
}
|
||||
}
|
||||
},
|
||||
_renderTitle: function () {
|
||||
if (this.title) {
|
||||
this.$el.prepend($('<label/>', {
|
||||
text: this.title,
|
||||
}));
|
||||
}
|
||||
},
|
||||
_changeState: function(){
|
||||
if(this.state.mode == 'month'){
|
||||
this.chart.config.scale_unit = "month";
|
||||
this.chart.config.date_scale = "%F, %Y";
|
||||
}else if(this.state.mode == 'week'){
|
||||
this.chart.config.scale_unit = "week";
|
||||
this.chart.config.date_scale = "Week #%W";
|
||||
}else{
|
||||
this.chart.config.scale_unit = "day";
|
||||
this.chart.config.date_scale = "%d %M";
|
||||
}
|
||||
this.chart.render();
|
||||
},
|
||||
_updateData: function(data){
|
||||
var updatedData = {};
|
||||
if(data.id.toString().startsWith('child_')){
|
||||
updatedData[this.child.startDate] = this.formatDate(data.start_date);
|
||||
updatedData[this.child.endDate] = this.formatDate(data.end_date);
|
||||
return this._rpc({
|
||||
model: this.child.model,
|
||||
method: 'write',
|
||||
args: [parseInt(data.id.substring("child_".length)), updatedData],
|
||||
});
|
||||
}else{
|
||||
updatedData[this.startDate] = this.formatDate(data.start_date);
|
||||
updatedData[this.endDate] = this.formatDate(data.end_date);
|
||||
return this._rpc({
|
||||
model: this.modelName,
|
||||
method: 'write',
|
||||
args: [data.id, updatedData],
|
||||
});
|
||||
}
|
||||
},
|
||||
formatDate: function(date){
|
||||
var d = new Date(date),
|
||||
month = '' + (d.getMonth() + 1),
|
||||
day = '' + d.getDate(),
|
||||
year = d.getFullYear();
|
||||
|
||||
if (month.length < 2)
|
||||
month = '0' + month;
|
||||
if (day.length < 2)
|
||||
day = '0' + day;
|
||||
|
||||
return [year, month, day].join('-');
|
||||
},
|
||||
});
|
||||
});
|
||||
171
addons/web_gantt/static/src/js/gantt_view.js
Normal file
171
addons/web_gantt/static/src/js/gantt_view.js
Normal file
@@ -0,0 +1,171 @@
|
||||
flectra.define('web_gantt.GanttView', function (require) {
|
||||
"use strict";
|
||||
|
||||
|
||||
var AbstractView = require('web.AbstractView');
|
||||
var core = require('web.core');
|
||||
var GraphModel = require('web.GraphModel');
|
||||
var Controller = require('web_gantt.GanttController');
|
||||
var GanttRenderer = require('web_gantt.GanttRenderer');
|
||||
var GanttModel = require('web_gantt.GanttModel');
|
||||
var view_registry = require('web.view_registry');
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var searchUtils = require('web.searchUtils');
|
||||
|
||||
var GanttView = AbstractView.extend({
|
||||
display_name: _lt('Gantt'),
|
||||
icon: 'fa-tasks',
|
||||
config: _.extend({}, AbstractView.prototype.config, {
|
||||
Model: GanttModel,
|
||||
Controller: Controller,
|
||||
Renderer: GanttRenderer,
|
||||
}),
|
||||
viewType: 'gantt',
|
||||
searchMenuTypes: ['filter', 'groupBy', 'comparison', 'favorite'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (viewInfo, params) {
|
||||
this._super.apply(this, arguments);
|
||||
const additionalMeasures = params.additionalMeasures || [];
|
||||
let title;
|
||||
let startDate;
|
||||
let endDate;
|
||||
let progress;
|
||||
let measure;
|
||||
let progress_scale;
|
||||
let childExist = false;
|
||||
let child = {};
|
||||
const measures = {};
|
||||
const measureStrings = {};
|
||||
let groupBys = [];
|
||||
const groupableFields = {};
|
||||
this.fields.__count__ = { string: _t("Count"), type: 'integer' };
|
||||
|
||||
this.arch.children.forEach(field => {
|
||||
let fieldName = field.attrs.name;
|
||||
if (fieldName === "id") {
|
||||
return;
|
||||
}
|
||||
const interval = field.attrs.interval;
|
||||
if (interval) {
|
||||
fieldName = fieldName + ':' + interval;
|
||||
}
|
||||
if (field.attrs.type === 'start') {
|
||||
startDate = fieldName;
|
||||
}
|
||||
if(field.attrs.type === 'end'){
|
||||
endDate = fieldName;
|
||||
}
|
||||
if(field.attrs.type === 'title'){
|
||||
title = fieldName;
|
||||
}
|
||||
if(field.attrs.type === 'progress'){
|
||||
progress = fieldName;
|
||||
if(field.attrs.scale){
|
||||
progress_scale = parseInt(field.attrs.scale);
|
||||
}
|
||||
}
|
||||
if(field.attrs.type === 'child'){
|
||||
if(this.fields[fieldName].type == 'many2many' || this.fields[fieldName].type == 'one2many'){
|
||||
if(field.attrs.start && field.attrs.end){
|
||||
child['name'] = fieldName;
|
||||
child['startDate'] = field.attrs.start;
|
||||
child['endDate'] = field.attrs.end;
|
||||
child['model'] = this.fields[fieldName].relation;
|
||||
childExist = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (field.attrs.string) {
|
||||
measureStrings[fieldName] = field.attrs.string;
|
||||
}
|
||||
});
|
||||
|
||||
for (const name in measureStrings) {
|
||||
if (measures[name]) {
|
||||
measures[name].description = measureStrings[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove invisible fields from the measures
|
||||
this.arch.children.forEach(field => {
|
||||
let fieldName = field.attrs.name;
|
||||
if (field.attrs.invisible && py.eval(field.attrs.invisible)) {
|
||||
groupBys = groupBys.filter(groupBy => groupBy !== fieldName);
|
||||
if (fieldName in groupableFields) {
|
||||
delete groupableFields[fieldName];
|
||||
}
|
||||
if (!additionalMeasures.includes(fieldName)) {
|
||||
delete measures[fieldName];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sortedMeasures = Object.values(measures).sort((a, b) => {
|
||||
const descA = a.description.toLowerCase();
|
||||
const descB = b.description.toLowerCase();
|
||||
return descA > descB ? 1 : descA < descB ? -1 : 0;
|
||||
});
|
||||
const countMeasure = {
|
||||
description: _t("Count"),
|
||||
fieldName: '__count__',
|
||||
groupNumber: 1,
|
||||
isActive: false,
|
||||
itemType: 'measure',
|
||||
};
|
||||
this.controllerParams.withButtons = params.withButtons !== false;
|
||||
this.controllerParams.measures = [...sortedMeasures, countMeasure];
|
||||
this.controllerParams.groupableFields = groupableFields;
|
||||
this.controllerParams.child = child;
|
||||
this.controllerParams.childExist = childExist;
|
||||
this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled");
|
||||
// retrieve form and list view ids from the action to open those views
|
||||
// when the graph is clicked
|
||||
function _findView(views, viewType) {
|
||||
const view = views.find(view => {
|
||||
return view.type === viewType;
|
||||
});
|
||||
return [view ? view.viewID : false, viewType];
|
||||
}
|
||||
this.controllerParams.views = [
|
||||
_findView(params.actionViews, 'list'),
|
||||
_findView(params.actionViews, 'form'),
|
||||
];
|
||||
|
||||
this.rendererParams.fields = this.fields;
|
||||
this.rendererParams.modelName = params.modelName;
|
||||
this.rendererParams.startDate = startDate;
|
||||
this.rendererParams.endDate = endDate;
|
||||
this.rendererParams.progress = progress;
|
||||
this.rendererParams.child = child;
|
||||
this.rendererParams.childExist = childExist;
|
||||
this.rendererParams.progress_scale = progress_scale;
|
||||
this.rendererParams.titleField = 'display_name';
|
||||
this.rendererParams.title = this.arch.attrs.title; // TODO: use attrs.string instead
|
||||
this.rendererParams.disableLinking = !!JSON.parse(this.arch.attrs.disable_linking || '0');
|
||||
|
||||
this.loadParams.startDate = startDate;
|
||||
this.loadParams.endDate = endDate;
|
||||
this.loadParams.progress = progress;
|
||||
this.loadParams.child = child;
|
||||
this.loadParams.childExist = childExist;
|
||||
this.loadParams.titleField = 'display_name';
|
||||
this.loadParams.mode = this.arch.attrs.type || 'day';
|
||||
this.loadParams.orderBy = this.arch.attrs.order;
|
||||
this.loadParams.measure = measure || '__count__';
|
||||
this.loadParams.groupBys = groupBys;
|
||||
this.loadParams.fields = this.fields;
|
||||
this.loadParams.comparisonDomain = params.comparisonDomain;
|
||||
this.loadParams.stacked = this.arch.attrs.stacked !== "False";
|
||||
},
|
||||
});
|
||||
view_registry.add('gantt', GanttView);
|
||||
|
||||
return GanttView;
|
||||
|
||||
});
|
||||
34
addons/web_gantt/static/src/xml/gantt_chart.xml
Normal file
34
addons/web_gantt/static/src/xml/gantt_chart.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<templates>
|
||||
|
||||
<t t-name="gantt_chart_popover_template">
|
||||
<div class="oe_tooltip_string" role="tooltip">
|
||||
<t t-esc="title"/>
|
||||
</div>
|
||||
<ul class="oe_tooltip_technical" role="tooltip">
|
||||
<li>
|
||||
<span class="oe_tooltip_technical_title">Start Date:</span>
|
||||
<t t-esc="startDate"/>
|
||||
</li>
|
||||
<li data-item="object">
|
||||
<span class="oe_tooltip_technical_title">End Date:</span>
|
||||
<t t-esc="endDate"/>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="GanttView.buttons">
|
||||
<button class="btn btn-primary">Add</button>
|
||||
<div class="btn-group" role="toolbar" aria-label="Change Mode">
|
||||
<button class="btn btn-secondary o_gantt_button" title="Month Mode" aria-label="Month Modet" data-mode="month">
|
||||
Month
|
||||
</button>
|
||||
<button class="btn btn-secondary o_gantt_button" title="Week Mode" aria-label="Week Mode" data-mode="week">
|
||||
Week
|
||||
</button>
|
||||
<button class="btn btn-secondary o_gantt_button" title="Day Mode" aria-label="Day Mode" data-mode="day">
|
||||
Day
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
19
addons/web_gantt/views/assets.xml
Normal file
19
addons/web_gantt/views/assets.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<flectra>
|
||||
|
||||
<template id="assets_backend_gantt_chart" inherit_id="web.assets_backend" name="Website Backend Assets Gantt Chart">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/web_gantt/static/src/dhtmlx/js/dhtmlxgantt.js"/>
|
||||
|
||||
<script type="text/javascript" src="/web_gantt/static/src/js/gantt_controller.js"/>
|
||||
<script type="text/javascript" src="/web_gantt/static/src/js/gantt_renderer.js"/>
|
||||
<script type="text/javascript" src="/web_gantt/static/src/js/gantt_model.js"/>
|
||||
<script type="text/javascript" src="/web_gantt/static/src/js/gantt_view.js"/>
|
||||
</xpath>
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/web_gantt/static/src/dhtmlx/css/dhtmlxgantt.css"/>
|
||||
|
||||
<link rel="stylesheet" type="text/scss" href="/web_gantt/static/src/css/style.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</flectra>
|
||||
Reference in New Issue
Block a user