Project Gantt View
This commit is contained in:
parent
42bc4fb51a
commit
00e73f0876
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import report
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': "Project Gantt",
|
||||||
|
'summary': """Bridge module for project""",
|
||||||
|
'description': """
|
||||||
|
Bridge module for project
|
||||||
|
""",
|
||||||
|
'category': 'Services/Project',
|
||||||
|
'version': '1.0',
|
||||||
|
'depends': ['project','web_gantt',],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/project_task_views.xml',
|
||||||
|
'views/project_views.xml',
|
||||||
|
'views/project_sharing_templates.xml',
|
||||||
|
'views/project_sharing_views.xml',
|
||||||
|
'views/project_portal_project_task_templates.xml',
|
||||||
|
],
|
||||||
|
'demo': ['data/project_demo.xml'],
|
||||||
|
'auto_install': True,
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'project_gantt/static/src/scss/**/*',
|
||||||
|
'project_gantt/static/src/components/**/*',
|
||||||
|
'project_gantt/static/src/xml/**',
|
||||||
|
'project_gantt/static/src/views/project_task_search_model.js',
|
||||||
|
'project_gantt/static/src/views/project_highlight_tasks.js',
|
||||||
|
'project_gantt/static/src/views/view_dialogs/**',
|
||||||
|
],
|
||||||
|
'web.assets_backend_lazy': [
|
||||||
|
'project_gantt/static/src/views/task_gantt/**',
|
||||||
|
'project_gantt/static/src/views/project_gantt/**',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import project_task
|
||||||
|
from . import project_task_recurrence
|
||||||
|
from . import res_users
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,14 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectTaskRecurrence(models.Model):
|
||||||
|
_inherit = 'project.task.recurrence'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_recurring_fields_to_postpone(self):
|
||||||
|
return super()._get_recurring_fields_to_postpone() + [
|
||||||
|
'planned_date_begin',
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
from odoo.addons.resource.models.utils import Intervals
|
||||||
|
|
||||||
|
class User(models.Model):
|
||||||
|
_inherit = 'res.users'
|
||||||
|
|
||||||
|
# -----------------------------------------
|
||||||
|
# Business Methods
|
||||||
|
# -----------------------------------------
|
||||||
|
def _get_calendars_validity_within_period(self, start, end):
|
||||||
|
""" Gets a dict of dict with user's id as first key and user's calendar as secondary key
|
||||||
|
The value is the validity interval of the calendar for the given user.
|
||||||
|
|
||||||
|
Here the validity interval for each calendar is the whole interval but it's meant to be overriden in further modules
|
||||||
|
handling user's employee contracts.
|
||||||
|
"""
|
||||||
|
assert start.tzinfo and end.tzinfo
|
||||||
|
user_resources = {user: user._get_project_task_resource() for user in self}
|
||||||
|
user_calendars_within_period = defaultdict(lambda: defaultdict(Intervals)) # keys are [user id:integer][calendar:self.env['resource.calendar']]
|
||||||
|
resource_calendars_within_period = self._get_project_task_resource()._get_calendars_validity_within_period(start, end)
|
||||||
|
if not self:
|
||||||
|
# if no user, add the company resource calendar.
|
||||||
|
user_calendars_within_period[False] = resource_calendars_within_period[False]
|
||||||
|
for user, resource in user_resources.items():
|
||||||
|
if resource:
|
||||||
|
user_calendars_within_period[user.id] = resource_calendars_within_period[resource.id]
|
||||||
|
else:
|
||||||
|
calendar = user.resource_calendar_id or user.company_id.resource_calendar_id or self.env.company.resource_calendar_id
|
||||||
|
user_calendars_within_period[user.id][calendar] = Intervals([(start, end, self.env['resource.calendar.attendance'])])
|
||||||
|
return user_calendars_within_period
|
||||||
|
|
||||||
|
def _get_valid_work_intervals(self, start, end, calendars=None):
|
||||||
|
""" Gets the valid work intervals of the user following their calendars between ``start`` and ``end``
|
||||||
|
|
||||||
|
This methods handle the eventuality of a user's resource having multiple resource calendars,
|
||||||
|
see _get_calendars_validity_within_period method for further explanation.
|
||||||
|
"""
|
||||||
|
assert start.tzinfo and end.tzinfo
|
||||||
|
user_calendar_validity_intervals = {}
|
||||||
|
calendar_users = defaultdict(lambda: self.env['res.users'])
|
||||||
|
user_work_intervals = defaultdict(Intervals)
|
||||||
|
calendar_work_intervals = dict()
|
||||||
|
user_resources = {user: user._get_project_task_resource() for user in self}
|
||||||
|
|
||||||
|
user_calendar_validity_intervals = self._get_calendars_validity_within_period(start, end)
|
||||||
|
for user in self:
|
||||||
|
# For each user, retrieve its calendar and their validity intervals
|
||||||
|
for calendar in user_calendar_validity_intervals[user.id]:
|
||||||
|
calendar_users[calendar] |= user
|
||||||
|
for calendar in (calendars or []):
|
||||||
|
calendar_users[calendar] |= self.env['res.users']
|
||||||
|
for calendar, users in calendar_users.items():
|
||||||
|
# For each calendar used by the users, retrieve the work intervals for every users using it
|
||||||
|
work_intervals_batch = calendar._work_intervals_batch(start, end, resources=users._get_project_task_resource())
|
||||||
|
for user in users:
|
||||||
|
# Make the conjunction between work intervals and calendar validity
|
||||||
|
user_work_intervals[user.id] |= work_intervals_batch[user_resources[user].id] & user_calendar_validity_intervals[user.id][calendar]
|
||||||
|
calendar_work_intervals[calendar.id] = work_intervals_batch[False]
|
||||||
|
|
||||||
|
return user_work_intervals, calendar_work_intervals
|
||||||
|
|
||||||
|
def _get_project_task_resource(self):
|
||||||
|
return self.env['resource.resource']
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||||
|
|
||||||
|
from . import project_report
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ReportProjectTaskUser(models.Model):
|
||||||
|
_inherit = 'report.project.task.user'
|
||||||
|
|
||||||
|
planned_date_begin = fields.Datetime("Start date", readonly=True)
|
||||||
|
|
||||||
|
def _select(self):
|
||||||
|
return super()._select() + """,
|
||||||
|
t.planned_date_begin
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _group_by(self):
|
||||||
|
return super()._group_by() + """,
|
||||||
|
t.planned_date_begin
|
||||||
|
"""
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
import { useBus, useService } from '@web/core/utils/hooks';
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { ProjectRightSidePanelSection } from '@project/components/project_right_side_panel/components/project_right_side_panel_section';
|
||||||
|
import { useState } from "@odoo/owl";
|
||||||
|
|
||||||
|
patch(ProjectRightSidePanelSection.prototype, {
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ isClosed: !!this.env.isSmall && this.props.canBeClosed });
|
||||||
|
this.ui = useService('ui');
|
||||||
|
|
||||||
|
useBus(this.ui.bus, "resize", this.setDefaultIsClosed);
|
||||||
|
},
|
||||||
|
|
||||||
|
setDefaultIsClosed() {
|
||||||
|
this.state.isClosed = this.ui.isSmall && this.props.canBeClosed;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSection() {
|
||||||
|
if (!this.env.isSmall || !this.props.canBeClosed) { // then no need to change the value.
|
||||||
|
this.state.isClosed = false;
|
||||||
|
} else {
|
||||||
|
this.state.isClosed = !this.state.isClosed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ProjectRightSidePanelSection.props.canBeClosed = { type: Boolean, optional: true };
|
||||||
|
ProjectRightSidePanelSection.defaultProps.canBeClosed = true;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="project_gantt.ProjectRightSidePanelSection" t-inherit="project.ProjectRightSidePanelSection" t-inherit-mode="extension">
|
||||||
|
<xpath expr="//div[hasclass('o_rightpanel_title')]" position="inside">
|
||||||
|
<span class="me-2" t-if="env.isSmall and props.canBeClosed"><i t-attf-class="fa {{state.isClosed ? 'fa-caret-right' : 'fa-caret-down'}}"></i></span>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[hasclass('o_rightpanel_header')]" position="attributes">
|
||||||
|
<attribute name="t-on-click">toggleSection</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[hasclass('o_rightpanel_data')]" position="attributes">
|
||||||
|
<attribute name="t-attf-class" separator=" " add="{{state.isClosed ? 'o_rightpanel_hidden d-none' : ''}}"/>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
@include media-breakpoint-down(lg) {
|
||||||
|
.o_controller_with_rightpanel .o_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: initial;
|
||||||
|
|
||||||
|
.o_kanban_renderer {
|
||||||
|
width: 100%;
|
||||||
|
overflow: inherit;
|
||||||
|
|
||||||
|
&.o_kanban_ungrouped {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_rightpanel {
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
flex-basis: auto;
|
||||||
|
height: auto;
|
||||||
|
border: 0;
|
||||||
|
padding: 0 $o-rightpanel-p;
|
||||||
|
width: 100%;
|
||||||
|
min-width: auto;
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
|
.o_rightpanel_section {
|
||||||
|
padding-top: $o-rightpanel-p-tiny;
|
||||||
|
padding-bottom: $o-rightpanel-p-tiny;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-inherit="project.ProjectRightSidePanel" t-inherit-mode="extension">
|
||||||
|
<xpath expr="//ProjectRightSidePanelSection[@name="'stat_buttons'"]" position="attributes">
|
||||||
|
<attribute name="canBeClosed">false</attribute>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
.o_kanban_detail_ungrouped > div:not(:last-child) {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Avatar } from "@mail/views/web/fields/avatar/avatar";
|
||||||
|
import { GanttRenderer } from "@web_gantt/gantt_renderer";
|
||||||
|
|
||||||
|
export class ProjectGanttRenderer extends GanttRenderer {
|
||||||
|
static components = {
|
||||||
|
...GanttRenderer.components,
|
||||||
|
Avatar,
|
||||||
|
};
|
||||||
|
static rowHeaderTemplate = "project_gantt.ProjectGanttRenderer.RowHeader";
|
||||||
|
|
||||||
|
computeDerivedParams() {
|
||||||
|
this.rowsWithAvatar = {};
|
||||||
|
super.computeDerivedParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
processRow(row) {
|
||||||
|
const { groupedByField, name, resId } = row;
|
||||||
|
if (groupedByField === "user_id" && Boolean(resId)) {
|
||||||
|
const { fields } = this.model.metaData;
|
||||||
|
const resModel = fields.user_id.relation;
|
||||||
|
this.rowsWithAvatar[row.id] = { resModel, resId, displayName: name };
|
||||||
|
}
|
||||||
|
return super.processRow(...arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvatarProps(row) {
|
||||||
|
return this.rowsWithAvatar[row.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAvatar(row) {
|
||||||
|
return row.id in this.rowsWithAvatar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="project_gantt.ProjectGanttRenderer.RowHeader" t-inherit="web_gantt.GanttRenderer.RowHeader" owl="1">
|
||||||
|
<xpath expr="//t[@t-esc='row.name']" position="replace">
|
||||||
|
<Avatar t-if="hasAvatar(row)" t-props="getAvatarProps(row)"/>
|
||||||
|
<t t-else="" t-esc="row.name"/>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ganttView } from "@web_gantt/gantt_view";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { ProjectGanttRenderer } from "./project_gantt_renderer";
|
||||||
|
|
||||||
|
export const projectGanttView = {
|
||||||
|
...ganttView,
|
||||||
|
Renderer: ProjectGanttRenderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("views").add("project_gantt", projectGanttView);
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export function useProjectModelActions({ getContext, getHighlightPlannedIds }) {
|
||||||
|
const orm = useService("orm");
|
||||||
|
return {
|
||||||
|
async getHighlightIds() {
|
||||||
|
const context = getContext();
|
||||||
|
if (!context || (!context.highlight_conflicting_task && !context.highlight_planned)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.highlight_conflicting_task) {
|
||||||
|
const highlightConflictingIds = await orm.search("project.task", [
|
||||||
|
["planning_overlap", "!=", false],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (context.highlight_planned) {
|
||||||
|
return Array.from(
|
||||||
|
new Set([...highlightConflictingIds, ...getHighlightPlannedIds()])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return highlightConflictingIds;
|
||||||
|
}
|
||||||
|
return getHighlightPlannedIds() || [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { SearchModel } from "@web/search/search_model";
|
||||||
|
|
||||||
|
export class ProjectTaskSearchModel extends SearchModel {
|
||||||
|
exportState() {
|
||||||
|
return {
|
||||||
|
...super.exportState(),
|
||||||
|
highlightPlannedIds: this.highlightPlannedIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_importState(state) {
|
||||||
|
this.highlightPlannedIds = state.highlightPlannedIds;
|
||||||
|
super._importState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivateGroup(groupId) {
|
||||||
|
if (this._getHighlightPlannedSearchItems()?.groupId === groupId) {
|
||||||
|
this.highlightPlannedIds = null;
|
||||||
|
}
|
||||||
|
super.deactivateGroup(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleHighlightPlannedFilter(highlightPlannedIds) {
|
||||||
|
const highlightPlannedSearchItems = this._getHighlightPlannedSearchItems();
|
||||||
|
if (highlightPlannedIds) {
|
||||||
|
this.highlightPlannedIds = highlightPlannedIds;
|
||||||
|
if (highlightPlannedSearchItems) {
|
||||||
|
if (
|
||||||
|
this.query.find(
|
||||||
|
(queryElem) => queryElem.searchItemId === highlightPlannedSearchItems.id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this._notify();
|
||||||
|
} else {
|
||||||
|
this.toggleSearchItem(highlightPlannedSearchItems.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (highlightPlannedSearchItems) {
|
||||||
|
this.deactivateGroup(highlightPlannedSearchItems.groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getHighlightPlannedSearchItems() {
|
||||||
|
return Object.values(this.searchItems).find((v) => v.name === "tasks_scheduled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component } from "@odoo/owl";
|
||||||
|
import { formatDate } from "@web/core/l10n/dates";
|
||||||
|
|
||||||
|
export class MilestonesPopover extends Component {
|
||||||
|
static template = "project_gantt.MilestonesPopover";
|
||||||
|
static props = ["close", "displayMilestoneDates", "displayProjectName", "projects"];
|
||||||
|
|
||||||
|
getDeadline(milestone) {
|
||||||
|
if (!milestone.deadline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return formatDate(milestone.deadline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="project_gantt.MilestonesPopover">
|
||||||
|
<div class="popover-body">
|
||||||
|
<ul class="mb-0 list-unstyled">
|
||||||
|
<li t-foreach="Object.values(props.projects)" t-as="project" t-key="project.id">
|
||||||
|
<t t-if="props.displayProjectName">
|
||||||
|
<div><u><t t-out="project.name"/></u></div>
|
||||||
|
</t>
|
||||||
|
<em t-if="project.isStartDate">Project start</em>
|
||||||
|
<em t-if="project.isDeadline">Project due</em>
|
||||||
|
<ul class="mb-0 list-unstyled">
|
||||||
|
<li t-foreach="project.milestones" t-as="milestone" t-key="milestone_index">
|
||||||
|
<t t-if="milestone.is_deadline_exceeded">
|
||||||
|
<i t-attf-class="fa fa-square-o fa-fw text-start o_unreached_milestones"></i>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<i class="fa fa-fw text-start" t-attf-class="{{milestone.is_reached ? 'fa-check-square-o o_milestones_reached' : 'fa-square-o'}}"></i>
|
||||||
|
</t>
|
||||||
|
<strong><t t-out="milestone.name"/></strong>
|
||||||
|
<span t-if="props.displayMilestoneDates"><br/><t t-out="getDeadline(milestone)"/></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { GanttArchParser } from "@web_gantt/gantt_arch_parser";
|
||||||
|
|
||||||
|
export class TaskGanttArchParser extends GanttArchParser {
|
||||||
|
parse() {
|
||||||
|
const archInfo = super.parse(...arguments);
|
||||||
|
const decorationFields = new Set([...archInfo.decorationFields, "project_id"]);
|
||||||
|
if (archInfo.dependencyEnabled) {
|
||||||
|
decorationFields.add("allow_task_dependencies");
|
||||||
|
decorationFields.add("display_warning_dependency_in_gantt");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...archInfo,
|
||||||
|
decorationFields: [...decorationFields],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { GanttController } from "@web_gantt/gantt_controller";
|
||||||
|
|
||||||
|
export class TaskGanttController extends GanttController {}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
:root {
|
||||||
|
--o-project-milestone-diamond-center: calc(var(--o-project-milestone-diamond-size) / 2);
|
||||||
|
--o-project-milestone-diamond-size: 20px;
|
||||||
|
// 0.707106781186548 being cos(45°), 1.414213562373095 is the length of a diagonal of a square of side length 1
|
||||||
|
// As such the border height needed to construct a triangle is the half of it, so 0.707106781186548.
|
||||||
|
--o-project-milestone-half-diamond-border-border-size: calc(0.707106781186548 * var(--o-project-milestone-diamond-size));
|
||||||
|
--o-project-milestone-half-diamond-border-size: calc(var(--o-project-milestone-half-diamond-border-border-size) - 2px);
|
||||||
|
--o-project-deadline-circle-center: calc(var(--o-project-deadline-circle-size) / 2);
|
||||||
|
--o-project-deadline-circle-size: 10px;
|
||||||
|
}
|
||||||
|
@mixin o_project_milestone {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.o_milestones_reached {
|
||||||
|
color: #00a09d;
|
||||||
|
}
|
||||||
|
.o_unreached_milestones {
|
||||||
|
color: #d3413b;
|
||||||
|
}
|
||||||
|
.o_project_milestone_diamond {
|
||||||
|
@include o_project_milestone;
|
||||||
|
.o_milestones_reached {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
&:not(.edge_slot) {
|
||||||
|
background-color: mix(#00a09d, $o-view-background-color, 10%);
|
||||||
|
border: solid #00a09d 1px;
|
||||||
|
bottom: calc( -1 * var(--o-project-milestone-diamond-center));
|
||||||
|
height: var(--o-project-milestone-diamond-size);
|
||||||
|
right: calc( -1 * var(--o-project-milestone-diamond-center));
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transform-origin: center;
|
||||||
|
width: var(--o-project-milestone-diamond-size);
|
||||||
|
&.o_unreached_milestones {
|
||||||
|
background-color: mix(#d3413b, $o-view-background-color, 10%);
|
||||||
|
border: solid #d3413b 1px;
|
||||||
|
}
|
||||||
|
.o_milestones_reached {
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
&.o_project_deadline_milestone, &.o_project_startdate_milestone {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.edge_slot {
|
||||||
|
border-bottom: var(--o-project-milestone-half-diamond-border-border-size) solid transparent;
|
||||||
|
border-right: var(--o-project-milestone-half-diamond-border-border-size) solid #00a09d;
|
||||||
|
border-top: var(--o-project-milestone-half-diamond-border-border-size) solid transparent;
|
||||||
|
bottom: calc(-1 * var(--o-project-milestone-half-diamond-border-border-size));
|
||||||
|
height: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 0;
|
||||||
|
&.o_unreached_milestones {
|
||||||
|
border-right-color: #d3413b;
|
||||||
|
&:after {
|
||||||
|
border-right-color: #fbeceb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:after{
|
||||||
|
@include o_project_milestone;
|
||||||
|
border-bottom: var(--o-project-milestone-half-diamond-border-size) solid transparent;
|
||||||
|
border-right: var(--o-project-milestone-half-diamond-border-size) solid #e6f6f5;
|
||||||
|
border-top: var(--o-project-milestone-half-diamond-border-size) solid transparent;
|
||||||
|
content: '';
|
||||||
|
height: 0;
|
||||||
|
left: 1px;
|
||||||
|
top: calc(-1 * var(--o-project-milestone-half-diamond-border-size));
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.o_milestones_reached {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: calc(-0.5 * var(--o-project-milestone-half-diamond-border-size));
|
||||||
|
left: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_project_deadline_circle, .o_project_startdate_circle, .o_project_edge_startdate_circle {
|
||||||
|
@include o_project_milestone;
|
||||||
|
bottom: calc( -1 * var(--o-project-deadline-circle-center));
|
||||||
|
height: var(--o-project-deadline-circle-size);
|
||||||
|
width: var(--o-project-deadline-circle-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.o_project_deadline_circle {
|
||||||
|
right: calc( -1 * var(--o-project-deadline-circle-center));
|
||||||
|
background-color: $o-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.o_project_startdate_circle {
|
||||||
|
right: calc( -1 * var(--o-project-deadline-circle-center));
|
||||||
|
background-color: $o-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.o_project_edge_startdate_circle {
|
||||||
|
left: calc( -1 * var(--o-project-deadline-circle-center));
|
||||||
|
background-color: $o-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_gantt_row_total,.o_gantt_cells {
|
||||||
|
.o_project_milestone {
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
@include o-gantt-zindex(interact);
|
||||||
|
border-right: 2px #00a09d solid;
|
||||||
|
&.o_unreached_milestones {
|
||||||
|
border-right: 2px #d3413b solid;
|
||||||
|
}
|
||||||
|
&.o_startdate_pin {
|
||||||
|
border-right: 2px $o-success solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_edge_startdate_pin {
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
@include o-gantt-zindex(interact);
|
||||||
|
border-left: 2px $o-success solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { deserializeDate, deserializeDateTime, serializeDateTime } from "@web/core/l10n/dates";
|
||||||
|
import { GanttModel } from "@web_gantt/gantt_model";
|
||||||
|
import { sortBy } from "@web/core/utils/arrays";
|
||||||
|
import { Domain } from "@web/core/domain";
|
||||||
|
import { useProjectModelActions } from "../project_highlight_tasks";
|
||||||
|
|
||||||
|
const MAP_MANY_2_MANY_FIELDS = [
|
||||||
|
{
|
||||||
|
many2many_field: "personal_stage_type_ids",
|
||||||
|
many2one_field: "personal_stage_type_id",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export class TaskGanttModel extends GanttModel {
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this.getHighlightIds = useProjectModelActions({
|
||||||
|
getContext: () => this.env.searchModel._context,
|
||||||
|
getHighlightPlannedIds: () => this.env.searchModel.highlightPlannedIds,
|
||||||
|
}).getHighlightIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDialogContext() {
|
||||||
|
const context = super.getDialogContext(...arguments);
|
||||||
|
this._replaceSpecialMany2manyKeys(context);
|
||||||
|
if ("user_ids" in context && !context.user_ids) {
|
||||||
|
delete context.user_ids;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleHighlightPlannedFilter(ids) {
|
||||||
|
super.toggleHighlightPlannedFilter(...arguments);
|
||||||
|
this.env.searchModel.toggleHighlightPlannedFilter(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
reschedule(ids, schedule, callback) {
|
||||||
|
if (!schedule.smart_task_scheduling) {
|
||||||
|
return super.reschedule(...arguments);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(ids)) {
|
||||||
|
ids = [ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allData = this._scheduleToData(schedule);
|
||||||
|
const endDateTime = deserializeDateTime(allData.date_deadline).endOf(
|
||||||
|
this.metaData.scale.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = this.removeRedundantData(allData, ids);
|
||||||
|
delete data.name;
|
||||||
|
return this.mutex.exec(async () => {
|
||||||
|
try {
|
||||||
|
const result = await this.orm.call(
|
||||||
|
this.metaData.resModel,
|
||||||
|
"schedule_tasks",
|
||||||
|
[ids, data],
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
...this.searchParams.context,
|
||||||
|
last_date_view: serializeDateTime(endDateTime),
|
||||||
|
cell_part: this.metaData.scale.cellPart,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result && Array.isArray(result) && result.length > 1) {
|
||||||
|
this.toggleHighlightPlannedFilter(Object.keys(result[1]).map(Number));
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_reschedule(ids, data, context) {
|
||||||
|
return this.orm.call(this.metaData.resModel, "web_gantt_write", [ids, data], {
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unscheduleTask(id) {
|
||||||
|
await this.orm.call("project.task", "action_unschedule_task", [id]);
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Protected
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the milestone data based on the task domain and the project deadline if applicable.
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
async _fetchData(metaData, additionalContext) {
|
||||||
|
const globalStart = metaData.globalStart.toISODate();
|
||||||
|
const globalStop = metaData.globalStop.toISODate();
|
||||||
|
const scale = metaData.scale.unit;
|
||||||
|
additionalContext = {
|
||||||
|
...(additionalContext || {}),
|
||||||
|
gantt_start_date: globalStart,
|
||||||
|
gantt_scale: scale,
|
||||||
|
};
|
||||||
|
const proms = [this.getHighlightIds(), super._fetchData(metaData, additionalContext)];
|
||||||
|
let milestones = [];
|
||||||
|
const projectDeadlines = [];
|
||||||
|
const projectStartDates = [];
|
||||||
|
if (!this.orm.isSample && !this.env.isSmall) {
|
||||||
|
const prom = this.orm
|
||||||
|
.call("project.task", "get_all_deadlines", [globalStart, globalStop], {
|
||||||
|
context: this.searchParams.context,
|
||||||
|
})
|
||||||
|
.then(({ milestone_id, project_id }) => {
|
||||||
|
milestones = milestone_id.map((m) => ({
|
||||||
|
...m,
|
||||||
|
deadline: deserializeDate(m.deadline),
|
||||||
|
}));
|
||||||
|
for (const project of project_id) {
|
||||||
|
const dateEnd = project.date;
|
||||||
|
const dateStart = project.date_start;
|
||||||
|
if (dateEnd >= globalStart && dateEnd <= globalStop) {
|
||||||
|
projectDeadlines.push({
|
||||||
|
...project,
|
||||||
|
date: deserializeDate(dateEnd),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (dateStart >= globalStart && dateStart <= globalStop) {
|
||||||
|
projectStartDates.push({
|
||||||
|
...project,
|
||||||
|
date: deserializeDate(dateStart),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
proms.push(prom);
|
||||||
|
}
|
||||||
|
this.highlightIds = (await Promise.all(proms))[0];
|
||||||
|
this.data.milestones = sortBy(milestones, (m) => m.deadline);
|
||||||
|
this.data.projectDeadlines = sortBy(projectDeadlines, (d) => d.date);
|
||||||
|
this.data.projectStartDates = sortBy(projectStartDates, (d) => d.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_generateRows(metaData, params) {
|
||||||
|
const { groupedBy, groups, parentGroup } = params;
|
||||||
|
if (groupedBy.length) {
|
||||||
|
const groupedByField = groupedBy[0];
|
||||||
|
if (groupedByField === "user_ids") {
|
||||||
|
// Here we are generating some rows under a common "parent" (if any).
|
||||||
|
// We make sure that a row with resId = false for "user_id"
|
||||||
|
// ('Unassigned Tasks') and same "parent" will be added by adding
|
||||||
|
// a suitable fake group to groups (a subset of the groups returned
|
||||||
|
// by read_group).
|
||||||
|
const fakeGroup = Object.assign({}, ...parentGroup);
|
||||||
|
groups.push(fakeGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rows = super._generateRows(...arguments);
|
||||||
|
|
||||||
|
// keep empty row to the head and sort the other rows alphabetically
|
||||||
|
// except when grouping by stage or personal stage
|
||||||
|
if (!["stage_id", "personal_stage_type_ids"].includes(groupedBy[0])) {
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
if (a.resId && !b.resId) {
|
||||||
|
return 1;
|
||||||
|
} else if (!a.resId && b.resId) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_getRowName(_, groupedByField, value) {
|
||||||
|
if (!value) {
|
||||||
|
if (groupedByField === "user_ids") {
|
||||||
|
return _t("👤 Unassigned");
|
||||||
|
} else if (groupedByField === "project_id") {
|
||||||
|
return _t("🔒 Private");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super._getRowName(...arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the case of special Many2many Fields, like personal_stage_type_ids in project.task
|
||||||
|
* model, we don't want to write the many2many field but use the inverse method of the
|
||||||
|
* linked Many2one field, in this case the personal_stage_type_id, to create or update the
|
||||||
|
* record - here set the stage_id - in the personal_stage_type_ids.
|
||||||
|
*
|
||||||
|
* This is mandatory since the python ORM doesn't support the creation of
|
||||||
|
* a personnal stage from scratch. If this method is not overriden, then an entry
|
||||||
|
* will be inserted in the project_task_user_rel.
|
||||||
|
* One for the faked Many2many user_ids field (1), and a second one for the other faked
|
||||||
|
* Many2many personal_stage_type_ids field (2).
|
||||||
|
*
|
||||||
|
* While the first one meets the constraint on the project_task_user_rel, the second one
|
||||||
|
* fails because it specifies no user_id; It tries to insert (task_id, stage_id) into the
|
||||||
|
* relation.
|
||||||
|
*
|
||||||
|
* If we don't remove those key from the context, the ORM will face two problems :
|
||||||
|
* - It will try to insert 2 entries in the project_task_user_rel
|
||||||
|
* - It will try to insert an incorrect entry in the project_task_user_rel
|
||||||
|
*
|
||||||
|
* @param {Object} object
|
||||||
|
*/
|
||||||
|
_replaceSpecialMany2manyKeys(object) {
|
||||||
|
for (const { many2many_field, many2one_field } of MAP_MANY_2_MANY_FIELDS) {
|
||||||
|
if (many2many_field in object) {
|
||||||
|
object[many2one_field] = object[many2many_field][0];
|
||||||
|
delete object[many2many_field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_scheduleToData() {
|
||||||
|
const data = super._scheduleToData(...arguments);
|
||||||
|
this._replaceSpecialMany2manyKeys(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
load(searchParams) {
|
||||||
|
const { context, domain, groupBy } = searchParams;
|
||||||
|
let displayUnassigned = false;
|
||||||
|
if (groupBy.length === 0 || groupBy[groupBy.length - 1] === "user_ids") {
|
||||||
|
for (const node of domain) {
|
||||||
|
if (node.length === 3 && node[0] === "user_ids.name" && node[1] === "ilike") {
|
||||||
|
displayUnassigned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (displayUnassigned) {
|
||||||
|
searchParams.domain = Domain.or([domain, "[('user_ids', '=', false)]"]).toList();
|
||||||
|
}
|
||||||
|
return super.load({ ...searchParams, context: { ...context }, displayUnassigned });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
import { SelectCreateAutoPlanDialog } from "@project_gantt/views/view_dialogs/select_auto_plan_create_dialog";
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { Avatar } from "@mail/views/web/fields/avatar/avatar";
|
||||||
|
import { markup, onWillUnmount, useEffect } from "@odoo/owl";
|
||||||
|
import { localization } from "@web/core/l10n/localization";
|
||||||
|
import { usePopover } from "@web/core/popover/popover_hook";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { GanttRenderer } from "@web_gantt/gantt_renderer";
|
||||||
|
import { escape } from "@web/core/utils/strings";
|
||||||
|
import { MilestonesPopover } from "./milestones_popover";
|
||||||
|
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||||
|
import { formatFloatTime } from "@web/views/fields/formatters";
|
||||||
|
|
||||||
|
export class TaskGanttRenderer extends GanttRenderer {
|
||||||
|
static components = {
|
||||||
|
...GanttRenderer.components,
|
||||||
|
Avatar,
|
||||||
|
};
|
||||||
|
static headerTemplate = "project_gantt.TaskGanttRenderer.Header";
|
||||||
|
static rowHeaderTemplate = "project_gantt.TaskGanttRenderer.RowHeader";
|
||||||
|
static rowContentTemplate = "project_gantt.TaskGanttRenderer.RowContent";
|
||||||
|
static totalRowTemplate = "project_gantt.TaskGanttRenderer.TotalRow";
|
||||||
|
static pillTemplate = "project_gantt.TaskGanttRenderer.Pill";
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this.notificationService = useService("notification");
|
||||||
|
this.orm = useService("orm");
|
||||||
|
useEffect(
|
||||||
|
(el) => el.classList.add("o_project_gantt"),
|
||||||
|
() => [this.gridRef.el]
|
||||||
|
);
|
||||||
|
const position = localization.direction === "rtl" ? "bottom" : "right";
|
||||||
|
this.milestonePopover = usePopover(MilestonesPopover, { position });
|
||||||
|
onWillUnmount(() => {
|
||||||
|
this.notificationFn?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
enrichPill(pill) {
|
||||||
|
const enrichedPill = super.enrichPill(pill);
|
||||||
|
if (enrichedPill?.record) {
|
||||||
|
if (
|
||||||
|
this.props.model.highlightIds &&
|
||||||
|
!this.props.model.highlightIds.includes(enrichedPill.record.id)
|
||||||
|
) {
|
||||||
|
pill.className += " opacity-25";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enrichedPill;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeVisibleColumns() {
|
||||||
|
super.computeVisibleColumns();
|
||||||
|
this.columnMilestones = {}; // deadlines and milestones by project
|
||||||
|
for (const column of this.columns) {
|
||||||
|
this.columnMilestones[column.id] = {
|
||||||
|
hasDeadLineExceeded: false,
|
||||||
|
allReached: true,
|
||||||
|
projects: {},
|
||||||
|
hasMilestone: false,
|
||||||
|
hasDeadline: false,
|
||||||
|
hasStartDate: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Handle start date at the beginning of the current period
|
||||||
|
this.columnMilestones[this.columns[0].id].edge = {
|
||||||
|
projects: {},
|
||||||
|
hasStartDate: false,
|
||||||
|
};
|
||||||
|
const projectStartDates = [...this.model.data.projectStartDates];
|
||||||
|
const projectDeadlines = [...this.model.data.projectDeadlines];
|
||||||
|
const milestones = [...this.model.data.milestones];
|
||||||
|
|
||||||
|
let project = projectStartDates.shift();
|
||||||
|
let projectDeadline = projectDeadlines.shift();
|
||||||
|
let milestone = milestones.shift();
|
||||||
|
let i = 0;
|
||||||
|
while (i < this.columns.length && (project || projectDeadline || milestone)) {
|
||||||
|
const column = this.columns[i];
|
||||||
|
const nextColumn = this.columns[i + 1];
|
||||||
|
const info = this.columnMilestones[column.id];
|
||||||
|
|
||||||
|
if (i == 0 && project && column && column.stop > project.date) {
|
||||||
|
// For the first column, start dates have to be displayed at the start of the period
|
||||||
|
if (!info.edge.projects[project.id]) {
|
||||||
|
info.edge.projects[project.id] = {
|
||||||
|
milestones: [],
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
info.edge.projects[project.id].isStartDate = true;
|
||||||
|
info.edge.hasStartDate = true;
|
||||||
|
project = projectStartDates.shift();
|
||||||
|
} else if (project && nextColumn?.stop > project.date) {
|
||||||
|
if (!info.projects[project.id]) {
|
||||||
|
info.projects[project.id] = {
|
||||||
|
milestones: [],
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
info.projects[project.id].isStartDate = true;
|
||||||
|
info.hasStartDate = true;
|
||||||
|
project = projectStartDates.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectDeadline && column.stop > projectDeadline.date) {
|
||||||
|
if (!info.projects[projectDeadline.id]) {
|
||||||
|
info.projects[projectDeadline.id] = {
|
||||||
|
milestones: [],
|
||||||
|
id: projectDeadline.id,
|
||||||
|
name: projectDeadline.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
info.projects[projectDeadline.id].isDeadline = true;
|
||||||
|
info.hasDeadline = true;
|
||||||
|
projectDeadline = projectDeadlines.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (milestone && column.stop > milestone.deadline) {
|
||||||
|
const [projectId, projectName] = milestone.project_id;
|
||||||
|
if (!info.projects[projectId]) {
|
||||||
|
info.projects[projectId] = {
|
||||||
|
milestones: [],
|
||||||
|
id: projectId,
|
||||||
|
name: projectName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { is_deadline_exceeded, is_reached } = milestone;
|
||||||
|
info.projects[projectId].milestones.push(milestone);
|
||||||
|
info.hasMilestone = true;
|
||||||
|
milestone = milestones.shift();
|
||||||
|
if (is_deadline_exceeded) {
|
||||||
|
info.hasDeadLineExceeded = true;
|
||||||
|
}
|
||||||
|
if (!is_reached) {
|
||||||
|
info.allReached = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(!project || !nextColumn || nextColumn?.stop < project.date) &&
|
||||||
|
(!projectDeadline || column.stop < projectDeadline.date) &&
|
||||||
|
(!milestone || column.stop < milestone.deadline)
|
||||||
|
) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
computeDerivedParams() {
|
||||||
|
this.rowsWithAvatar = {};
|
||||||
|
super.computeDerivedParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectorAlert(masterRecord, slaveRecord) {
|
||||||
|
if (
|
||||||
|
masterRecord.display_warning_dependency_in_gantt &&
|
||||||
|
slaveRecord.display_warning_dependency_in_gantt
|
||||||
|
) {
|
||||||
|
return super.getConnectorAlert(...arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPopoverProps(pill) {
|
||||||
|
const props = super.getPopoverProps(...arguments);
|
||||||
|
const { record } = pill;
|
||||||
|
if (record.planning_overlap) {
|
||||||
|
props.context.planningOverlapHtml = markup(record.planning_overlap);
|
||||||
|
}
|
||||||
|
props.context.allocated_hours = formatFloatTime(props.context.allocated_hours);
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvatarProps(row) {
|
||||||
|
return this.rowsWithAvatar[row.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectCreateDialogProps() {
|
||||||
|
const props = super.getSelectCreateDialogProps(...arguments);
|
||||||
|
const onCreateEdit = () => {
|
||||||
|
this.dialogService.add(FormViewDialog, {
|
||||||
|
context: props.context,
|
||||||
|
resModel: props.resModel,
|
||||||
|
onRecordSaved: async (record) => {
|
||||||
|
await record.save({ reload: false });
|
||||||
|
await this.model.fetchData();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onSelectedAutoPlan = (resIds) => {
|
||||||
|
props.context.smart_task_scheduling = true;
|
||||||
|
if (resIds.length) {
|
||||||
|
this.model.reschedule(
|
||||||
|
resIds,
|
||||||
|
props.context,
|
||||||
|
this.openPlanDialogCallback.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
props.onSelectedNoSmartSchedule = props.onSelected;
|
||||||
|
props.onSelected = onSelectedAutoPlan;
|
||||||
|
props.onCreateEdit = onCreateEdit;
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAvatar(row) {
|
||||||
|
return row.id in this.rowsWithAvatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotificationOnSmartSchedule(warningString, old_vals_per_task_id) {
|
||||||
|
this.notificationFn?.();
|
||||||
|
this.notificationFn = this.notificationService.add(
|
||||||
|
markup(
|
||||||
|
`<i class="fa btn-link fa-check"></i><span class="ms-1">${escape(
|
||||||
|
warningString
|
||||||
|
)}</span>`
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "success",
|
||||||
|
sticky: true,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: "Undo",
|
||||||
|
icon: "fa-undo",
|
||||||
|
onClick: async () => {
|
||||||
|
const ids = Object.keys(old_vals_per_task_id).map(Number);
|
||||||
|
await this.orm.call("project.task", "action_rollback_auto_scheduling", [
|
||||||
|
ids,
|
||||||
|
old_vals_per_task_id,
|
||||||
|
]);
|
||||||
|
this.model.toggleHighlightPlannedFilter(false);
|
||||||
|
this.notificationFn();
|
||||||
|
await this.model.fetchData();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
openPlanDialogCallback(res) {
|
||||||
|
if (res && Array.isArray(res)) {
|
||||||
|
const warnings = Object.entries(res[0]);
|
||||||
|
const old_vals_per_task_id = res[1];
|
||||||
|
for (const warning of warnings) {
|
||||||
|
this.notificationService.add(warning[1], {
|
||||||
|
title: _t("Warning"),
|
||||||
|
type: "warning",
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (warnings.length === 0) {
|
||||||
|
this.getNotificationOnSmartSchedule(
|
||||||
|
_t("Tasks have been successfully scheduled for the upcoming periods."),
|
||||||
|
old_vals_per_task_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processRow(row) {
|
||||||
|
const { groupedByField, name, resId } = row;
|
||||||
|
if (groupedByField === "user_ids" && Boolean(resId)) {
|
||||||
|
const { fields } = this.model.metaData;
|
||||||
|
const resModel = fields.user_ids.relation;
|
||||||
|
this.rowsWithAvatar[row.id] = { resModel, resId, displayName: name };
|
||||||
|
}
|
||||||
|
return super.processRow(...arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRenderRecordConnectors(record) {
|
||||||
|
if (record.allow_task_dependencies) {
|
||||||
|
return super.shouldRenderRecordConnectors(...arguments);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightPill(pillId, highlighted) {
|
||||||
|
if (!this.connectorDragState.dragging) {
|
||||||
|
return super.highlightPill(pillId, highlighted);
|
||||||
|
}
|
||||||
|
const pill = this.pills[pillId];
|
||||||
|
if (!pill) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { record } = pill;
|
||||||
|
if (!this.shouldRenderRecordConnectors(record)) {
|
||||||
|
return super.highlightPill(pillId, false);
|
||||||
|
}
|
||||||
|
return super.highlightPill(pillId, highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPlan(rowId, columnStart, columnStop) {
|
||||||
|
const { start, stop } = this.getColumnStartStop(columnStart, columnStop);
|
||||||
|
this.dialogService.add(
|
||||||
|
SelectCreateAutoPlanDialog,
|
||||||
|
this.getSelectCreateDialogProps({ rowId, start, stop, withDefault: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
onMilestoneMouseEnter(ev, projects) {
|
||||||
|
this.milestonePopover.open(ev.target, {
|
||||||
|
displayMilestoneDates: this.model.metaData.scale.id === "year",
|
||||||
|
displayProjectName: !this.model.searchParams.context.default_project_id,
|
||||||
|
projects,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMilestoneMouseLeave() {
|
||||||
|
this.milestonePopover.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="project_gantt.TaskGanttRenderer.RowHeader" t-inherit="web_gantt.GanttRenderer.RowHeader">
|
||||||
|
<xpath expr="//t[@t-esc='row.name']" position="replace">
|
||||||
|
<Avatar t-if="hasAvatar(row)" t-props="getAvatarProps(row)"/>
|
||||||
|
<t t-else="" t-esc="row.name"/>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="project_gantt.TaskGanttRenderer.Header" t-inherit="web_gantt.GanttRenderer.Header">
|
||||||
|
<xpath expr="//t[@t-foreach='columns']/div" position="inside">
|
||||||
|
<t t-set="columnInfo" t-value="columnMilestones[column.id]"/>
|
||||||
|
<t t-if="columnInfo.edge && columnInfo.edge.hasStartDate">
|
||||||
|
<div class="o_project_edge_startdate_circle"
|
||||||
|
t-on-mouseenter="(ev) => this.onMilestoneMouseEnter(ev, columnInfo.edge.projects)"
|
||||||
|
t-on-mouseleave="onMilestoneMouseLeave"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
<t t-if="columnInfo.hasMilestone">
|
||||||
|
<div class="o_project_milestone_diamond"
|
||||||
|
t-att-class="{
|
||||||
|
'o_unreached_milestones': columnInfo.hasDeadLineExceeded,
|
||||||
|
'edge_slot': column_last,
|
||||||
|
'o_project_deadline_milestone': columnInfo.hasDeadline,
|
||||||
|
'o_project_startdate_milestone': !columnInfo.hasDeadline && columnInfo.hasStartDate,
|
||||||
|
}"
|
||||||
|
t-on-mouseenter="(ev) => this.onMilestoneMouseEnter(ev, columnInfo.projects)"
|
||||||
|
t-on-mouseleave="onMilestoneMouseLeave"
|
||||||
|
>
|
||||||
|
<i class="fa fa-check o_milestones_reached" t-att-class="{ 'edge_slot': column_last }" t-if="columnInfo.allReached"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-elif="columnInfo.hasDeadline || columnInfo.hasStartDate">
|
||||||
|
<div
|
||||||
|
t-att-class="{
|
||||||
|
'o_project_deadline_circle': columnInfo.hasDeadline,
|
||||||
|
'o_project_startdate_circle': !columnInfo.hasDeadline && columnInfo.hasStartDate,
|
||||||
|
}"
|
||||||
|
t-on-mouseenter="(ev) => this.onMilestoneMouseEnter(ev, columnInfo.projects)"
|
||||||
|
t-on-mouseleave="onMilestoneMouseLeave"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="project_gantt.TaskGanttRenderer.ColoredCellBorder">
|
||||||
|
<t t-set="columnInfo" t-value="columnMilestones[column.id]"/>
|
||||||
|
<t t-if="columnInfo.edge && columnInfo.edge.hasStartDate">
|
||||||
|
<div class="o_edge_startdate_pin" t-att-style="coloredCellBorderStyle"/>
|
||||||
|
</t>
|
||||||
|
<t t-if="columnInfo.hasMilestone">
|
||||||
|
<div class="o_project_milestone" t-att-style="coloredCellBorderStyle" t-att-class="{ 'o_unreached_milestones': columnInfo.hasDeadLineExceeded }"/>
|
||||||
|
</t>
|
||||||
|
<t t-elif="columnInfo.hasDeadline">
|
||||||
|
<div class="o_project_milestone o_unreached_milestones" t-att-style="coloredCellBorderStyle"/>
|
||||||
|
</t>
|
||||||
|
<t t-elif="columnInfo.hasStartDate">
|
||||||
|
<div class="o_project_milestone o_startdate_pin" t-att-style="coloredCellBorderStyle"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="project_gantt.TaskGanttRenderer.RowContent" t-inherit="web_gantt.GanttRenderer.RowContent">
|
||||||
|
<xpath expr="//div[hasclass('o_gantt_cell')]" position="after">
|
||||||
|
<t t-call="project_gantt.TaskGanttRenderer.ColoredCellBorder">
|
||||||
|
<t t-set="coloredCellBorderStyle" t-value="getGridPosition({ column: column.grid.column, row: row.grid.row })"/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="project_gantt.TaskGanttRenderer.TotalRow" t-inherit="web_gantt.GanttRenderer.TotalRow">
|
||||||
|
<xpath expr="//div[hasclass('o_gantt_cell')]" position="after">
|
||||||
|
<t t-call="project_gantt.TaskGanttRenderer.ColoredCellBorder">
|
||||||
|
<t t-set="coloredCellBorderStyle" t-value="getGridPosition({ column: column.grid.column })"/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="project_gantt.TaskGanttRenderer.Pill" t-inherit="web_gantt.GanttRenderer.Pill">
|
||||||
|
<xpath expr="//div[hasclass('o_gantt_lock')]" position="before">
|
||||||
|
<div t-if="!renderConnectors" class="o_gantt_forbidden fa fa-ban ms-auto me-2" />
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { ganttView } from "@web_gantt/gantt_view";
|
||||||
|
import { TaskGanttController } from "./task_gantt_controller";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { TaskGanttArchParser } from "./task_gantt_arch_parser";
|
||||||
|
import { TaskGanttModel } from "./task_gantt_model";
|
||||||
|
import { TaskGanttRenderer } from "./task_gantt_renderer";
|
||||||
|
import { ProjectTaskSearchModel } from "../project_task_search_model";
|
||||||
|
|
||||||
|
const viewRegistry = registry.category("views");
|
||||||
|
|
||||||
|
export const taskGanttView = {
|
||||||
|
...ganttView,
|
||||||
|
Controller: TaskGanttController,
|
||||||
|
ArchParser: TaskGanttArchParser,
|
||||||
|
Model: TaskGanttModel,
|
||||||
|
Renderer: TaskGanttRenderer,
|
||||||
|
SearchModel: ProjectTaskSearchModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
viewRegistry.add("task_gantt", taskGanttView);
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
.o_gantt_view:has(.o_gantt_renderer:not(.o_connect)) {
|
||||||
|
.o_gantt_pill_wrapper {
|
||||||
|
.o_gantt_forbidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.o_gantt_view:has(.o_gantt_renderer.o_connect) {
|
||||||
|
.o_gantt_pill_wrapper:not(:hover) {
|
||||||
|
.o_gantt_forbidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog"
|
||||||
|
|
||||||
|
export class SelectCreateAutoPlanDialog extends SelectCreateDialog {
|
||||||
|
static template = "project_gantt.SelectCreateAutoPlanDialog";
|
||||||
|
static props = {
|
||||||
|
...SelectCreateDialog.props,
|
||||||
|
onSelectedNoSmartSchedule: { type: Function },
|
||||||
|
}
|
||||||
|
|
||||||
|
select(resIds) {
|
||||||
|
if (this.props.onSelectedNoSmartSchedule) {
|
||||||
|
this.executeOnceAndClose(() => this.props.onSelectedNoSmartSchedule(resIds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectWithSmartSchedule(resIds) {
|
||||||
|
if (this.props.onSelected) {
|
||||||
|
this.executeOnceAndClose(() => this.props.onSelected(resIds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="project_gantt.SelectCreateAutoPlanDialog" t-inherit="web.SelectCreateDialog" t-inherit-mode="primary">
|
||||||
|
<xpath expr="//button[hasclass('o_create_button')]" position="after">
|
||||||
|
<button class="btn btn-secondary o_auto_plan_button"
|
||||||
|
t-att-disabled="state.resIds.length === 0"
|
||||||
|
data-hotkey="k"
|
||||||
|
t-on-click="() => this.selectWithSmartSchedule(state.resIds)"
|
||||||
|
>
|
||||||
|
Auto Plan
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//button[hasclass('o_select_button')]" position="attributes">
|
||||||
|
<attribute name="t-on-click">() => this.select(state.resIds)</attribute>
|
||||||
|
<attribute name="data-hotkey">s</attribute>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="industry_fsm.task_confirm_schedule_warning">
|
||||||
|
<div class="o-tooltip">
|
||||||
|
<span class="text-danger" t-esc="text"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<template id="portal_my_task" inherit_id="project.portal_my_task">
|
||||||
|
<xpath expr="//div[@t-if='task.date_deadline']" position="replace">
|
||||||
|
<div t-if="task.planned_date_begin">
|
||||||
|
<strong>Planned Date:</strong>
|
||||||
|
<span t-field="task.planned_date_begin" t-options='{"widget": "datetime"}'/>
|
||||||
|
<i class="fa fa-long-arrow-right"/>
|
||||||
|
<span t-field="task.date_deadline" t-options='{"widget": "datetime"}'/>
|
||||||
|
</div>
|
||||||
|
<div t-elif="task.date_deadline">
|
||||||
|
<strong>Deadline:</strong> <span t-field="task.date_deadline" t-options='{"widget": "datetime"}'/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<template id="project_sharing_embed" inherit_id="project.project_sharing_embed">
|
||||||
|
<xpath expr="//t[@t-set='head']" position="after">
|
||||||
|
<t t-set="head_project_sharing">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
|
||||||
|
<meta name="theme-color" content="#875A7B"/>
|
||||||
|
</t>
|
||||||
|
<t t-set="head" t-value="head_project_sharing + (head or '')" />
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="project_sharing_project_task_view_form_inherited" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.form.timesheet.inherited</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="priority">400</field>
|
||||||
|
<field name="inherit_id" ref="project.project_sharing_project_task_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//group/field[@name='date_deadline']" position="attributes">
|
||||||
|
<attribute name="nolabel">1</attribute>
|
||||||
|
<attribute name="widget">daterange</attribute>
|
||||||
|
<attribute name="options">{'start_date_field': 'planned_date_begin'}</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//group/field[@name='date_deadline']" position="after">
|
||||||
|
<field name="planned_date_begin" invisible="1"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//group/field[@name='date_deadline']" position="before">
|
||||||
|
<label for="date_deadline" invisible="planned_date_begin"
|
||||||
|
decoration-danger="date_deadline < current_date and state not in ['1_done', '1_canceled']"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//group/label[@for='date_deadline']" position="after">
|
||||||
|
<label for="date_deadline" string="Planned Date" invisible="not planned_date_begin"
|
||||||
|
decoration-danger="date_deadline < current_date and state not in ['1_done', '1_canceled']"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='child_ids']/list//field[@name='date_deadline']" position="attributes">
|
||||||
|
<attribute name="widget">daterange</attribute>
|
||||||
|
<attribute name="options">{'start_date_field': 'planned_date_begin'}</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='child_ids']/list//field[@name='date_deadline']" position="after">
|
||||||
|
<field name="planned_date_begin" column_invisible="True"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_sharing_project_task_view_kanban_inherited" model="ir.ui.view">
|
||||||
|
<field name="name">project.sharing.project.task.view.kanban.inherit</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project.project_sharing_project_task_view_kanban"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="date_deadline" position="attributes">
|
||||||
|
<attribute name="invisible">planned_date_begin or not date_deadline or state in ['1_done', '1_canceled']</attribute>
|
||||||
|
</field>
|
||||||
|
<field name="date_deadline" position="after">
|
||||||
|
<div invisible="not planned_date_begin" t-att-class="(luxon.DateTime.fromISO(record.date_deadline.raw_value) < luxon.DateTime.local() and !['1_done', '1_canceled'].includes(record.state.raw_value)) ? 'text-danger' : ''">
|
||||||
|
<field name="planned_date_begin"/>
|
||||||
|
<i class="fa fa-long-arrow-right mx-2 oe_read_only" aria-label="Arrow icon" title="Arrow" invisible="not planned_date_begin"/>
|
||||||
|
<field name="date_deadline"/>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_sharing_project_task_view_tree_inherited" model="ir.ui.view">
|
||||||
|
<field name="name">project_gantt.project.task.view.list.inherit</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project.project_sharing_project_task_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='date_deadline']" position="attributes">
|
||||||
|
<attribute name="widget">daterange</attribute>
|
||||||
|
<attribute name="options">{'start_date_field': 'planned_date_begin'}</attribute>
|
||||||
|
<attribute name="decoration-danger">date_deadline < current_date and state not in ['1_done', '1_canceled']</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date_deadline']" position="after">
|
||||||
|
<field name="planned_date_begin" column_invisible="True"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="project_task_view_search_conflict_task_project_" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.view.search.conflict.task.project</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project.view_task_search_form_project_fsm_base"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<filter name="unassigned" position="before">
|
||||||
|
<filter name="tasks_scheduled" string="Tasks Scheduled" context="{'highlight_planned': 1}" invisible="1"/>
|
||||||
|
<filter name="conflict_task" string="Tasks in Conflict" context="{'highlight_conflicting_task': 1}" invisible="1"/>
|
||||||
|
</filter>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.view.list.inherit.project.</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project.project_task_view_tree_base"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='date_deadline']" position="attributes">
|
||||||
|
<attribute name="widget">daterange</attribute>
|
||||||
|
<attribute name="options">{'start_date_field': 'planned_date_begin'}</attribute>
|
||||||
|
<attribute name="decoration-danger">date_deadline < current_date and state not in ['1_done', '1_canceled']</attribute>
|
||||||
|
<attribute name="invisible">not project_id</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date_deadline']" position="after">
|
||||||
|
<field name="planned_date_begin" column_invisible="True"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<record id="project_task_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.view.form.inherit.project.</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//t[@name='warning_section']" position="inside">
|
||||||
|
<div role="alert" class="alert alert-warning d-flex flex-wrap gap-3"
|
||||||
|
invisible="not planning_overlap">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fa fa-random me-2" role="img" title="Planning overlap"/>
|
||||||
|
<field name="planning_overlap" widget="html" nolabel="1" class="m-0"/>
|
||||||
|
</div>
|
||||||
|
<a name="action_fsm_view_overlapping_tasks" type="object" class="alert-link ms-auto" invisible="not planning_overlap">
|
||||||
|
Check it out <i class="oi oi-chevron-right ms-2"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div role="alert"
|
||||||
|
class="alert alert-warning d-flex align-items-center"
|
||||||
|
invisible="not dependency_warning">
|
||||||
|
<i class="fa fa-exclamation-circle me-2" role="img" title="Dependency warning"/>
|
||||||
|
<field name="dependency_warning" widget="html" class="m-0"/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//label[@for='date_deadline']" position="attributes">
|
||||||
|
<attribute name="invisible">planned_date_begin</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//label[@for='date_deadline']" position="after">
|
||||||
|
<label for="date_deadline" string="Planned Date" invisible="not planned_date_begin"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date_deadline']" position="attributes">
|
||||||
|
<attribute name="widget">daterange</attribute>
|
||||||
|
<attribute name="options">{'start_date_field': 'planned_date_begin'}</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date_deadline']" position="after">
|
||||||
|
<field name="planned_date_begin" invisible="1"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='child_ids']/list//field[@name='date_deadline']" position="attributes">
|
||||||
|
<attribute name="widget">daterange</attribute>
|
||||||
|
<attribute name="options">{'start_date_field': 'planned_date_begin'}</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='child_ids']/list//field[@name='date_deadline']" position="after">
|
||||||
|
<field name="planned_date_begin" column_invisible="True"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='depend_on_ids']/list//field[@name='date_deadline']" position="attributes">
|
||||||
|
<attribute name="widget">daterange</attribute>
|
||||||
|
<attribute name="options">{'start_date_field': 'planned_date_begin'}</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='depend_on_ids']/list//field[@name='date_deadline']" position="after">
|
||||||
|
<field name="planned_date_begin" column_invisible="True"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_view_form_in_gantt" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.view.form.gantt</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project_task_view_form"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//sheet" position="after">
|
||||||
|
<footer>
|
||||||
|
<button string="Save" special="save" data-hotkey="q" class="btn btn-primary" close="1"/>
|
||||||
|
<button name="action_unschedule_task" string="Unschedule" type="object" data-hotkey="u" class="btn btn-secondary" close="1" invisible="not id"/>
|
||||||
|
<button string="Discard" special="cancel" data-hotkey="x" class="btn btn-secondary" close="1"/>
|
||||||
|
</footer>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Adding manager gantt view to Project -->
|
||||||
|
<record id="project_task_view_gantt" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.view.gantt</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="priority">10</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<gantt date_start="planned_date_begin"
|
||||||
|
form_view_id="%(project_task_view_form_in_gantt)d"
|
||||||
|
date_stop="date_deadline"
|
||||||
|
default_scale="month"
|
||||||
|
scales="day,week,month,year"
|
||||||
|
color="project_id"
|
||||||
|
string="Planning"
|
||||||
|
js_class="task_gantt"
|
||||||
|
display_unavailability="1"
|
||||||
|
precision="{'day': 'hour:quarter', 'week': 'day:half', 'month': 'day:half'}"
|
||||||
|
decoration-danger="planning_overlap"
|
||||||
|
default_group_by="user_ids"
|
||||||
|
progress_bar="user_ids"
|
||||||
|
pill_label="True"
|
||||||
|
total_row="True"
|
||||||
|
dependency_field="depend_on_ids"
|
||||||
|
dependency_inverted_field="dependent_ids">
|
||||||
|
<templates>
|
||||||
|
<div t-name="gantt-popover">
|
||||||
|
<div name="project_id">
|
||||||
|
<strong>Project — </strong>
|
||||||
|
<t t-if="project_id" t-esc="project_id[1]"/>
|
||||||
|
<t t-else=""><span class="fst-italic text-muted"><i class="fa fa-lock"></i> Private</span></t>
|
||||||
|
</div>
|
||||||
|
<div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone">
|
||||||
|
<strong>Milestone — </strong> <t t-esc="milestone_id[1]"/>
|
||||||
|
</div>
|
||||||
|
<div t-if="user_names"><strong>Assignees — </strong> <t t-esc="user_names"/></div>
|
||||||
|
<div t-if="partner_id"><strong>Customer — </strong> <t t-esc="partner_id[1]"/></div>
|
||||||
|
<div t-if="project_id" name="allocated_hours"><strong>Allocated Time — </strong> <t t-esc="allocated_hours"/></div>
|
||||||
|
<div t-if="project_id">
|
||||||
|
<t t-esc="planned_date_begin.toFormat('f ')"/>
|
||||||
|
<i class="fa fa-long-arrow-right" title="Arrow"/>
|
||||||
|
<t t-esc="date_deadline.toFormat(' f')"/>
|
||||||
|
</div>
|
||||||
|
<div class="text-danger mt-2" t-if="planning_overlap">
|
||||||
|
<t t-out="planningOverlapHtml"/>
|
||||||
|
</div>
|
||||||
|
<footer replace="0">
|
||||||
|
<button name="action_unschedule_task" type="object" string="Unschedule" class="btn btn-sm btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
<field name="project_id"/>
|
||||||
|
<field name="allow_milestones"/>
|
||||||
|
<field name="milestone_id"/>
|
||||||
|
<field name="user_ids"/>
|
||||||
|
<field name="user_names"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="planning_overlap"/>
|
||||||
|
<field name="allocated_hours"/>
|
||||||
|
</gantt>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_task_gantt_inherit_all_task" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.all.gantt</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project_task_view_gantt"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//gantt" position="attributes">
|
||||||
|
<attribute name="color">project_id</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_task_gantt_inherit_my_task" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.my.gantt</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project_task_view_gantt"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<gantt position="attributes">
|
||||||
|
<attribute name="default_group_by">project_id</attribute>
|
||||||
|
</gantt>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_dependency_view_gantt" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.dependency.view.gantt</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project_gantt.project_task_view_gantt"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//gantt" position="attributes">
|
||||||
|
<attribute name="color">stage_id</attribute>
|
||||||
|
<attribute name="display_mode">sparse</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[@name='project_id']" position="replace"/>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- All Task action with map view -->
|
||||||
|
<record id="project.action_view_task" model="ir.actions.act_window">
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="view_mode">kanban,list,form,calendar,pivot,graph,gantt,activity</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.action_view_my_task" model="ir.actions.act_window">
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="view_mode">kanban,list,form,gantt,calendar,pivot,graph,activity</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="open_view_my_task_list_gantt" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="30"/>
|
||||||
|
<field name="view_mode">gantt</field>
|
||||||
|
<field name="act_window_id" ref="project.action_view_my_task"/>
|
||||||
|
<field name="view_id" ref="view_task_gantt_inherit_my_task"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.action_view_all_task" model="ir.actions.act_window">
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="view_mode">kanban,list,form,gantt,calendar,pivot,graph,activity</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="open_view_all_task_list_gantt" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="40"/>
|
||||||
|
<field name="view_mode">gantt</field>
|
||||||
|
<field name="act_window_id" ref="project.action_view_all_task"/>
|
||||||
|
<field name="view_id" ref="view_task_gantt_inherit_all_task"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Set map view and gantt view -->
|
||||||
|
<record id="project_all_task_gantt_action_view" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="30"/>
|
||||||
|
<field name="view_mode">gantt</field>
|
||||||
|
<field name="act_window_id" ref="project.act_project_project_2_project_task_all"/>
|
||||||
|
<field name="view_id" ref="project_gantt.project_task_dependency_view_gantt"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<record id="project.action_view_task_from_milestone" model="ir.actions.act_window">
|
||||||
|
<field name="view_mode">kanban,list,gantt,calendar,pivot,activity,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_view_task_from_milestone_kanban_view" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="0"/>
|
||||||
|
<field name="view_mode">kanban</field>
|
||||||
|
<field name="act_window_id" ref="project.action_view_task_from_milestone"/>
|
||||||
|
</record>
|
||||||
|
<record id="action_view_task_from_milestone_tree_view" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="10"/>
|
||||||
|
<field name="view_mode">list</field>
|
||||||
|
<field name="act_window_id" ref="project.action_view_task_from_milestone"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_view_task_from_milestone_gantt_view" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="20"/>
|
||||||
|
<field name="view_mode">gantt</field>
|
||||||
|
<field name="act_window_id" ref="project.action_view_task_from_milestone"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_view_task_from_milestone_calendar_view" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="30"/>
|
||||||
|
<field name="view_mode">calendar</field>
|
||||||
|
<field name="act_window_id" ref="project.action_view_task_from_milestone"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<record id="project.project_task_action_sub_task" model="ir.actions.act_window">
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="view_mode">list,kanban,form,calendar,gantt,pivot,graph,activity</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Views for 'Tasks' stat button via Contact form -->
|
||||||
|
<record id="project.project_task_action_from_partner" model="ir.actions.act_window">
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="view_mode">kanban,list,form,gantt,calendar,pivot,activity</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_view_form_in_gantt_res_partner" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.view.form.gantt.res.partner</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project_task_view_form_in_gantt"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//group//field[@name='project_id']" position="attributes">
|
||||||
|
<attribute name="required">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_view_gantt_res_partner" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.view.gantt.res.partner</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="view_task_gantt_inherit_all_task"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//gantt" position="attributes">
|
||||||
|
<attribute name="form_view_id">%(project_gantt.project_task_view_form_in_gantt_res_partner)d</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_action_from_partner_gantt_view" model="ir.actions.act_window.view">
|
||||||
|
<field name="view_mode">gantt</field>
|
||||||
|
<field name="act_window_id" ref="project.project_task_action_from_partner"/>
|
||||||
|
<field name="view_id" ref="project_task_view_gantt_res_partner"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="project_project_view_gantt" model="ir.ui.view">
|
||||||
|
<field name="name">project.project.view.gantt</field>
|
||||||
|
<field name="model">project.project</field>
|
||||||
|
<field name="priority">10</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<gantt date_start="date_start"
|
||||||
|
date_stop="date"
|
||||||
|
default_scale="month"
|
||||||
|
scales="week,month,year"
|
||||||
|
color="stage_id"
|
||||||
|
string="Planning"
|
||||||
|
display_unavailability="1"
|
||||||
|
js_class="project_gantt"
|
||||||
|
precision="{'week': 'day:full', 'month': 'day:full'}"
|
||||||
|
default_group_by="user_id"
|
||||||
|
total_row="True">
|
||||||
|
<templates>
|
||||||
|
<div t-name="gantt-popover">
|
||||||
|
<div t-if="user_id"><strong>Project Manager — </strong> <t t-esc="user_id[1]"/></div>
|
||||||
|
<div t-if="partner_id"><strong>Customer — </strong> <t t-esc="partner_id[1]"/></div>
|
||||||
|
<div>
|
||||||
|
<t t-esc="date_start.toFormat('D ')"/>
|
||||||
|
<i class="fa fa-long-arrow-right" title="Arrow"/>
|
||||||
|
<t t-esc="date.toFormat(' D')"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
</gantt>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.open_view_project_all_group_stage" model="ir.actions.act_window">
|
||||||
|
<field name="view_mode">kanban,list,form,gantt,calendar,activity</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.open_view_project_all_config_group_stage" model="ir.actions.act_window">
|
||||||
|
<field name="view_mode">list,kanban,form,gantt,calendar,activity</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.projec</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="priority" eval="50"/>
|
||||||
|
<field name="inherit_id" ref="project.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//setting[@id='log_time_tasks_setting']" position="attributes">
|
||||||
|
<attribute name="string">Timesheets</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue