Compare commits

...

50 Commits

Author SHA1 Message Date
administrator 0b74c8df80 19-06-2025
Merge branch 'develop'
2025-06-19 17:29:36 +05:30
administrator 306d6913df Initial commit 2025-06-19 17:28:39 +05:30
administrator 26da8bd171 Initial commit 2025-06-19 17:28:39 +05:30
administrator 9afe78b875 Initial commit 2025-06-19 17:28:39 +05:30
administrator f79f73ea4c Initial commit 2025-06-19 17:28:39 +05:30
administrator 0517a9d451 Initial commit 2025-06-19 17:28:39 +05:30
administrator 0ae0986697 Initial commit 2025-06-19 17:28:39 +05:30
administrator a284e0e52e Initial commit 2025-06-19 17:28:39 +05:30
administrator 252bd42b99 Initial commit 2025-06-19 17:28:39 +05:30
administrator a62d952d43 Initial commit 2025-06-19 17:28:39 +05:30
administrator 15631facd2 Initial commit 2025-06-19 17:28:39 +05:30
administrator 7963086770 Initial commit 2025-06-19 17:28:39 +05:30
administrator 8ef6efc933 Initial commit 2025-06-19 17:28:39 +05:30
administrator 05a5de2b19 Initial commit 2025-06-19 17:28:38 +05:30
administrator c3836240c8 Initial commit 2025-06-19 17:28:38 +05:30
administrator 21f5f0d7ef Initial commit 2025-06-19 17:28:38 +05:30
administrator 4f2eb41439 Initial commit 2025-06-19 17:28:38 +05:30
administrator b1ac31c526 Initial commit 2025-06-19 17:28:38 +05:30
administrator eb32dac1ba Initial commit 2025-06-19 17:28:38 +05:30
administrator 97c3855ed3 Initial commit 2025-06-19 17:28:38 +05:30
administrator a475e8144f Initial commit 2025-06-19 17:28:38 +05:30
administrator 8c8e3bf638 Initial commit 2025-06-19 17:28:38 +05:30
administrator 67862444fa pull commit 2025-06-19 17:28:38 +05:30
administrator 0962e500c0 Initial commit 2025-06-19 17:28:38 +05:30
Pranay 5cf1f228c2 TimeOff Fix 2025-06-19 17:28:38 +05:30
Pranay 121857b882 time-off FIX 2025-06-19 17:28:38 +05:30
Pranay 4bcb965090 Recruitment Changes 2025-06-19 17:28:38 +05:30
Pranay fbeab83d25 fix whatsapp 2025-06-19 17:28:38 +05:30
Pranay d047287eda update whatsapp code 2025-06-19 17:28:38 +05:30
administrator 22222843ce Initial commit 2025-06-19 17:28:38 +05:30
administrator c6e65e7724 Initial commit 2025-06-19 17:28:38 +05:30
administrator 4990ff4f1a Initial commit 2025-06-19 17:28:38 +05:30
administrator 436de449ac Initial commit 2025-06-19 17:28:38 +05:30
administrator 82aee9d70f Initial commit 2025-06-19 17:28:38 +05:30
administrator abdc085473 Initial commit 2025-06-19 17:28:38 +05:30
administrator 417b0115cf Initial commit 2025-06-19 17:28:38 +05:30
administrator e154b72e6b Initial commit 2025-06-19 17:28:38 +05:30
administrator bf87e1d6af Initial commit 2025-06-19 17:28:38 +05:30
administrator 6be5e59688 Initial commit 2025-06-19 17:28:37 +05:30
administrator 6505422b64 Initial commit 2025-06-19 17:28:37 +05:30
administrator 3ffb3a869c Initial commit 2025-06-19 17:28:37 +05:30
administrator 0e576134b3 Initial commit 2025-06-19 17:28:37 +05:30
administrator 0e2cba236b Initial commit 2025-06-19 17:28:37 +05:30
administrator 27adc2c3e2 Initial commit 2025-06-19 17:28:37 +05:30
administrator e5f75df9c7 Initial commit 2025-06-19 17:28:37 +05:30
administrator 3f847fc835 Initial commit 2025-06-19 17:28:37 +05:30
administrator 4439d655c1 Initial commit 2025-06-19 17:28:37 +05:30
administrator 6dc60f7bf0 Initial commit 2025-06-19 17:28:37 +05:30
administrator f9a5bea8b8 Initial commit 2025-06-19 17:28:37 +05:30
raman 00e73f0876 Project Gantt View 2025-06-19 17:25:25 +05:30
38 changed files with 3275 additions and 0 deletions

View File

@ -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

View File

@ -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/**',
],
}
}

View File

@ -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

View File

@ -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',
]

View File

@ -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']

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from . import project_report

View File

@ -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
"""

View File

@ -0,0 +1 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

@ -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;

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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=&quot;'stat_buttons'&quot;]" position="attributes">
<attribute name="canBeClosed">false</attribute>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,5 @@
@include media-breakpoint-down(md) {
.o_kanban_detail_ungrouped > div:not(:last-child) {
padding-bottom: 10px;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);

View File

@ -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() || [];
},
};
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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],
};
}
}

View File

@ -0,0 +1,3 @@
import { GanttController } from "@web_gantt/gantt_controller";
export class TaskGanttController extends GanttController {}

View File

@ -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;
}
}

View File

@ -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 });
}
}

View File

@ -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();
}
}

View File

@ -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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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>

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 &lt; 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 &lt; 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) &lt; 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 &lt; 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>

View File

@ -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 &lt; 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>

View File

@ -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>

View File

@ -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>