PMS Updates

This commit is contained in:
pranay 2025-11-20 10:06:10 +05:30
parent 4ce02e58fa
commit 0fa84c6d43
16 changed files with 2286 additions and 77 deletions

View File

@ -24,6 +24,7 @@ Key Features:
'hr_timesheet', 'hr_timesheet',
'base', 'base',
'analytic', 'analytic',
'project_gantt',
], ],
'data': [ 'data': [
'security/security.xml', 'security/security.xml',
@ -37,6 +38,10 @@ Key Features:
'view/task_stages.xml', 'view/task_stages.xml',
'view/project.xml', 'view/project.xml',
'view/project_task.xml', 'view/project_task.xml',
'view/timesheets.xml',
'view/pro_task_gantt.xml',
'view/user_availability.xml',
# 'view/project_task_gantt.xml',
], ],
'assets': { 'assets': {
}, },

View File

@ -11,6 +11,20 @@
action = records.action_toggle_pause() action = records.action_toggle_pause()
</field> </field>
</record> </record>
<!-- <record id="action_reward_user" model="ir.actions.server">-->
<!-- <field name="name">Reward User</field>-->
<!-- <field name="model_id" ref="project.model_project_task"/>-->
<!-- <field name="binding_model_id" ref="project.model_project_task"/>-->
<!-- <field name="binding_type">action</field>-->
<!-- <field name="binding_view_types">form</field>-->
<!-- <field name="state">code</field>-->
<!-- <field name="code">-->
<!-- if records:-->
<!-- action = records.action_reward_user()-->
<!-- </field>-->
<!-- </record>-->
<data noupdate="1"> <data noupdate="1">
<record id="default_projects_channel" model="discuss.channel"> <record id="default_projects_channel" model="discuss.channel">
<field name="name">Projects Channel</field> <field name="name">Projects Channel</field>

View File

@ -2,3 +2,6 @@ from . import teams
from . import task_stages from . import task_stages
from . import project from . import project
from . import project_task from . import project_task
from . import timesheets
# from . import project_task_gantt
from . import user_availability

View File

@ -15,7 +15,7 @@ class ProjectProject(models.Model):
) )
discuss_channel_id = fields.Many2one( discuss_channel_id = fields.Many2one(
'discuss.channel', 'discuss.channel',
string="Project Channel", string="Channel",
domain="[('parent_channel_id', '=', default_projects_channel_id)]", domain="[('parent_channel_id', '=', default_projects_channel_id)]",
help="Select a channel for project communications. Channels must be sub-channels of the main Projects Channel." help="Select a channel for project communications. Channels must be sub-channels of the main Projects Channel."
) )
@ -159,6 +159,21 @@ class ProjectProject(models.Model):
type_ids = fields.Many2many(default=lambda self: self._default_type_ids()) type_ids = fields.Many2many(default=lambda self: self._default_type_ids())
estimated_hours = fields.Float(string="Estimated Hours")
task_estimated_hours = fields.Float(string="Task Estimated Hours", compute="_compute_task_estimated_hours", store=True)
actual_hours = fields.Float(string="Actual Hours", compute="_compute_actual_hours", store=True)
@api.depends('task_ids.estimated_hours')
def _compute_task_estimated_hours(self):
for project in self:
project.task_estimated_hours = sum(project.task_ids.mapped('estimated_hours'))
@api.depends('task_ids.timesheet_ids.unit_amount')
def _compute_actual_hours(self):
for project in self:
project.actual_hours = sum(project.task_ids.timesheet_ids.mapped('unit_amount'))
def add_users(self): def add_users(self):
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',

View File

@ -0,0 +1,279 @@
from odoo import models, fields, api, _
from datetime import datetime, date
from odoo.exceptions import ValidationError
class ProjectTask(models.Model):
_inherit = 'project.task'
# Existing fields
gantt_color = fields.Char(compute='_compute_gantt_color', store=True)
gantt_user_id = fields.Many2one('res.users', compute='_compute_gantt_user_id', store=True)
performance_icon = fields.Char(compute='_compute_performance_icon', store=True)
is_overdue = fields.Boolean(compute='_compute_is_overdue', store=True)
gantt_date_start = fields.Datetime(compute='_compute_gantt_dates', store=True, inverse='_inverse_gantt_date_start')
current_stage_performance = fields.Selection([
('good', 'Good'),
('normal', 'Normal'),
('bad', 'Bad')
], compute='_compute_current_stage_performance', store=True)
formatted_start_date = fields.Char(compute='_compute_formatted_dates')
formatted_end_date = fields.Char(compute='_compute_formatted_dates')
# New fields for better Gantt view
progress = fields.Integer(string="Progress", compute='_compute_progress', store=True)
task_priority = fields.Selection([
('0', 'Normal'),
('1', 'High'),
('2', 'Urgent')
], default='0', string="Priority")
is_completed = fields.Boolean(compute='_compute_is_completed', store=True)
completion_date = fields.Datetime(string="Completion Date")
days_remaining = fields.Integer(compute='_compute_days_remaining', store=True)
@api.depends('stage_id', 'project_id.type_ids')
def _compute_progress(self):
for task in self:
if not task.stage_id or not task.project_id.type_ids:
task.progress = 0
continue
stages = task.project_id.type_ids.sorted('sequence')
total_stages = len(stages)
if total_stages == 0:
task.progress = 0
continue
current_stage_index = next((i for i, stage in enumerate(stages) if stage.id == task.stage_id.id), 0)
task.progress = int((current_stage_index + 1) / total_stages * 100)
@api.depends('state')
def _compute_is_completed(self):
for task in self:
task.is_completed = task.state in ['1_done', 'done']
@api.depends('date_deadline')
def _compute_days_remaining(self):
today = date.today()
for task in self:
if not task.date_deadline:
task.days_remaining = 0
continue
if isinstance(task.date_deadline, datetime):
deadline_date = task.date_deadline.date()
else:
deadline_date = task.date_deadline
delta = deadline_date - today
task.days_remaining = delta.days
@api.depends('planned_date_begin', 'date_deadline')
def _compute_formatted_dates(self):
for task in self:
if task.planned_date_begin:
if isinstance(task.planned_date_begin, datetime):
task.formatted_start_date = task.planned_date_begin.strftime('%d-%b-%Y %I:%M %p')
else:
task.formatted_start_date = task.planned_date_begin.strftime('%d-%b-%Y')
else:
task.formatted_start_date = False
if task.date_deadline:
if isinstance(task.date_deadline, datetime):
task.formatted_end_date = task.date_deadline.strftime('%d-%b-%Y %I:%M %p')
else:
task.formatted_end_date = task.date_deadline.strftime('%d-%b-%Y')
else:
task.formatted_end_date = False
@api.depends('show_approval_flow', 'assignees_timelines.actual_time', 'assignees_timelines.estimated_time')
def _compute_current_stage_performance(self):
for task in self:
# Don't compute performance for completed tasks
if task.is_completed:
task.current_stage_performance = False
continue
if task.show_approval_flow:
current_timeline = task.assignees_timelines.filtered(lambda t: t.stage_id == task.stage_id)
if current_timeline:
timeline = current_timeline[0]
actual = timeline.actual_time
estimated = timeline.estimated_time
if actual < estimated:
task.current_stage_performance = 'good'
elif actual == estimated:
task.current_stage_performance = 'normal'
else:
task.current_stage_performance = 'bad'
else:
task.current_stage_performance = False
else:
task.current_stage_performance = False
@api.depends('planned_date_begin')
def _compute_gantt_dates(self):
for task in self:
if task.planned_date_begin:
# Ensure we're working with a datetime object
if isinstance(task.planned_date_begin, date) and not isinstance(task.planned_date_begin, datetime):
# Convert date to datetime at start of day
task.gantt_date_start = datetime.combine(task.planned_date_begin, datetime.min.time())
else:
task.gantt_date_start = task.planned_date_begin
else:
task.gantt_date_start = False
def _inverse_gantt_date_start(self):
for task in self:
if task.gantt_date_start:
task.planned_date_begin = task.gantt_date_start
@api.depends('assignees_timelines.assigned_to', 'user_ids', 'show_approval_flow', 'stage_id')
def _compute_gantt_user_id(self):
for task in self:
if task.show_approval_flow:
current_timeline = task.assignees_timelines.filtered(lambda t: t.stage_id == task.stage_id)
if current_timeline:
task.gantt_user_id = current_timeline[0].assigned_to
else:
task.gantt_user_id = task.user_ids[:1] if task.user_ids else self.env.user
else:
task.gantt_user_id = task.user_ids[:1] if task.user_ids else self.env.user
@api.depends('show_approval_flow', 'assignees_timelines.actual_time', 'assignees_timelines.estimated_time')
def _compute_performance_icon(self):
for task in self:
# Don't show performance icon for completed tasks
if task.is_completed:
task.performance_icon = False
continue
if task.show_approval_flow:
current_timeline = task.assignees_timelines.filtered(lambda t: t.stage_id == task.stage_id)
if current_timeline:
timeline = current_timeline[0]
actual = timeline.actual_time
estimated = timeline.estimated_time
if actual < estimated:
task.performance_icon = 'fa-smile-o'
elif actual == estimated:
task.performance_icon = 'fa-meh-o'
else:
task.performance_icon = 'fa-frown-o'
else:
task.performance_icon = 'fa-question'
else:
task.performance_icon = False
@api.depends('date_deadline')
def _compute_is_overdue(self):
today = date.today()
for task in self:
# Completed tasks are never overdue
if task.is_completed:
task.is_overdue = False
continue
if not task.date_deadline:
task.is_overdue = False
continue
# Convert datetime to date if needed
if isinstance(task.date_deadline, datetime):
deadline_date = task.date_deadline.date()
else:
deadline_date = task.date_deadline
task.is_overdue = deadline_date < today
@api.depends('show_approval_flow', 'assignees_timelines.actual_time', 'assignees_timelines.estimated_time',
'date_deadline', 'state', 'is_completed')
def _compute_gantt_color(self):
today = date.today()
for task in self:
# If task is completed, always show green
if task.is_completed:
task.gantt_color = '#28a745' # Green
continue
if task.show_approval_flow:
current_timeline = task.assignees_timelines.filtered(lambda t: t.stage_id == task.stage_id)
if current_timeline:
timeline = current_timeline[0]
actual = timeline.actual_time
estimated = timeline.estimated_time
if actual < estimated:
task.gantt_color = '#28a745' # Green
elif actual == estimated:
task.gantt_color = '#007bff' # Blue
else:
task.gantt_color = '#dc3545' # Red
else:
task.gantt_color = '#6c757d' # Gray if no timeline
else:
# Color based on deadline
if task.date_deadline:
# Convert datetime to date if needed
if isinstance(task.date_deadline, datetime):
deadline_date = task.date_deadline.date()
else:
deadline_date = task.date_deadline
if deadline_date < today:
task.gantt_color = '#dc3545' # Red for overdue
else:
task.gantt_color = '#28a745' # Green for on time
else:
task.gantt_color = '#6c757d' # Gray if no deadline
@api.model
def _expand_domain_dates(self, domain):
new_domain = []
for dom in domain:
if dom[0] in ['date_start', 'date_end', 'date_deadline', 'gantt_date_start'] and dom[1] in ['>=', '<=', '>',
'<', '=']:
# Handle both datetime and date string formats
if isinstance(dom[2], datetime):
min_date = dom[2]
else:
try:
# Try parsing with time first
min_date = datetime.strptime(dom[2], '%Y-%m-%d %H:%M:%S')
except ValueError:
try:
# If that fails, try parsing without time
min_date = datetime.strptime(dom[2], '%Y-%m-%d')
except ValueError:
# If both fail, use the original value
min_date = dom[2]
new_domain.append((dom[0], dom[1], min_date))
else:
new_domain.append(dom)
return new_domain
def write(self, vals):
# Handle date validation more flexibly
if 'planned_date_begin' in vals and 'date_deadline' in vals:
if vals['planned_date_begin'] and vals['date_deadline']:
if vals['planned_date_begin'] > vals['date_deadline']:
raise ValidationError(_("Planned start date must be before planned end date."))
elif 'planned_date_begin' in vals and vals['planned_date_begin'] and self.date_deadline:
if vals['planned_date_begin'] > self.date_deadline:
raise ValidationError(_("Planned start date must be before planned end date."))
elif 'date_deadline' in vals and vals['date_deadline'] and self.planned_date_begin:
if self.planned_date_begin > vals['date_deadline']:
raise ValidationError(_("Planned start date must be before planned end date."))
# Update gantt_date_start when planned_date_begin changes
if 'planned_date_begin' in vals:
vals['gantt_date_start'] = vals['planned_date_begin']
# Set completion date when task is marked as done
if 'state' in vals and vals['state'] in ['1_done', 'done']:
vals['completion_date'] = fields.Datetime.now()
return super(ProjectTask, self).write(vals)

View File

@ -0,0 +1,19 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
stage_id = fields.Many2one(
'project.task.type',
string="Stage",
domain="[('id', 'in', stage_ids)]"
)
stage_ids = fields.Many2many('project.task.type',related="task_id.project_id.type_ids")
@api.constrains('stage_id', 'task_id')
def _check_stage_required(self):
for line in self:
if line.task_id.show_approval_flow and not line.stage_id:
raise ValidationError(_("Stage is required when Approval Flow is enabled for this task."))

View File

@ -0,0 +1,202 @@
from odoo import models, fields, api, _
class UserTaskAvailability(models.Model):
_name = 'user.task.availability'
_description = 'User Task Availability'
_auto = False
_order = 'work_start_datetime'
# --------------------------------------------------------------
# VIEW FIELDS
# --------------------------------------------------------------
user_id = fields.Many2one('res.users', string="User", readonly=True)
task_id = fields.Many2one('project.task', string="Task", readonly=True)
project_id = fields.Many2one('project.project', string="Project", readonly=True)
stage_id = fields.Many2one('project.task.type', string="Stage", readonly=True)
task_name = fields.Char(readonly=True)
task_sequence = fields.Char(readonly=True)
is_generic = fields.Boolean(readonly=True)
work_start_datetime = fields.Datetime(string="Work Start", readonly=True)
work_end_datetime = fields.Datetime(string="Work End", readonly=True)
estimated_hours = fields.Float(string="Estimated Hours", readonly=True)
actual_hours = fields.Float(string="Actual Hours")
source_type = fields.Selection([
('timeline', 'Timeline'),
('task', 'Task'),
], string="Source", readonly=True)
# task state options
state = fields.Selection([
('01_in_progress', 'In Progress'),
('02_changes_requested', 'Changes Requested'),
('03_approved', 'Approved'),
('1_done', 'Done'),
('1_canceled', 'Cancelled'),
('04_waiting_normal', 'Waiting'),
], string="Task State", readonly=True)
status_label = fields.Char(string="Status Label", readonly=True)
progress_emoji = fields.Char(compute='_compute_progress_emoji', string='Progress Emoji')
task_status_color = fields.Integer(compute='_compute_task_status_color', string='Status Color')
progress_percentage = fields.Float(compute='_compute_progress_percentage', string='Progress %')
is_overdue = fields.Boolean(compute='_compute_is_overdue', string='Is Overdue')
is_future = fields.Boolean(compute='_compute_is_future', string='Is Future')
@api.depends('actual_hours', 'estimated_hours')
def _compute_progress_emoji(self):
for record in self:
if record.actual_hours <= record.estimated_hours:
record.progress_emoji = '😊'
elif record.actual_hours > record.estimated_hours:
record.progress_emoji = '😞'
else:
record.progress_emoji = ''
@api.depends('state', 'work_start_datetime', 'work_end_datetime')
def _compute_task_status_color(self):
today = fields.Date.today()
for record in self:
if record.state == '1_done':
record.task_status_color = 10 # Green
elif record.state == '1_canceled':
record.task_status_color = 0
elif record.work_start_datetime and record.work_start_datetime.date() > today:
record.task_status_color = 0 # Gray
elif record.work_end_datetime and record.work_end_datetime.date() < today and record.state != '1_done':
record.task_status_color = 1 # Red
else:
record.task_status_color = 8 # Default blue
@api.depends('actual_hours', 'estimated_hours')
def _compute_progress_percentage(self):
for record in self:
if record.estimated_hours > 0:
record.progress_percentage = min(100, (record.actual_hours / record.estimated_hours) * 100)
else:
record.progress_percentage = 0
@api.depends('work_end_datetime', 'state')
def _compute_is_overdue(self):
today = fields.Date.today()
for record in self:
record.is_overdue = (record.work_end_datetime and record.work_end_datetime.date() < today and record.state != '1_done') or (record.actual_hours > record.estimated_hours)
@api.depends('work_start_datetime')
def _compute_is_future(self):
today = fields.Date.today()
for record in self:
record.is_future = record.work_start_datetime and record.work_start_datetime.date() > today
@api.depends('task_id', 'task_sequence', 'stage_id')
def _compute_display_name(self):
for rec in self:
rec.display_name = f"{rec.task_sequence}" if rec.task_sequence else rec.task_id.name
# --------------------------------------------------------------
# CREATE SQL VIEW
# --------------------------------------------------------------
def init(self):
self.env.cr.execute("""DROP VIEW IF EXISTS user_task_availability CASCADE;""")
self.env.cr.execute("""
CREATE OR REPLACE VIEW user_task_availability AS (
-----------------------------------------------------------------
-- 1 MAIN SOURCE: project.task.time.lines
-- Only when timelines_requested + show_approval_flow are TRUE
-----------------------------------------------------------------
SELECT
ROW_NUMBER() OVER () AS id,
tl.assigned_to AS user_id,
tl.task_id AS task_id,
t.project_id AS project_id,
tl.stage_id AS stage_id,
t.name AS task_name,
t.sequence_name AS task_sequence,
t.is_generic AS is_generic,
tl.estimated_start_datetime AS work_start_datetime,
tl.estimated_end_datetime AS work_end_datetime,
tl.estimated_time AS estimated_hours,
tl.actual_time AS actual_hours,
'timeline' AS source_type,
t.state AS state,
CASE
WHEN t.state = '01_in_progress' THEN 'In Progress'
WHEN t.state = '02_changes_requested' THEN 'Changes Requested'
WHEN t.state = '03_approved' THEN 'Approved'
WHEN t.state = '1_done' THEN 'Done'
WHEN t.state = '1_canceled' THEN 'Cancelled'
WHEN t.state = '04_waiting_normal' THEN 'Waiting'
ELSE 'Unknown'
END AS status_label
FROM project_task_time_lines tl
JOIN project_task t ON t.id = tl.task_id
WHERE t.timelines_requested = TRUE
AND t.show_approval_flow = TRUE
AND tl.assigned_to IS NOT NULL
AND tl.estimated_start_datetime IS NOT NULL
AND tl.estimated_end_datetime IS NOT NULL
UNION ALL
-----------------------------------------------------------------
-- 2 FALLBACK SOURCE: project.task + user_ids (Many2many)
-- Only tasks WITHOUT timeline entries for that user
-----------------------------------------------------------------
SELECT
ROW_NUMBER() OVER () + 1000000 AS id, -- avoid overlap
rel.user_id AS user_id,
t.id AS task_id,
t.project_id AS project_id,
t.stage_id AS stage_id,
t.name AS task_name,
t.sequence_name AS task_sequence,
t.is_generic AS is_generic,
COALESCE(t.date_assign, t.create_date) AS work_start_datetime,
t.date_deadline AS work_end_datetime,
t.estimated_hours AS estimated_hours,
t.actual_hours AS actual_hours,
'task' AS source_type,
t.state AS state,
CASE
WHEN t.state = '01_in_progress' THEN 'In Progress'
WHEN t.state = '02_changes_requested' THEN 'Changes Requested'
WHEN t.state = '03_approved' THEN 'Approved'
WHEN t.state = '1_done' THEN 'Done'
WHEN t.state = '1_canceled' THEN 'Cancelled'
WHEN t.state = '04_waiting_normal' THEN 'Waiting'
ELSE 'Unknown'
END AS status_label
FROM project_task t
JOIN project_task_user_rel rel ON rel.task_id = t.id
WHERE NOT EXISTS (
SELECT 1
FROM project_task_time_lines tl2
WHERE tl2.task_id = t.id
AND tl2.assigned_to = rel.user_id
AND tl2.estimated_start_datetime IS NOT NULL
AND tl2.estimated_end_datetime IS NOT NULL
)
);
""")

View File

@ -19,3 +19,5 @@ access_project_tags_supervisor,project.project_tags_supervisor,project.model_pro
access_project_task_time_lines_user,access_project_task_time_lines_user,model_project_task_time_lines,base.group_user,1,1,1,1 access_project_task_time_lines_user,access_project_task_time_lines_user,model_project_task_time_lines,base.group_user,1,1,1,1
access_project_task_time_lines_manager,access_project_task_time_lines_manager,model_project_task_time_lines,project.group_project_manager,1,1,1,1 access_project_task_time_lines_manager,access_project_task_time_lines_manager,model_project_task_time_lines,project.group_project_manager,1,1,1,1
access_user_task_availability,user.task.availability.access,model_user_task_availability,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
19
20
21
22
23

View File

@ -65,6 +65,19 @@
<field name="perm_unlink" eval="0"/> <field name="perm_unlink" eval="0"/>
</record> </record>
<record model="ir.rule" id="user_task_availability_project_lead_rule">
<field name="name">Task Availability: project lead: see all user tasks</field>
<field name="model_id" ref="model_user_task_availability"/>
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
<field name="domain_force">[
'|', '|',
('project_id.project_lead', '=', user.id),
('user_id', '=', user.id),
('project_id.user_id', '=', user.id),
]
</field>
</record>
<!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">--> <!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">-->
<!-- <field name="name">timesheet: users: see own tasks</field>--> <!-- <field name="name">timesheet: users: see own tasks</field>-->
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>--> <!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="project_gantt.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_gantt.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>
</odoo>

View File

@ -12,6 +12,7 @@
</h1> </h1>
</group> </group>
</xpath> </xpath>
<xpath expr="//field[@name='user_id']" position="after"> <xpath expr="//field[@name='user_id']" position="after">
<field name="project_lead" widget="many2one_avatar_user"/> <field name="project_lead" widget="many2one_avatar_user"/>
</xpath> </xpath>
@ -80,4 +81,17 @@
</page> </page>
</field> </field>
</record> </record>
<record id="project_invoice_form_inherit" model="ir.ui.view">
<field name="name">project.invoice.inherit.form.view</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='allocated_hours']" position="after">
<field name="estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="task_estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
</field>
</record>
</odoo> </odoo>

View File

@ -23,23 +23,33 @@
<!-- <field name="assigned_team"/>--> <!-- <field name="assigned_team"/>-->
<!-- </xpath>--> <!-- </xpath>-->
<xpath expr="//field[@name='user_ids']" position="after"> <xpath expr="//field[@name='user_ids']" position="after">
<field name="is_generic"/> <field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/> <field name="record_paused" invisible="1"/>
</xpath> </xpath>
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>-->
<!-- <field name="actual_hours"/>-->
<!-- </xpath>-->
<xpath expr="//sheet/notebook" position="inside"> <xpath expr="//sheet/notebook" position="inside">
<page string="Assignees Timelines" invisible="not show_approval_flow"> <page string="Assignees Timelines" invisible="not show_approval_flow">
<button name="button_update_assignees" type="object" string="Update Assignees" class="oe_highlight"/> <button name="button_update_assignees" type="object" string="Update Assignees" class="oe_highlight"/>
<button name="action_assign_approx_deadlines" type="object" string="Assign Approx Timeline" class="oe_highlight"/>
<field name="assignees_timelines" context="{'default_task_id': id}" > <field name="assignees_timelines" context="{'default_task_id': id}" >
<list editable="bottom"> <list editable="bottom">
<field name="stage_id"/> <field name="stage_id" readonly="not has_edit_access"/>
<field name="responsible_lead"/> <field name="responsible_lead" readonly="not has_edit_access"/>
<field name="team_id"/> <field name="team_id" readonly="not has_edit_access"/>
<field name="assigned_to"/> <field name="assigned_to" readonly="not has_edit_access"/>
<!-- <field name="team_all_member_ids" widget="many2many_tags"/>--> <!-- <field name="team_all_member_ids" widget="many2many_tags"/>-->
<field name="estimated_time" widget="float_time"/> <field name="estimated_time" widget="float_time" readonly="not has_edit_access or estimated_time_readonly"/>
<field name="actual_time" readonly="1" optional="hide" widget="float_time"/> <field name="actual_time" readonly="1" optional="hide" widget="float_time"/>
<field name="request_date" readonly="1" optional="hide"/> <field name="has_edit_access" column_invisible="True" optional="hide" readonly="1"/>
<field name="done_date" readonly="1" optional="hide"/> <field name="estimated_time_readonly" column_invisible="True" optional="hide" readonly="1"/>
<field name="estimated_start_datetime" optional="show" readonly="not has_edit_access"/>
<field name="estimated_end_datetime" optional="show" readonly="not has_edit_access"/>
<!-- <field name="request_date" readonly="1" optional="hide"/>-->
<!-- <field name="done_date" readonly="1" optional="hide"/>-->
</list> </list>
</field> </field>
</page> </page>
@ -48,11 +58,11 @@
</page> </page>
</xpath> </xpath>
<xpath expr="//header" position="inside"> <xpath expr="//header" position="inside">
<button type="object" name="request_timelines" string="Request Timelines" class="oe_highlight" invisible="not show_approval_flow or timelines_requested"/> <button type="object" name="request_timelines" string="Request Timelines" class="oe_highlight" invisible="not show_approval_flow or timelines_requested or record_paused"/>
<button type="object" name="submit_for_approval" string="Request Approval" class="oe_highlight" invisible="not show_approval_flow or not show_submission_button or not timelines_requested"/> <button type="object" name="submit_for_approval" string="Request Approval" class="oe_highlight" invisible="not show_approval_flow or not show_submission_button or not timelines_requested or record_paused"/>
<button type="object" name="proceed_further" string="Approve &amp; Proceed" class="oe_highlight" invisible="not show_approval_flow or not show_approval_button or not timelines_requested"/> <button type="object" name="proceed_further" string="Approve &amp; Proceed" class="oe_highlight" invisible="not show_approval_flow or not show_approval_button or not timelines_requested or record_paused"/>
<button type="object" name="action_open_reject_wizard" string="Reject &amp; Return" class="oe_highlight" invisible="not show_approval_flow or not show_refuse_button or not timelines_requested"/> <button type="object" name="action_open_reject_wizard" string="Reject &amp; Return" class="oe_highlight" invisible="not show_approval_flow or not show_refuse_button or not timelines_requested or record_paused"/>
<button type="object" name="back_button" string="Go Back" class="oe_highlight" invisible="not show_approval_flow or not show_back_button or not timelines_requested"/> <button type="object" name="back_button" string="Go Back" class="oe_highlight" invisible="not show_approval_flow or not show_back_button or not timelines_requested or record_paused"/>
</xpath> </xpath>
<xpath expr="//form" position="inside"> <xpath expr="//form" position="inside">
<field name="approval_status" invisible="1"/> <field name="approval_status" invisible="1"/>
@ -62,6 +72,7 @@
<field name="show_approval_button" invisible="1"/> <field name="show_approval_button" invisible="1"/>
<field name="show_refuse_button" invisible="1"/> <field name="show_refuse_button" invisible="1"/>
<field name="show_back_button" invisible="1"/> <field name="show_back_button" invisible="1"/>
<field name="has_supervisor_access" invisible="1"/>
</xpath> </xpath>
<xpath expr="//field[@name='stage_id']" position="attributes"> <xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="readonly">show_approval_flow or state in ['1_canceled','04_waiting_normal'] or record_paused</attribute> <attribute name="readonly">show_approval_flow or state in ['1_canceled','04_waiting_normal'] or record_paused</attribute>
@ -76,6 +87,47 @@
</field> </field>
</record> </record>
<record id="view_task_form2_inherited_timesheet" model="ir.ui.view">
<field name="name">project.task.form.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="hr_timesheet.view_task_form2_inherited"/>
<field name="arch" type="xml">
<xpath expr="//label[@for='allocated_hours']" position="before">
<div role="alert" class="alert alert-warning d-flex flex-wrap gap-3"
invisible="not is_suggested_deadline_warning">
<div class="d-flex align-items-center">
<i class="fa fa-exclamation-triangle text-danger me-2" role="img" title="Deadline Issue"/>
Based on the timelines, the deadline can't be met
</div>
<div class="d-flex align-items-center">
Suggested Deadline:
<field name="suggested_deadline" widget="date"/>
<field name="is_suggested_deadline_warning" invisible="1"/>
</div>
</div>
<!-- <div>-->
<!-- <field name="suggested_deadline" invisible="not suggested_deadline or not show_approval_flow or not timelines_requested"/>-->
<!-- <field name="is_suggested_deadline_warning" invisible="1"/>-->
<!-- <label for="suggested_deadline"-->
<!-- class="text-warning"-->
<!-- invisible="not is_suggested_deadline_warning">-->
<!-- ⚠ Based on the timelines, the deadline can't be met-->
<!-- </label>-->
<!-- </div>-->
<field name="estimated_hours" widget="timesheet_uom_no_toggle" readonly="show_approval_flow and timelines_requested"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle"/>
<field name="is_suggested_deadline_warning" />
</xpath>
<xpath expr="//page[@name='page_timesheets']/field[@name='timesheet_ids']/list/field[@name='name']" position="after">
<field name="stage_id" required="0" readonly="readonly_timesheet" options="{'no_create': True,'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
</xpath>
</field>
</record>
<record id="view_task_kanban_inherit" model="ir.ui.view"> <record id="view_task_kanban_inherit" model="ir.ui.view">
<field name="name">project.task.kanban.inherit</field> <field name="name">project.task.kanban.inherit</field>
<field name="model">project.task</field> <field name="model">project.task</field>

View File

@ -0,0 +1,367 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- <record id="project_gantt.project_task_view_gantt" model="ir.ui.view">-->
<!-- <field name="name">project.task.view.gantt.custom</field>-->
<!-- <field name="model">project.task</field>-->
<!-- <field name="priority">20</field>-->
<!-- <field name="arch" type="xml">-->
<!-- <gantt date_start="gantt_date_start"-->
<!-- date_stop="date_deadline"-->
<!-- default_scale="month"-->
<!-- scales="day,week,month,year"-->
<!-- color="gantt_color"-->
<!-- string="Task Planning"-->
<!-- js_class="task_gantt"-->
<!-- display_unavailability="1"-->
<!-- precision="{'day': 'hour:quarter', 'week': 'day:half', 'month': 'day:half'}"-->
<!-- decoration-danger="is_overdue"-->
<!-- decoration-info="priority == '1'"-->
<!-- default_group_by="gantt_user_id"-->
<!-- 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 class="o_gantt_popover_header">-->
<!-- <strong t-esc="name"/>-->
<!-- <span t-if="priority == '1'" class="float-end text-warning">-->
<!-- <i class="fa fa-star"/>-->
<!-- High Priority-->
<!-- </span>-->
<!-- </div>-->
<!-- <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="show_approval_flow and current_stage_performance">-->
<!-- <strong>Performance —</strong>-->
<!-- <t t-if="current_stage_performance == 'good'">-->
<!-- <span class="text-success">-->
<!-- <i class="fa fa-smile-o"></i>-->
<!-- Good (Actual &lt; Estimated)-->
<!-- </span>-->
<!-- </t>-->
<!-- <t t-elif="current_stage_performance == 'normal'">-->
<!-- <span class="text-primary">-->
<!-- <i class="fa fa-meh-o"></i>-->
<!-- Normal (Actual == Estimated)-->
<!-- </span>-->
<!-- </t>-->
<!-- <t t-elif="current_stage_performance == 'bad'">-->
<!-- <span class="text-danger">-->
<!-- <i class="fa fa-frown-o"></i>-->
<!-- Bad (Actual &gt; Estimated)-->
<!-- </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="show_approval_flow">-->
<!-- <strong>Timeline —</strong>-->
<!-- <t t-esc="estimated_hours"/>-->
<!-- hours estimated /-->
<!-- <t t-esc="actual_hours"/>-->
<!-- hours actual-->
<!-- </div>-->
<!-- <div t-if="project_id" name="allocated_hours">-->
<!-- <strong>Allocated Time —</strong>-->
<!-- <t t-esc="allocated_hours"/>-->
<!-- </div>-->
<!-- <div t-if="formatted_start_date and formatted_end_date">-->
<!-- <strong>Dates: </strong>-->
<!-- <t t-esc="formatted_start_date"/>-->
<!-- <i class="fa fa-long-arrow-right" title="Arrow"/>-->
<!-- <t t-esc="formatted_end_date"/>-->
<!-- </div>-->
<!-- <div t-elif="formatted_start_date">-->
<!-- <strong>Start Date: </strong>-->
<!-- <t t-esc="formatted_start_date"/>-->
<!-- </div>-->
<!-- <div t-elif="formatted_end_date">-->
<!-- <strong>Deadline: </strong>-->
<!-- <t t-esc="formatted_end_date"/>-->
<!-- </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>-->
<!-- <div t-name="gantt-pulse">-->
<!-- <div class="o_gantt_pill">-->
<!-- <t t-if="priority == '1'">-->
<!-- <i class="fa fa-star text-warning me-1"/>-->
<!-- </t>-->
<!-- <t t-if="show_approval_flow and performance_icon">-->
<!-- <i t-attf-class="fa {{performance_icon}} me-1"/>-->
<!-- </t>-->
<!-- <t t-esc="name"/>-->
<!-- </div>-->
<!-- </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"/>-->
<!-- <field name="gantt_color"/>-->
<!-- <field name="gantt_user_id"/>-->
<!-- <field name="performance_icon"/>-->
<!-- <field name="show_approval_flow"/>-->
<!-- <field name="priority"/>-->
<!-- <field name="current_stage_performance"/>-->
<!-- <field name="estimated_hours"/>-->
<!-- <field name="actual_hours"/>-->
<!-- <field name="is_overdue"/>-->
<!-- <field name="gantt_date_start"/>-->
<!-- <field name="planned_date_begin"/>-->
<!-- <field name="date_deadline"/>-->
<!-- <field name="formatted_start_date"/>-->
<!-- <field name="formatted_end_date"/>-->
<!-- </gantt>-->
<!-- </field>-->
<!-- </record>-->
<record id="project_gantt.project_task_view_gantt" model="ir.ui.view">
<field name="name">project.task.view.gantt.enhanced</field>
<field name="model">project.task</field>
<field name="priority">30</field>
<field name="arch" type="xml">
<gantt date_start="gantt_date_start"
date_stop="date_deadline"
default_scale="month"
scales="day,week,month,year"
color="gantt_color"
string="Task Planning"
js_class="task_gantt"
display_unavailability="1"
precision="{'day': 'hour:quarter', 'week': 'day:half', 'month': 'day:half'}"
decoration-danger="is_overdue"
decoration-info="task_priority == '1'"
decoration-warning="task_priority == '2'"
decoration-success="is_completed"
default_group_by="gantt_user_id"
progress_bar="progress"
pill_label="True"
total_row="True"
dependency_field="depend_on_ids"
dependency_inverted_field="dependent_ids">
<templates>
<div t-name="gantt-popover">
<div class="o_gantt_popover_header d-flex justify-content-between align-items-center">
<div>
<strong t-esc="name"/>
<span t-if="task_priority == '1'" class="badge bg-warning ms-2">
<i class="fa fa-exclamation"/> High Priority
</span>
<span t-if="task_priority == '2'" class="badge bg-danger ms-2">
<i class="fa fa-fire"/> Urgent
</span>
</div>
<div>
<span t-if="is_completed" class="badge bg-success">
<i class="fa fa-check-circle"/> Completed
</span>
</div>
</div>
<div class="mt-2">
<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="show_approval_flow and current_stage_performance and not is_completed" class="mt-1">
<strong>Performance —</strong>
<t t-if="current_stage_performance == 'good'">
<span class="text-success">
<i class="fa fa-smile-o"></i> Good (Actual &lt; Estimated)
</span>
</t>
<t t-elif="current_stage_performance == 'normal'">
<span class="text-primary">
<i class="fa fa-meh-o"></i> Normal (Actual == Estimated)
</span>
</t>
<t t-elif="current_stage_performance == 'bad'">
<span class="text-danger">
<i class="fa fa-frown-o"></i> Bad (Actual &gt; Estimated)
</span>
</t>
</div>
<div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone" class="mt-1">
<strong>Milestone —</strong> <t t-esc="milestone_id[1]"/>
</div>
<div t-if="user_names" class="mt-1">
<strong>Assignees —</strong> <t t-esc="user_names"/>
</div>
<div t-if="partner_id" class="mt-1">
<strong>Customer —</strong> <t t-esc="partner_id[1]"/>
</div>
<div t-if="show_approval_flow and not is_completed" class="mt-1">
<strong>Timeline —</strong>
<t t-esc="estimated_hours"/> hours estimated /
<t t-esc="actual_hours"/> hours actual
</div>
<div t-if="project_id" name="allocated_hours" class="mt-1">
<strong>Allocated Time —</strong> <t t-esc="allocated_hours"/>
</div>
<div class="mt-2">
<div class="d-flex justify-content-between">
<strong>Progress:</strong>
<span><t t-esc="progress"/>%</span>
</div>
<div class="progress mt-1">
<div class="progress-bar" role="progressbar"
t-attf-style="width: {{progress}}%"
t-att-aria-valuenow="progress"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
</div>
<div t-if="formatted_start_date and formatted_end_date" class="mt-2">
<div class="d-flex justify-content-between">
<div>
<strong>Start:</strong> <t t-esc="formatted_start_date"/>
</div>
<div>
<strong>End:</strong> <t t-esc="formatted_end_date"/>
</div>
</div>
</div>
<div t-elif="formatted_start_date" class="mt-1">
<strong>Start Date: </strong> <t t-esc="formatted_start_date"/>
</div>
<div t-elif="formatted_end_date" class="mt-1">
<strong>Deadline: </strong> <t t-esc="formatted_end_date"/>
</div>
<div t-if="days_remaining != 0 and not is_completed" class="mt-1">
<t t-if="days_remaining > 0">
<span class="text-success">
<i class="fa fa-clock-o"></i> <t t-esc="days_remaining"/> days remaining
</span>
</t>
<t t-else="">
<span class="text-danger">
<i class="fa fa-exclamation-triangle"></i> <t t-esc="abs(days_remaining)"/> days overdue
</span>
</t>
</div>
<div class="text-danger mt-2" t-if="planning_overlap">
<t t-out="planningOverlapHtml"/>
</div>
</div>
<footer class="mt-3">
<button name="action_unschedule_task" type="object" string="Unschedule" class="btn btn-sm btn-secondary"/>
<button name="button_update_assignees" type="object" string="Update Assignees" class="btn btn-sm btn-primary"/>
</footer>
</div>
<div t-name="gantt-pulse">
<div class="o_gantt_pill d-flex align-items-center">
<div class="me-2">
<t t-if="task_priority == '1'">
<i class="fa fa-exclamation text-warning"/>
</t>
<t t-elif="task_priority == '2'">
<i class="fa fa-fire text-danger"/>
</t>
<t t-if="is_completed">
<i class="fa fa-check-circle text-success"/>
</t>
<t t-if="show_approval_flow and performance_icon and not is_completed">
<i t-attf-class="fa {{performance_icon}}"/>
</t>
</div>
<div>
<t t-esc="name"/>
</div>
</div>
</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"/>
<field name="gantt_color"/>
<field name="gantt_user_id"/>
<field name="performance_icon"/>
<field name="show_approval_flow"/>
<field name="task_priority"/>
<field name="current_stage_performance"/>
<field name="estimated_hours"/>
<field name="actual_hours"/>
<field name="is_overdue"/>
<field name="gantt_date_start"/>
<field name="planned_date_begin"/>
<field name="date_deadline"/>
<field name="formatted_start_date"/>
<field name="formatted_end_date"/>
<field name="state"/>
<field name="progress"/>
<field name="is_completed"/>
<field name="days_remaining"/>
</gantt>
</field>
</record>
<!-- <record id="project_gantt.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_custom_gantt_view"/>-->
<!-- </record>-->
</odoo>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_timesheet_line_tree_inherit" model="ir.ui.view">
<field name="name">hr.timesheet.tree.inherit</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="stage_id" required="0" optional="show" readonly="readonly_timesheet"
options="{'no_create': True,'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
</xpath>
</field>
</record>
<record id="hr_timesheet_line_form_inherit" model="ir.ui.view">
<field name="name">hr.timesheet.form.inherit</field>
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='task_id']" position="after">
<field name="stage_id" required="0" optional="show" readonly="readonly_timesheet"
options="{'no_create': True,'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- ===================================================== -->
<!-- 1. list VIEW -->
<!-- ===================================================== -->
<record id="view_user_task_availability_list" model="ir.ui.view">
<field name="name">user.task.availability.list</field>
<field name="model">user.task.availability</field>
<field name="arch" type="xml">
<list string="User Task Availability" create="false" edit="false" delete="false">
<field name="user_id"/>
<field name="task_id"/>
<field name="is_generic"/>
<field name="project_id"/>
<field name="status_label"/>
<field name="work_start_datetime"/>
<field name="work_end_datetime"/>
<field name="estimated_hours"/>
<field name="actual_hours" optional="hide"/>
</list>
</field>
</record>
<!-- ===================================================== -->
<!-- 2. SEARCH FILTERS -->
<!-- ===================================================== -->
<record id="view_user_task_availability_search" model="ir.ui.view">
<field name="name">user.task.availability.search</field>
<field name="model">user.task.availability</field>
<field name="arch" type="xml">
<search string="Search Task Availability">
<field name="task_sequence"/>
<!-- FILTER BY STATE -->
<filter name="state_in_progress"
string="In Progress"
domain="[('state','=','01_in_progress')]"/>
<filter name="state_closed"
string="Closed"
domain="[('state','in',['1_done','1_canceled'])]"/>
<!-- FILTER BY USER -->
<filter name="user_me"
string="My Tasks"
domain="[('user_id','=',uid)]"/>
<filter name="is_generic_task"
string="Generic Tasks"
domain="[('is_generic','=',True)]"/>
<filter name="non_generic_task"
string="Non-Generic Tasks"
domain="[('is_generic','=',False)]"/>
<!-- GROUP BY USER, STATE, PROJECT -->
<group expand="0" string="Group By">
<filter name="group_user" string="User" context="{'group_by':'user_id'}"/>
<filter name="group_project" string="Project" context="{'group_by':'project_id'}"/>
<filter name="group_state" string="State" context="{'group_by':'state'}"/>
</group>
<!-- DATE RANGE -->
<group expand="0" string="Date">
<filter string="Today"
name="today"
domain="[('work_start_datetime','>=',context_today())]"/>
<filter string="This Week"
name="week"
domain="['&amp;',('work_start_datetime','&gt;=',context_today()),('work_start_datetime','&lt;=',(context_today() + datetime.timedelta(days=7)))]"/>
</group>
</search>
</field>
</record>
<!-- ===================================================== -->
<!-- 3. CALENDAR VIEW -->
<!-- ===================================================== -->
<record id="view_user_task_availability_calendar" model="ir.ui.view">
<field name="name">user.task.availability.calendar</field>
<field name="model">user.task.availability</field>
<field name="arch" type="xml">
<calendar string="Task Calendar"
date_start="work_start_datetime"
date_stop="work_end_datetime"
color="user_id"
create="false">
<field name="task_id"/>
<field name="user_id"/>
<field name="project_id"/>
<field name="status_label"/>
</calendar>
</field>
</record>
<!-- ===================================================== -->
<!-- 4. GANTT VIEW -->
<!-- ===================================================== -->
<record id="view_user_task_availability_gantt" model="ir.ui.view">
<field name="name">user.task.availability.gantt</field>
<field name="model">user.task.availability</field>
<field name="arch" type="xml">
<gantt string="User Workload Gantt"
date_start="work_start_datetime"
date_stop="work_end_datetime"
default_scale="week"
scales="day,week,month,year"
color="task_status_color"
default_group_by="user_id"
progress_bar="progress_percentage"
pill_label="True"
total_row="True"
decoration-success="state == '1_done'"
decoration-danger="state == '1_canceled'"
decoration-warning="is_overdue"
decoration-info="is_future"
js_class="task_gantt"
display_unavailability="1"
precision="{'day': 'hour:quarter', 'week': 'day:half', 'month': 'day:half'}">
<templates>
<div t-name="gantt-popover"
style="min-width:300px; max-width:340px; padding:16px 20px 16px 16px; border-radius:6px;">
<!-- HEADER -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<div>
<div style="font-size:16px; font-weight:600;" t-esc="task_name"/>
<t t-if="progress_emoji">
<span class="me-1" t-esc="progress_emoji"/>
</t>
<!-- <div style="font-size:12px; color:#888;" t-esc="task_id[1]"/>-->
</div>
<div>
<span t-if="state == '1_done'"
style="background:#d4edda; color:#155724; padding:2px 6px; border-radius:4px; font-size:11px;">
✔ Done
</span>
<span t-if="is_overdue"
style="background:#f8d7da; color:#721c24; padding:2px 6px; border-radius:4px; font-size:11px;">
⚠ Overdue
</span>
<span t-if="is_future"
style="background:#d1ecf1; color:#0c5460; padding:2px 6px; border-radius:4px; font-size:11px;">
⏱ Scheduled
</span>
</div>
</div>
<!-- GRID (NO BOOTSTRAP) -->
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; font-size:13px;">
<div>
<div style="color:#888;">Assigned To</div>
<div style="font-weight:600;" t-esc="user_id[1]"/>
</div>
<div>
<div style="color:#888;">Project</div>
<div style="font-weight:600;" t-esc="project_id[1]"/>
</div>
<div>
<div style="color:#888;">Stage</div>
<div style="font-weight:600;" t-esc="stage_id[1]"/>
</div>
<div>
<div style="color:#888;">Status</div>
<div style="font-weight:600;" t-esc="status_label"/>
</div>
</div>
<div style="border-top:1px solid #eee; margin:14px 0;"></div>
<!-- TIME SECTION -->
<div style="display:flex; justify-content:space-between; font-size:13px;">
<div>
<div style="color:#888; margin-bottom:2px;">📅 Start</div>
<div style="font-weight:600;"
t-esc="work_start_datetime.toFormat('MMM dd, yyyy HH:mm')"/>
</div>
<div style="text-align:right;">
<div style="color:#888; margin-bottom:2px;">🏁 End</div>
<div style="font-weight:600;" t-esc="work_end_datetime.toFormat('MMM dd, yyyy HH:mm')"/>
</div>
</div>
<div style="border-top:1px solid #eee; margin:14px 0;"></div>
<!-- PROGRESS -->
<div>
<div style="display:flex; justify-content:space-between; font-size:13px; margin-bottom:5px;">
<span style="color:#888;">Progress</span>
<span style="font-weight:600;"><t t-esc="progress_percentage"/>%
</span>
</div>
<!-- <div style="background:#eee; border-radius:4px; height:6px; width:100%; margin-top:4px;">-->
<!-- <div style="background:#4c8bf5; height:6px; border-radius:4px;"-->
<!-- t-att-style="'width: %s%% !important' % progress_percentage">-->
<!-- </div>-->
<!-- </div>-->
</div>
<div style="border-top:1px solid #eee; margin:14px 0;"></div>
<!-- FOOTER -->
<div style="display:flex; justify-content:space-between; align-items:center; font-size:13px;">
<div>
<span style="color:#888;">Source:</span>
<span style="background:#f1f1f1; padding:2px 6px; border-radius:4px;"
t-esc="source_type"/>
</div>
</div>
</div>
<div t-name="gantt-pulse">
<div class="o_gantt_pill d-flex align-items-center shadow-sm">
<div class="me-2 d-flex align-items-center">
<t t-if="progress_emoji">
<span class="me-1" t-esc="progress_emoji"/>
</t>
<t t-if="state == '1_done'">
<i class="fa fa-check-circle text-success me-1"/>
</t>
<t t-if="is_overdue">
<i class="fa fa-exclamation-triangle text-danger me-1"/>
</t>
<t t-if="is_future">
<i class="fa fa-clock-o text-info me-1"/>
</t>
</div>
<div class="flex-grow-1">
<div class="fw-bold" t-esc="task_name"/>
<div class="small text-muted">
<span t-esc="estimated_hours" widget="timesheet_uom_no_toggle"/>h /
<span t-esc="actual_hours" widget="timesheet_uom_no_toggle"/>h
</div>
</div>
<div class="ms-2">
<div class="progress" style="height: 5px; width: 60px;">
<div class="progress-bar" role="progressbar"
t-attf-style="width: {{progress_percentage}}%"
t-att-aria-valuenow="progress_percentage"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
</div>
</div>
</div>
</templates>
<field name="task_id"/>
<field name="user_id"/>
<field name="project_id"/>
<field name="stage_id"/>
<field name="task_name"/>
<field name="status_label"/>
<field name="work_start_datetime"/>
<field name="work_end_datetime"/>
<field name="estimated_hours"/>
<field name="actual_hours"/>
<field name="progress_percentage"/>
<field name="progress_emoji"/>
<field name="task_status_color"/>
<field name="state"/>
<field name="is_overdue"/>
<field name="is_future"/>
<field name="source_type"/>
</gantt>
</field>
</record>
<!-- ===================================================== -->
<!-- 5. KANBAN VIEW -->
<!-- “Who is Working Now?” -->
<!-- ===================================================== -->
<record id="view_user_task_availability_kanban" model="ir.ui.view">
<field name="name">user.task.availability.kanban</field>
<field name="model">user.task.availability</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" create="false" edit="false">
<field name="user_id"/>
<field name="task_id"/>
<field name="project_id"/>
<field name="status_label"/>
<field name="work_start_datetime"/>
<field name="work_end_datetime"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card">
<div class="o_kanban_primary_left">
<strong>
<field name="user_id"/>
</strong>
<div>
<field name="task_id"/>
</div>
<div>
<span>Status:</span>
<field name="status_label"/>
</div>
<div>
<span>From:</span>
<field name="work_start_datetime"/>
</div>
<div>
<span>To:</span>
<field name="work_end_datetime"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- ===================================================== -->
<!-- 6. ACTION & MENU -->
<!-- ===================================================== -->
<record id="action_user_task_availability" model="ir.actions.act_window">
<field name="name">User Task Availability</field>
<field name="res_model">user.task.availability</field>
<field name="view_mode">list,kanban,calendar,gantt</field>
</record>
<menuitem
name="User Task Availability"
id="menu_user_task_availability"
action="action_user_task_availability"
parent="project.menu_project_report"
sequence="10"
/>
</odoo>