new timeline feature and bug fixes

This commit is contained in:
pranaysaidurga 2026-05-06 17:26:15 +05:30
parent 723dcbe225
commit a9a6c683d7
28 changed files with 1506 additions and 36 deletions

View File

@ -27,6 +27,7 @@ Key Features:
'project_gantt',
'hr',
'hr_contract',
'user_timelines',
],
'data': [
'security/security.xml',
@ -54,6 +55,7 @@ Key Features:
'view/timesheets.xml',
'view/pro_task_gantt.xml',
'view/user_availability.xml',
'view/user_timelines_project.xml',
'view/project_task_view.xml',
# 'view/project_task_gantt.xml',
'view/stage_approval_wizard.xml',

View File

@ -1,3 +1,4 @@
from . import project_task_module
from . import teams
from . import project_roles_master
from . import project_attachments
@ -16,11 +17,14 @@ from . import project_closer
from . import project
from . import project_actual_costing
from . import project_portfolio
from . import project_portfolio_timeline
from . import project_portfolio_dashboard
from . import project_task
from . import timesheets
# from . import project_task_gantt
from . import project_team_timeline
from . import user_availability
from . import user_timeline_entry
from . import stage_visibility
from . import account_analytic_line
from . import stage_approval_wizard
from . import stage_approval_wizard

View File

@ -0,0 +1,106 @@
from odoo import _, api, fields, models
class ProjectPortfolio(models.Model):
_inherit = "project.portfolio"
portfolio_member_ids = fields.Many2many(
"res.users",
compute="_compute_portfolio_timeline_data",
string="Portfolio Team Members",
readonly=True,
)
portfolio_timeline_entry_ids = fields.Many2many(
"user.timeline.entry",
compute="_compute_portfolio_timeline_data",
string="Portfolio Timeline Entries",
readonly=True,
)
portfolio_member_count = fields.Integer(
compute="_compute_portfolio_timeline_data",
string="Team Members",
)
portfolio_timeline_entry_count = fields.Integer(
compute="_compute_portfolio_timeline_data",
string="Timeline Entries",
)
portfolio_timeline_task_count = fields.Integer(
compute="_compute_portfolio_timeline_data",
string="Task Blocks",
)
portfolio_timeline_leave_count = fields.Integer(
compute="_compute_portfolio_timeline_data",
string="Leave Blocks",
)
portfolio_timeline_public_holidays_count = fields.Integer(
compute="_compute_portfolio_timeline_data",
string="Leave Blocks",
)
@api.depends(
"project_ids",
"project_ids.members_ids",
"project_ids.user_id",
"project_ids.project_lead",
)
def _compute_portfolio_timeline_data(self):
Timeline = self.env["user.timeline.entry"]
for portfolio in self:
projects = portfolio.project_ids.filtered("active")
members = projects.mapped("members_ids") | projects.mapped("user_id") | projects.mapped("project_lead")
member_ids = members.ids
portfolio.portfolio_member_ids = members
portfolio.portfolio_member_count = len(members)
if not projects:
portfolio.portfolio_timeline_entry_ids = Timeline
portfolio.portfolio_timeline_entry_count = 0
portfolio.portfolio_timeline_task_count = 0
portfolio.portfolio_timeline_leave_count = 0
portfolio.portfolio_timeline_public_holidays_count = 0
continue
entries = Timeline.search(
[
"|",
("project_id", "in", projects.ids),
"&",
("entry_type", "=", "leave"),
("user_id", "in", member_ids),
],
order="date_start desc",
)
portfolio.portfolio_timeline_entry_ids = entries
portfolio.portfolio_timeline_entry_count = len(entries)
portfolio.portfolio_timeline_task_count = len(
entries.filtered(lambda entry: entry.entry_type == "task")
)
portfolio.portfolio_timeline_leave_count = len(
entries.filtered(lambda entry: entry.entry_type == "leave" and entry.source_label != 'Public Holiday')
)
portfolio.portfolio_timeline_public_holidays_count = len(
entries.filtered(lambda entry: entry.entry_type == "leave" and entry.source_label == 'Public Holiday')
)
def action_open_portfolio_timelines(self):
self.ensure_one()
projects = self.project_ids.filtered("active")
members = self.portfolio_member_ids
action = self.env["ir.actions.act_window"]._for_xml_id(
"user_timelines.action_user_timeline_entries"
)
action["name"] = _("Portfolio Timelines: %s", self.name)
action["domain"] = [
"|",
("project_id", "in", projects.ids),
"&",
("entry_type", "=", "leave"),
("user_id", "in", members.ids),
]
action["context"] = {
"search_default_group_employee": 1,
"search_default_group_user": 0,
"search_default_group_project": 0,
"search_default_public_holidays_remove": 1,
"default_is_public_holiday": 0,
"default_portfolio_id": self.id,
}
return action

View File

@ -33,6 +33,8 @@ class projectTask(models.Model):
('normal', 'Normal'),
], compute='_compute_deadline_status')
model_id = fields.Many2one('project.module.source')
@api.depends('date_deadline')
def _compute_deadline_status(self):
today = fields.datetime.today()
@ -227,7 +229,10 @@ class projectTask(models.Model):
@api.onchange('user_ids')
def _onchange_user_ids(self):
if self.project_id and (self.project_id.user_id or self.project_id.project_lead):
if (self.project_id.user_id.id != self.env.user.id) and (self.project_id.project_lead.id != self.env.user.id):
administrative_users = self.env['project.role'].search([('role_level','=','administrative')])
first_stage = self.project_id.type_ids.sorted(key=lambda r: r.sequence)[0]
create_access_users = first_stage.team_id.team_lead + first_stage.involved_user_ids + administrative_users.user_ids
if (self.project_id.user_id.id != self.env.user.id) and (self.project_id.project_lead.id != self.env.user.id) and self.env.user.id not in list(set(create_access_users.ids)):
raise ValidationError(
"Only Project Manager/Lead can assign/remove assignees"
)
@ -460,7 +465,11 @@ class projectTask(models.Model):
for task in self:
current_user = self.env.user
task.has_supervisor_access = False
if current_user.has_group("project.group_project_manager") or current_user == task.project_id.user_id or current_user == task.project_id.project_lead:
administrative_users = self.env['project.role'].search([('role_level', '=', 'administrative')])
first_stage = self.project_id.type_ids.sorted(key=lambda r: r.sequence)[0]
create_access_users = first_stage.team_id.team_lead + first_stage.involved_user_ids + administrative_users.user_ids
if current_user.has_group("project.group_project_manager") or current_user == task.project_id.user_id or current_user == task.project_id.project_lead or (current_user.id in list(set(create_access_users.ids)) and task.stage_id.id == first_stage.id):
task.has_supervisor_access = True
@api.depends('assignees_timelines.estimated_time', 'show_approval_flow')

View File

@ -0,0 +1,8 @@
from odoo import _, api, fields, models
class ProjectModuleSource(models.Model):
_name = "project.module.source"
_description = "Project Source"
name = fields.Char(required=True)

View File

@ -0,0 +1,88 @@
from odoo import _, api, fields, models
class ProjectProject(models.Model):
_inherit = "project.project"
team_timeline_entry_ids = fields.Many2many(
"user.timeline.entry",
compute="_compute_team_timeline_entries",
string="Team Timeline Entries",
readonly=True,
)
team_timeline_entry_count = fields.Integer(
compute="_compute_team_timeline_entries",
string="Timeline Entries",
)
team_timeline_member_count = fields.Integer(
compute="_compute_team_timeline_entries",
string="Team Members",
)
team_timeline_leave_count = fields.Integer(
compute="_compute_team_timeline_entries",
string="Leave Blocks",
)
team_timeline_public_holidays_count = fields.Integer(
compute="_compute_team_timeline_entries",
string="Leave Blocks",
)
team_timeline_task_count = fields.Integer(
compute="_compute_team_timeline_entries",
string="Task Blocks",
)
@api.depends("members_ids", "user_id", "project_lead")
def _compute_team_timeline_entries(self):
Timeline = self.env["user.timeline.entry"]
for project in self:
users = project.members_ids | project.user_id | project.project_lead
user_ids = users.ids
if not user_ids:
project.team_timeline_entry_ids = Timeline
project.team_timeline_entry_count = 0
project.team_timeline_member_count = 0
project.team_timeline_leave_count = 0
project.team_timeline_public_holidays_count = 0
project.team_timeline_task_count = 0
continue
entries = Timeline.search(
[
"|",
("project_id", "=", project.id),
"&",'&',
("entry_type", "=", "leave"),
("is_public_holiday","=", False),
("user_id", "in", user_ids),
],
order="date_start desc",
)
project.team_timeline_entry_ids = entries
project.team_timeline_entry_count = len(entries)
project.team_timeline_member_count = len(entries.mapped("user_id"))
project.team_timeline_leave_count = len(entries.filtered(lambda entry: entry.entry_type == "leave" and entry.source_label != 'Public Holiday'))
project.team_timeline_public_holidays_count = len(entries.filtered(lambda entry: entry.entry_type == "leave" and entry.source_label == 'Public Holiday'))
project.team_timeline_task_count = len(entries.filtered(lambda entry: entry.entry_type == "task"))
def action_open_team_timelines(self):
self.ensure_one()
users = self.members_ids | self.user_id | self.project_lead
action = self.env["ir.actions.act_window"]._for_xml_id(
"user_timelines.action_user_timeline_entries"
)
action["name"] = _("Team Timelines: %s", self.name)
action["domain"] = [
"|",
("project_id", "=", self.id),
"&",
("entry_type", "=", "leave"),
("user_id", "in", users.ids),
]
action["context"] = {
"search_default_group_employee": 1,
"search_default_group_user": 0,
"search_default_group_project": 0,
"search_default_public_holidays_remove": 1,
"default_is_public_holiday": 0,
"default_project_id": self.id,
}
return action

View File

@ -0,0 +1,99 @@
from odoo import models
class UserTimelineEntry(models.Model):
_inherit = "user.timeline.entry"
def _get_normal_task_select_sql(self):
project_color_case = self._color_case_sql("COALESCE(project.color, 0)")
return f"""
SELECT
CONCAT('timeline-', timeline.id::varchar) AS source_key,
CONCAT(COALESCE(task.sequence_name, task.name), ' - ', stage.name) AS name,
employee.id AS employee_id,
timeline.assigned_to AS user_id,
task.project_id AS project_id,
task.id AS task_id,
timeline.id AS timeline_id,
timeline.stage_id AS stage_id,
NULL::integer AS leave_id,
NULL::integer AS leave_type_id,
timeline.estimated_start_datetime AS date_start,
timeline.estimated_end_datetime AS date_stop,
'task'::varchar AS entry_type,
COALESCE(project.color, 0) AS project_color,
NULL::integer AS leave_color,
COALESCE(project.color, 0) AS display_color,
COALESCE(NULLIF(project.timeline_color_hex, ''), {project_color_case}) AS display_color_hex,
task.name::varchar AS description,
'Project Timeline'::varchar AS source_label,
task.state::varchar AS state,
FALSE AS is_public_holiday,
COALESCE(employee.name, assigned_partner.name, task.name)::varchar AS focus_label
FROM project_task_time_lines timeline
JOIN project_task task
ON task.id = timeline.task_id
LEFT JOIN project_project project
ON project.id = task.project_id
LEFT JOIN project_task_type stage
ON stage.id = timeline.stage_id
LEFT JOIN hr_employee employee
ON employee.user_id = timeline.assigned_to
LEFT JOIN res_users assigned_user
ON assigned_user.id = timeline.assigned_to
LEFT JOIN res_partner assigned_partner
ON assigned_partner.id = assigned_user.partner_id
WHERE timeline.assigned_to IS NOT NULL
AND timeline.estimated_start_datetime IS NOT NULL
AND timeline.estimated_end_datetime IS NOT NULL
UNION ALL
SELECT
CONCAT('task-', task.id::varchar, '-', rel.user_id::varchar) AS source_key,
COALESCE(task.sequence_name, task.name) AS name,
employee.id AS employee_id,
rel.user_id AS user_id,
task.project_id AS project_id,
task.id AS task_id,
NULL::integer AS timeline_id,
task.stage_id AS stage_id,
NULL::integer AS leave_id,
NULL::integer AS leave_type_id,
COALESCE(task.date_assign, task.create_date) AS date_start,
GREATEST(
COALESCE(task.date_deadline, task.date_assign, task.create_date),
COALESCE(task.date_assign, task.create_date)
) AS date_stop,
'task'::varchar AS entry_type,
COALESCE(project.color, 0) AS project_color,
NULL::integer AS leave_color,
COALESCE(project.color, 0) AS display_color,
COALESCE(NULLIF(project.timeline_color_hex, ''), {project_color_case}) AS display_color_hex,
task.name::varchar AS description,
'Project Task'::varchar AS source_label,
task.state::varchar AS state,
FALSE AS is_public_holiday,
COALESCE(employee.name, user_partner.name, task.name)::varchar AS focus_label
FROM project_task task
JOIN project_task_user_rel rel
ON rel.task_id = task.id
LEFT JOIN project_project project
ON project.id = task.project_id
LEFT JOIN hr_employee employee
ON employee.user_id = rel.user_id
LEFT JOIN res_users users
ON users.id = rel.user_id
LEFT JOIN res_partner user_partner
ON user_partner.id = users.partner_id
WHERE rel.user_id IS NOT NULL
AND COALESCE(task.date_assign, task.create_date) IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM project_task_time_lines timeline
WHERE timeline.task_id = task.id
AND timeline.assigned_to = rel.user_id
AND timeline.estimated_start_datetime IS NOT NULL
AND timeline.estimated_end_datetime IS NOT NULL
)
"""

View File

@ -2,7 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_project_manager,1,1,1,1
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
project_module_source_user,project.module.source.user,model_project_module_source,,1,1,1,1
access_project_portfolio_employee_performance_user,project.portfolio.employee.performance.user,model_project_portfolio_employee_performance,base.group_user,1,1,1,1
access_project_portfolio_dashboard,project.portfolio.dashboard,model_project_portfolio_dashboard,base.group_user,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 internal_teams_admin internal.teams.admin model_internal_teams project.group_project_manager 1 1 1 1
3 internal_teams_manager internal.teams.manager model_internal_teams project.group_project_user 1 1 1 0
4 internal_teams_user internal.teams.user model_internal_teams base.group_user 1 0 0 0
5 access_project_portfolio_employee_performance_user project_module_source_user project.portfolio.employee.performance.user project.module.source.user model_project_portfolio_employee_performance model_project_module_source base.group_user 1 1 1 1
6 access_project_portfolio_dashboard access_project_portfolio_employee_performance_user project.portfolio.dashboard project.portfolio.employee.performance.user model_project_portfolio_dashboard model_project_portfolio_employee_performance base.group_user 1 0 1 0 1 0 1
7 access_project_portfolio_dashboard_manager access_project_portfolio_dashboard project.portfolio.dashboard model_project_portfolio_dashboard project.group_project_manager base.group_user 1 0 0 0
8 access_project_portfolio_budget_overview access_project_portfolio_dashboard_manager project.portfolio.budget.overview project.portfolio.dashboard model_project_portfolio_budget_overview model_project_portfolio_dashboard base.group_user project.group_project_manager 1 0 0 0

View File

@ -18,10 +18,10 @@
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="project_lead" widget="many2one_avatar_user"/>
<field name="project_lead" widget="user_timeline"/>
</xpath>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="user_id" widget="many2one_avatar_user"/>
<field name="user_id" widget="user_timeline"/>
</xpath>
<xpath expr="//page[@name='settings']" position="inside">
<group>
@ -113,21 +113,21 @@
<field name="activate"/>
</list>
</field>
</page>
<page name="task_stages" string="Task Stages" invisible="not is_project_editor">
<field name="type_ids" context="{'default_project_id': id, 'project_stage_project_id': id}">
<list editable="bottom" open_form_view="True" delete="0">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="team_id" string="Assigned Team"/>
<field name="approval_by" string="Approval Owner"/>
<field name="fold"/>
<field name="team_related_user_ids" invisible="1" column_invisible="1"/>
<field name="involved_user_ids" widget="many2many_tags" domain="[('id', 'in', team_related_user_ids)]"/>
<field name="is_workflow_template" invisible="1" column_invisible="1"/>
</list>
</field>
</page>
</page>
<page name="task_stages" string="Task Stages" invisible="not is_project_editor">
<field name="type_ids" context="{'default_project_id': id, 'project_stage_project_id': id}">
<list editable="bottom" open_form_view="True" delete="0">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="team_id" string="Assigned Team"/>
<field name="approval_by" string="Approval Owner"/>
<field name="fold"/>
<field name="team_related_user_ids" invisible="1" column_invisible="1"/>
<field name="involved_user_ids" widget="many2many_tags" domain="[('id', 'in', team_related_user_ids)]"/>
<field name="is_workflow_template" invisible="1" column_invisible="1"/>
</list>
</field>
</page>
<page string="Team" name="team" invisible="not is_project_editor">
<group>
<button name="fetch_project_task_stage_users" type="object" string="Fetch Users from Related Stages" class="btn-primary"/>
@ -884,15 +884,15 @@
</field>
</record>
<!-- <record id="project_view_kanban_inherit" model="ir.ui.view">-->
<!-- <field name="name">project.view.kanban.inherit</field>-->
<!-- <field name="model">project.project</field>-->
<!-- <field name="inherit_id" ref="project.view_project_kanban"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//kanban" position="attributes">-->
<!-- <attribute name="context">{'view_type':'kanban'}</attribute>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
</odoo>
<!-- <record id="project_view_kanban_inherit" model="ir.ui.view">-->
<!-- <field name="name">project.view.kanban.inherit</field>-->
<!-- <field name="model">project.project</field>-->
<!-- <field name="inherit_id" ref="project.view_project_kanban"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//kanban" position="attributes">-->
<!-- <attribute name="context">{'view_type':'kanban'}</attribute>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
</odoo>

View File

@ -59,6 +59,12 @@
<span class="o_stat_text">Refresh Performance</span>
</div>
</button>
<button name="action_open_portfolio_timelines" type="object"
class="oe_stat_button" icon="fa-calendar" invisible="project_count == 0">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Portfolio Timelines</span>
</div>
</button>
</div>
<div class="oe_title">
@ -144,7 +150,7 @@
<list>
<field name="name"/>
<field name="partner_id"/>
<field name="user_id"/>
<field name="user_id" widget="user_timeline"/>
<field name="stage_id"/>
<field name="estimated_amount" widget="monetary"/>
<field name="project_cost" widget="monetary"/>
@ -198,6 +204,94 @@
</field>
</page>
<page string="Team Timelines" invisible="project_count == 0">
<field name="portfolio_member_count" invisible="1"/>
<field name="portfolio_timeline_entry_count" invisible="1"/>
<field name="portfolio_timeline_task_count" invisible="1"/>
<field name="portfolio_timeline_leave_count" invisible="1"/>
<field name="portfolio_timeline_public_holidays_count" invisible="1"/>
<div class="o_user_timeline_project_panel">
<div class="o_user_timeline_project_hero">
<div class="o_user_timeline_project_heading">
<div class="o_user_timeline_project_kicker">Portfolio Timeline</div>
<h2>
<field name="name" readonly="1"/>
</h2>
<p>
Combined visibility of all people working across the projects in this portfolio,
together with their task timelines and leave context for better planning.
</p>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="portfolio_member_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Team Members</span>
</div>
</div>
<div class="o_user_timeline_project_metrics">
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="portfolio_timeline_entry_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Total Blocks</span>
</div>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="portfolio_timeline_task_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Task Blocks</span>
</div>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="portfolio_timeline_leave_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Leave Blocks</span>
</div>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="portfolio_timeline_public_holidays_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Public Holidays</span>
</div>
</div>
</div>
<div class="o_user_timeline_project_toolbar">
<button name="action_open_portfolio_timelines"
type="object"
string="Open Full Portfolio Timeline"
class="btn btn-primary"/>
<span class="text-muted">
Review all people involved in this portfolio and the timelines linked to its projects.
</span>
</div>
<group string="Portfolio Team">
<field name="portfolio_member_ids"
widget="many2many_avatar_user"
readonly="1"
nolabel="1"
options="{'no_create': True, 'no_create_edit': True, 'no_open': False, 'no_quick_create': True}"/>
</group>
<group string="Timeline Preview">
<field name="portfolio_timeline_entry_ids" readonly="1" domain="[]">
<list create="false" edit="false" delete="false" class="o_user_timeline_preview_list">
<field name="name"/>
<field name="project_id"/>
<field name="focus_label" string="For"/>
<field name="entry_type"/>
<field name="leave_type_id" optional="show"/>
<field name="date_start"/>
<field name="date_stop"/>
<field name="source_label"/>
</list>
</field>
</group>
</div>
</page>
<page string="Description">
<field name="description"/>
</page>
@ -525,4 +619,4 @@
<!-- (0, 0, {'view_mode': 'pivot'})-->
<!-- ]"/>-->
</record>
</odoo>
</odoo>

View File

@ -58,6 +58,7 @@
<xpath expr="//field[@name='user_ids']" position="after">
<field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/>
<field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/>
</xpath>
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>-->

View File

@ -9,7 +9,7 @@
<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="user_id" widget="user_timeline"/>
<field name="task_id"/>
<field name="is_generic"/>
<field name="project_id"/>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="project_project_form_team_timelines" model="ir.ui.view">
<field name="name">project.project.form.team.timelines</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project_task_timesheet_extended.project_project_inherit_form_view2"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='team']" position="after">
<page string="Team Timelines" name="team_timelines" invisible="not is_project_editor">
<field name="team_timeline_entry_count" invisible="1"/>
<field name="team_timeline_member_count" invisible="1"/>
<field name="team_timeline_leave_count" invisible="1"/>
<field name="team_timeline_public_holidays_count" invisible="1"/>
<field name="team_timeline_task_count" invisible="1"/>
<div class="o_user_timeline_project_panel">
<div class="o_user_timeline_project_hero">
<div class="o_user_timeline_project_heading">
<div class="o_user_timeline_project_kicker">Project Timeline</div>
<h2>
<field name="name" readonly="1"/>
</h2>
<div class="o_user_timeline_project_color_row">
<span class="o_user_timeline_project_color_label">Accent</span>
<field name="timeline_color_hex" widget="color" readonly="1" class="w-auto"/>
<field name="timeline_color_hex" readonly="1" class="oe_inline"/>
</div>
<p>
A polished team timeline focused on this project only, combining assigned work,
detailed stage timelines, and leave overlays for quick planning visibility.
</p>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="team_timeline_member_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Team Members</span>
</div>
</div>
<div class="o_user_timeline_project_metrics">
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="team_timeline_entry_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Total Blocks</span>
</div>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="team_timeline_task_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Task Blocks</span>
</div>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="team_timeline_leave_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Leave Blocks</span>
</div>
<div class="o_user_timeline_metric_card">
<span class="o_user_timeline_metric_value">
<field name="team_timeline_public_holidays_count" readonly="1"/>
</span>
<span class="o_user_timeline_metric_label">Public Holidays</span>
</div>
</div>
</div>
<div class="o_user_timeline_project_toolbar">
<button name="action_open_team_timelines"
type="object"
string="Open Full Timeline"
class="btn btn-primary"/>
<span class="text-muted">
Use the full timeline to switch between gantt, calendar, and list views.
</span>
</div>
<group string="Timeline Preview">
<field name="team_timeline_entry_ids"
readonly="1"
context="{'default_project_id': id}"
domain="[]">
<list create="false" edit="false" delete="false" class="o_user_timeline_preview_list">
<field name="name"/>
<field name="focus_label" string="For"/>
<field name="entry_type"/>
<field name="leave_type_id" optional="show"/>
<field name="date_start"/>
<field name="date_stop"/>
<field name="source_label"/>
</list>
</field>
</group>
</div>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
{
"name": "User Timelines",
"version": "18.0.1.0.0",
"category": "Human Resources",
"summary": "Employee and user timeline views powered by project timelines and leaves",
"author": "OpenAI",
"license": "LGPL-3",
"depends": [
"hr",
"hr_holidays",
"project",
"project_gantt",
"web",
],
"data": [
"security/ir.model.access.csv",
"views/user_timeline_views.xml",
"views/hr_employee_views.xml",
"views/res_users_views.xml",
"views/project_views.xml",
],
"assets": {
"web.assets_backend": [
"user_timelines/static/src/js/user_timeline_field.js",
"user_timelines/static/src/xml/user_timeline_field.xml",
"user_timelines/static/src/scss/user_timeline.scss",
],
},
"installable": True,
"application": False,
}

View File

@ -0,0 +1,4 @@
from . import hr_employee
from . import project_project
from . import res_users
from . import user_timeline_entry

View File

@ -0,0 +1,22 @@
from odoo import _, models
class HrEmployee(models.Model):
_inherit = "hr.employee"
def action_open_timeline(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"user_timelines.action_user_timeline_entries"
)
action["name"] = _("Timeline: %s", self.name)
action["domain"] = [("employee_id", "=", self.id)]
action["context"] = {
"search_default_group_employee": 0,
"search_default_group_user": 0,
"search_default_group_project": 0,
"search_default_public_holidays_remove": 1,
"default_is_public_holiday": 0,
"timeline_focus_employee_id": self.id,
}
return action

View File

@ -0,0 +1,65 @@
from odoo import _, api, fields, models
class ProjectProject(models.Model):
_inherit = "project.project"
timeline_color_hex = fields.Char(
string="Timeline Color",
default="#5794dd",
help="Custom color used for this project's timeline bars.",
)
timeline_entry_ids = fields.One2many(
"user.timeline.entry",
"project_id",
string="Timeline Entries",
readonly=True,
)
timeline_entry_count = fields.Integer(
compute="_compute_timeline_metrics",
string="Timeline Entries",
)
timeline_member_count = fields.Integer(
compute="_compute_timeline_metrics",
string="Team Members in Timeline",
)
timeline_leave_count = fields.Integer(
compute="_compute_timeline_metrics",
string="Leave Blocks",
)
timeline_task_count = fields.Integer(
compute="_compute_timeline_metrics",
string="Task Blocks",
)
@api.depends("timeline_entry_ids")
def _compute_timeline_metrics(self):
grouped = {}
if self.ids:
entries = self.env["user.timeline.entry"].search([("project_id", "in", self.ids)])
for project in self:
project_entries = entries.filtered(lambda entry: entry.project_id.id == project.id)
grouped[project.id] = project_entries
for project in self:
project_entries = grouped.get(project.id, self.env["user.timeline.entry"])
project.timeline_entry_count = len(project_entries)
project.timeline_member_count = len(project_entries.mapped("user_id"))
project.timeline_leave_count = len(project_entries.filtered(lambda entry: entry.entry_type == "leave"))
project.timeline_task_count = len(project_entries.filtered(lambda entry: entry.entry_type == "task"))
def action_open_team_timelines(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"user_timelines.action_user_timeline_entries"
)
action["name"] = _("Team Timelines: %s", self.name)
action["domain"] = [("project_id", "=", self.id)]
action["context"] = {
"search_default_group_employee": 1,
"search_default_group_user": 0,
"search_default_group_project": 0,
"search_default_public_holidays_remove": 1,
"default_is_public_holiday": 0,
"default_project_id": self.id,
}
return action

View File

@ -0,0 +1,22 @@
from odoo import _, models
class ResUsers(models.Model):
_inherit = "res.users"
def action_open_timeline(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"user_timelines.action_user_timeline_entries"
)
action["name"] = _("Timeline: %s", self.name)
action["domain"] = [("user_id", "=", self.id)]
action["context"] = {
"search_default_group_employee": 0,
"search_default_group_user": 0,
"search_default_group_project": 0,
"search_default_public_holidays_remove": 1,
"default_is_public_holiday": 0,
"timeline_focus_user_id": self.id,
}
return action

View File

@ -0,0 +1,227 @@
from odoo import fields, models, tools
ODOO_COLOR_MAP = {
0: "#a2a2a2",
1: "#ee2d2d",
2: "#dc8534",
3: "#e8bb1d",
4: "#5794dd",
5: "#9f628f",
6: "#db8865",
7: "#41a9a2",
8: "#304be0",
9: "#ee2f8a",
10: "#61c36e",
11: "#9872e6",
}
class UserTimelineEntry(models.Model):
_name = "user.timeline.entry"
_description = "User Timeline Entry"
_auto = False
_order = "date_start, employee_id, user_id, id"
_rec_name = "name"
name = fields.Char(readonly=True)
employee_id = fields.Many2one("hr.employee", readonly=True)
user_id = fields.Many2one("res.users", readonly=True)
project_id = fields.Many2one("project.project", readonly=True)
task_id = fields.Many2one("project.task", readonly=True)
timeline_id = fields.Integer(readonly=True)
stage_id = fields.Many2one("project.task.type", readonly=True)
leave_id = fields.Many2one("hr.leave", readonly=True)
leave_type_id = fields.Many2one("hr.leave.type", readonly=True)
date_start = fields.Datetime(string="Start", readonly=True)
date_stop = fields.Datetime(string="End", readonly=True)
entry_type = fields.Selection(
[("task", "Task"), ("leave", "Leave")],
string="Timeline Type",
readonly=True,
)
project_color = fields.Integer(readonly=True)
leave_color = fields.Integer(readonly=True)
display_color = fields.Integer(readonly=True)
display_color_hex = fields.Char(readonly=True)
description = fields.Char(readonly=True)
source_label = fields.Char(readonly=True)
state = fields.Char(readonly=True)
is_public_holiday = fields.Boolean(readonly=True)
focus_label = fields.Char(readonly=True)
def _color_case_sql(self, field_name):
return "CASE {field} {cases} ELSE '{default}' END".format(
field=field_name,
cases=" ".join(
f"WHEN {index} THEN '{color}'" for index, color in ODOO_COLOR_MAP.items()
),
default=ODOO_COLOR_MAP[4],
)
def _get_normal_task_select_sql(self):
project_color_case = self._color_case_sql("COALESCE(project.color, 0)")
return f"""
SELECT
CONCAT('task-', task.id::varchar, '-', rel.user_id::varchar) AS source_key,
COALESCE(task.sequence_name, task.name) AS name,
employee.id AS employee_id,
rel.user_id AS user_id,
task.project_id AS project_id,
task.id AS task_id,
NULL::integer AS timeline_id,
task.stage_id AS stage_id,
NULL::integer AS leave_id,
NULL::integer AS leave_type_id,
COALESCE(task.date_assign, task.create_date) AS date_start,
GREATEST(
COALESCE(task.date_deadline, task.date_assign, task.create_date),
COALESCE(task.date_assign, task.create_date)
) AS date_stop,
'task'::varchar AS entry_type,
COALESCE(project.color, 0) AS project_color,
NULL::integer AS leave_color,
COALESCE(project.color, 0) AS display_color,
COALESCE(NULLIF(project.timeline_color_hex, ''), {project_color_case}) AS display_color_hex,
task.name::varchar AS description,
'Project Task'::varchar AS source_label,
task.state::varchar AS state,
FALSE AS is_public_holiday,
COALESCE(employee.name, user_partner.name, task.name)::varchar AS focus_label
FROM project_task task
JOIN project_task_user_rel rel
ON rel.task_id = task.id
LEFT JOIN project_project project
ON project.id = task.project_id
LEFT JOIN hr_employee employee
ON employee.user_id = rel.user_id
LEFT JOIN res_users users
ON users.id = rel.user_id
LEFT JOIN res_partner user_partner
ON user_partner.id = users.partner_id
WHERE rel.user_id IS NOT NULL
AND COALESCE(task.date_assign, task.create_date) IS NOT NULL
"""
def _get_leave_select_sql(self):
leave_color_case = self._color_case_sql("COALESCE(leave_type.color, 0)")
return f"""
SELECT
CONCAT('leave-', leave.id::varchar) AS source_key,
CONCAT('Leave - ', leave_type.name) AS name,
leave.employee_id AS employee_id,
employee.user_id AS user_id,
NULL::integer AS project_id,
NULL::integer AS task_id,
NULL::integer AS timeline_id,
NULL::integer AS stage_id,
leave.id AS leave_id,
leave.holiday_status_id AS leave_type_id,
leave.date_from AS date_start,
leave.date_to AS date_stop,
'leave'::varchar AS entry_type,
NULL::integer AS project_color,
COALESCE(leave_type.color, 0) AS leave_color,
COALESCE(leave_type.color, 0) AS display_color,
{leave_color_case} AS display_color_hex,
leave_type.name::varchar AS description,
'Approved Time Off'::varchar AS source_label,
leave.state::varchar AS state,
FALSE AS is_public_holiday,
employee.name::varchar AS focus_label
FROM hr_leave leave
JOIN hr_employee employee
ON employee.id = leave.employee_id
JOIN hr_leave_type leave_type
ON leave_type.id = leave.holiday_status_id
WHERE leave.state IN ('confirm', 'validate1', 'validate')
AND leave.date_from IS NOT NULL
AND leave.date_to IS NOT NULL
"""
def _get_public_holiday_select_sql(self):
holiday_color_case = self._color_case_sql("3")
return f"""
SELECT
CONCAT('public-holiday-', holiday.id::varchar, '-', employee.id::varchar) AS source_key,
CONCAT('Public Holiday - ', COALESCE(holiday.name, 'Company Holiday')) AS name,
employee.id AS employee_id,
employee.user_id AS user_id,
NULL::integer AS project_id,
NULL::integer AS task_id,
NULL::integer AS timeline_id,
NULL::integer AS stage_id,
NULL::integer AS leave_id,
NULL::integer AS leave_type_id,
holiday.date_from AS date_start,
holiday.date_to AS date_stop,
'leave'::varchar AS entry_type,
NULL::integer AS project_color,
3 AS leave_color,
3 AS display_color,
{holiday_color_case} AS display_color_hex,
holiday.name::varchar AS description,
'Public Holiday'::varchar AS source_label,
'public_holiday'::varchar AS state,
TRUE AS is_public_holiday,
employee.name::varchar AS focus_label
FROM resource_calendar_leaves holiday
JOIN hr_employee employee
ON employee.active = TRUE
AND (
holiday.company_id IS NULL
OR holiday.company_id = employee.company_id
)
WHERE holiday.resource_id IS NULL
AND holiday.time_type = 'leave'
AND holiday.date_from IS NOT NULL
AND holiday.date_to IS NOT NULL
"""
def _get_source_selects_sql(self):
return [
self._get_normal_task_select_sql(),
self._get_leave_select_sql(),
self._get_public_holiday_select_sql(),
]
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(
f"""
CREATE OR REPLACE VIEW {self._table} AS (
SELECT
ROW_NUMBER() OVER (
ORDER BY
entry_order.date_start,
entry_order.employee_id,
entry_order.user_id,
entry_order.source_key
) AS id,
entry_order.name,
entry_order.employee_id,
entry_order.user_id,
entry_order.project_id,
entry_order.task_id,
entry_order.timeline_id,
entry_order.stage_id,
entry_order.leave_id,
entry_order.leave_type_id,
entry_order.date_start,
entry_order.date_stop,
entry_order.entry_type,
entry_order.project_color,
entry_order.leave_color,
entry_order.display_color,
entry_order.display_color_hex,
entry_order.description,
entry_order.source_label,
entry_order.state,
entry_order.is_public_holiday,
entry_order.focus_label
FROM (
{" UNION ALL ".join(self._get_source_selects_sql())}
) entry_order
)
"""
)

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_user_timeline_entry_user,user.timeline.entry user,model_user_timeline_entry,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_user_timeline_entry_user user.timeline.entry user model_user_timeline_entry base.group_user 1 0 0 0

View File

@ -0,0 +1,32 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { Many2OneAvatarField, many2OneAvatarField } from "@web/views/fields/many2one_avatar/many2one_avatar_field";
export class UserTimelineField extends Many2OneAvatarField {
static template = "user_timelines.UserTimelineField";
get hasTimelineButton() {
return !!this.resId && ["hr.employee", "res.users"].includes(this.relation);
}
async onTimelineBtnClick(ev) {
ev.preventDefault();
ev.stopPropagation();
if (!this.hasTimelineButton) {
return;
}
const action = await this.orm.call(this.relation, "action_open_timeline", [[this.resId]]);
await this.action.doAction(action);
}
}
export const userTimelineField = {
...many2OneAvatarField,
component: UserTimelineField,
displayName: _t("User Timeline"),
};
registry.category("fields").add("user_timeline", userTimelineField);
registry.category("fields").add("list.user_timeline", userTimelineField);

View File

@ -0,0 +1,183 @@
.o_user_timeline_button {
border-radius: 999px;
transition: background-color 0.2s ease, color 0.2s ease;
white-space: nowrap;
&:hover {
background: rgba(48, 75, 224, 0.08);
color: #304be0;
}
}
.o_gantt_view {
.o_gantt_pill_wrapper {
padding-block: 2px;
}
.o_user_timeline_pill {
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
}
}
.o_user_timeline_project_panel {
display: grid;
gap: 1rem;
}
.o_user_timeline_project_hero {
background:
radial-gradient(circle at top right, rgba(87, 148, 221, 0.18), transparent 30%),
linear-gradient(135deg, #f8fbff 0%, #eef4ff 52%, #f8fafc 100%);
border: 1px solid rgba(148, 163, 184, 0.22);
border-radius: 20px;
display: grid;
gap: 1rem;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
padding: 1.25rem;
}
.o_user_timeline_project_heading h2 {
color: #0f172a;
font-size: 1.55rem;
font-weight: 700;
margin: 0.15rem 0 0.45rem;
}
.o_user_timeline_project_heading p {
color: #475569;
line-height: 1.6;
margin: 0;
max-width: 68ch;
}
.o_user_timeline_project_kicker {
color: #304be0;
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.o_user_timeline_project_color_row {
align-items: center;
color: #475569;
display: flex;
gap: 0.6rem;
margin-bottom: 0.65rem;
}
.o_user_timeline_project_color_label {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.o_user_timeline_project_metrics {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.o_user_timeline_metric_card {
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 16px;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.06);
display: grid;
gap: 0.2rem;
min-height: 92px;
padding: 1rem;
}
.o_user_timeline_metric_value {
color: #0f172a;
font-size: 1.6rem;
font-weight: 800;
line-height: 1;
}
.o_user_timeline_metric_label {
color: #475569;
font-size: 0.82rem;
font-weight: 600;
}
.o_user_timeline_project_toolbar {
align-items: center;
background: #fff;
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 16px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
display: flex;
gap: 1rem;
justify-content: space-between;
padding: 0.9rem 1rem;
}
.o_user_timeline_preview_list {
.o_data_row td {
vertical-align: middle;
}
}
.o_user_timeline_pill {
--user-timeline-color: #5794dd;
align-items: center;
backdrop-filter: blur(8px);
background: linear-gradient(
135deg,
color-mix(in srgb, var(--user-timeline-color) 90%, #ffffff) 0%,
color-mix(in srgb, var(--user-timeline-color) 72%, #0f172a) 100%
);
border: 1px solid color-mix(in srgb, var(--user-timeline-color) 65%, #0f172a);
border-radius: 10px;
color: #fff;
display: flex;
gap: 0.45rem;
min-height: 100%;
overflow: hidden;
padding: 0.35rem 0.65rem;
width: 100%;
}
.o_user_timeline_leave {
background-image: repeating-linear-gradient(
135deg,
color-mix(in srgb, var(--user-timeline-color) 92%, #ffffff) 0,
color-mix(in srgb, var(--user-timeline-color) 92%, #ffffff) 8px,
color-mix(in srgb, var(--user-timeline-color) 62%, #ffffff) 8px,
color-mix(in srgb, var(--user-timeline-color) 62%, #ffffff) 16px
);
}
.o_user_timeline_badge {
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 999px;
flex-shrink: 0;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
padding: 0.15rem 0.45rem;
text-transform: uppercase;
}
.o_user_timeline_title {
font-size: 12px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 991px) {
.o_user_timeline_project_hero {
grid-template-columns: 1fr;
}
.o_user_timeline_project_toolbar {
align-items: flex-start;
flex-direction: column;
}
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="user_timelines.UserTimelineField">
<div class="d-flex align-items-center gap-1" t-att-data-tooltip="props.record.data[props.name] &amp;&amp; props.record.data[props.name][1]">
<span class="o_avatar o_m2o_avatar">
<span t-if="props.record.data[props.name] === false &amp;&amp; !props.readonly" class="o_avatar_empty o_m2o_avatar_empty"></span>
<img t-if="props.record.data[props.name] !== false"
t-attf-src="/web/image/{{relation}}/{{props.record.data[props.name][0]}}/avatar_128"
class="rounded"/>
</span>
<div class="o_field_user_timeline_selection flex-grow-1">
<t t-if="props.readonly">
<t t-if="!props.canOpen">
<span>
<span t-esc="displayName"/>
<t t-foreach="extraLines" t-as="extraLine" t-key="extraLine_index">
<br/>
<span t-esc="extraLine"/>
</t>
</span>
</t>
<t t-else="">
<a t-if="value"
t-attf-class="o_form_uri #{classFromDecoration}"
t-att-href="value ? `/odoo/${urlRelation}/${value[0]}` : '/'"
t-on-click.prevent="onClick">
<span t-esc="displayName"/>
<t t-foreach="extraLines" t-as="extraLine" t-key="extraLine_index">
<br/>
<span t-esc="extraLine"/>
</t>
</a>
</t>
</t>
<t t-else="">
<div class="o_field_many2one_selection">
<Many2XAutocomplete t-props="Many2XAutocompleteProps"/>
<t t-if="hasExternalButton">
<button type="button"
class="btn btn-link text-action oi o_external_button px-1"
t-att-class="env.inDialog ? 'oi-launch' : 'oi-arrow-right'"
tabindex="-1"
draggable="false"
aria-label="Internal link"
data-tooltip="Internal link"
t-on-click="onExternalBtnClick"/>
</t>
<t t-if="hasTimelineButton">
<button type="button"
class="btn btn-link text-action fa fa-calendar px-1 o_user_timeline_button"
tabindex="-1"
draggable="false"
aria-label="Open timeline"
data-tooltip="Open timeline"
t-on-click="onTimelineBtnClick"/>
</t>
</div>
<div class="o_field_many2one_extra">
<t t-foreach="extraLines" t-as="extraLine" t-key="extraLine_index">
<br t-if="!extraLine_first"/>
<span t-esc="extraLine"/>
</t>
</div>
</t>
</div>
<t t-if="props.readonly &amp;&amp; hasTimelineButton">
<button type="button"
class="btn btn-link text-action fa fa-calendar px-1 o_user_timeline_button"
tabindex="-1"
draggable="false"
aria-label="Open timeline"
data-tooltip="Open timeline"
t-on-click="onTimelineBtnClick"/>
</t>
</div>
</t>
</templates>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_employee_form_user_timeline" model="ir.ui.view">
<field name="name">hr.employee.form.user.timeline</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_open_timeline"
type="object"
class="oe_stat_button"
icon="fa-calendar"
invisible="not id">
<span class="o_stat_text">Timeline</span>
</button>
</xpath>
<xpath expr="//field[@name='parent_id']" position="replace">
<field name="parent_id" widget="user_timeline"/>
</xpath>
<xpath expr="//field[@name='coach_id']" position="replace">
<field name="coach_id" widget="user_timeline"/>
</xpath>
<xpath expr="//group[@name='active_group']//field[@name='user_id']" position="replace">
<field name="user_id"
string="Related User"
help=""
domain="[('company_ids', 'in', company_id), ('share', '=', False)]"
context="{'default_create_employee_id': id}"
widget="user_timeline"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="project_project_form_user_timeline_color" model="ir.ui.view">
<field name="name">project.project.form.user.timeline.color</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="after">
<label for="timeline_color_hex" string="Timeline Color"/>
<div class="o_row align-items-center gap-2">
<field name="timeline_color_hex" widget="color" class="w-auto"/>
<field name="timeline_color_hex" placeholder="#5794dd"/>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="res_users_view_form_profile_user_timeline" model="ir.ui.view">
<field name="name">res.users.form.profile.user.timeline</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_open_timeline"
type="object"
class="oe_stat_button"
icon="fa-calendar"
invisible="not id">
<span class="o_stat_text">Timeline</span>
</button>
</xpath>
<xpath expr="//field[@name='employee_parent_id']" position="replace">
<field name="employee_parent_id" readonly="not can_edit" widget="user_timeline"/>
</xpath>
<xpath expr="//field[@name='coach_id']" position="replace">
<field name="coach_id" readonly="not can_edit" widget="user_timeline"/>
</xpath>
</field>
</record>
<record id="res_users_view_form_user_timeline" model="ir.ui.view">
<field name="name">res.users.form.user.timeline</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="hr.res_users_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_open_timeline"
type="object"
class="oe_stat_button"
icon="fa-calendar"
invisible="not id">
<span class="o_stat_text">Timeline</span>
</button>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_user_timeline_entry_search" model="ir.ui.view">
<field name="name">user.timeline.entry.search</field>
<field name="model">user.timeline.entry</field>
<field name="arch" type="xml">
<search string="Timelines">
<field name="name"/>
<field name="employee_id"/>
<field name="user_id"/>
<field name="project_id"/>
<field name="task_id"/>
<field name="leave_type_id"/>
<field name="focus_label"/>
<group>
<filter name="tasks_only" string="Tasks" domain="[('entry_type', '=', 'task')]"/>
<filter name="leaves_only" string="Leaves"
domain="[('entry_type', '=', 'leave'),('is_public_holiday', '=', False)]"/>
<filter name="public_holidays_only" string="Public Holidays"
domain="[('is_public_holiday', '=', True)]"/>
</group>
<group>
<filter name="public_holidays_remove" string="Hide Public Holidays"
domain="[('is_public_holiday', '=', False)]"/>
</group>
<group expand="0" string="Group By">
<filter name="group_employee" string="Employee" context="{'group_by': 'employee_id'}"/>
<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_type" string="Type" context="{'group_by': 'entry_type'}"/>
</group>
</search>
</field>
</record>
<record id="view_user_timeline_entry_list" model="ir.ui.view">
<field name="name">user.timeline.entry.list</field>
<field name="model">user.timeline.entry</field>
<field name="arch" type="xml">
<list string="Timelines" create="false" edit="false" delete="false">
<field name="name"/>
<field name="employee_id"/>
<field name="user_id" optional="hide"/>
<field name="entry_type"/>
<field name="project_id" optional="show"/>
<field name="task_id" optional="show"/>
<field name="leave_type_id" optional="show"/>
<field name="is_public_holiday" optional="hide"/>
<field name="date_start"/>
<field name="date_stop"/>
<field name="source_label"/>
</list>
</field>
</record>
<record id="view_user_timeline_entry_calendar" model="ir.ui.view">
<field name="name">user.timeline.entry.calendar</field>
<field name="model">user.timeline.entry</field>
<field name="arch" type="xml">
<calendar string="Employee Timeline"
date_start="date_start"
date_stop="date_stop"
color="display_color"
mode="week"
create="false"
quick_create="false">
<field name="name"/>
<field name="employee_id"/>
<field name="project_id"/>
<field name="leave_type_id"/>
<field name="entry_type"/>
</calendar>
</field>
</record>
<record id="view_user_timeline_entry_gantt" model="ir.ui.view">
<field name="name">user.timeline.entry.gantt</field>
<field name="model">user.timeline.entry</field>
<field name="arch" type="xml">
<gantt string="Employee Timeline"
date_start="date_start"
date_stop="date_stop"
default_scale="week"
scales="day,week,month,year"
default_group_by="employee_id"
color="display_color"
display_mode="sparse"
create="false"
edit="false"
delete="false"
pill_label="True"
total_row="True"
precision="{'day': 'hour:quarter', 'week': 'day:half', 'month': 'day:half'}">
<templates>
<div t-name="gantt-popover">
<div>
<strong>Timeline -</strong>
<t t-esc="name"/>
</div>
<div t-if="focus_label">
<strong>Focus -</strong>
<t t-esc="focus_label"/>
</div>
<div t-if="employee_id">
<strong>Employee -</strong>
<t t-esc="employee_id[1]"/>
</div>
<div t-if="project_id">
<strong>Project -</strong>
<t t-esc="project_id[1]"/>
</div>
<div t-if="task_id">
<strong>Task -</strong>
<t t-esc="task_id[1]"/>
</div>
<div t-if="leave_type_id">
<strong>Leave Type -</strong>
<t t-esc="leave_type_id[1]"/>
</div>
<div t-if="is_public_holiday"><strong>Scope -</strong>Company Holiday
</div>
<div>
<strong>Type -</strong>
<t t-esc="entry_type"/>
</div>
<div>
<t t-esc="date_start.toFormat('MMM dd, yyyy HH:mm')"/>
<i class="fa fa-long-arrow-right" title="Arrow"/>
<t t-esc="date_stop.toFormat('MMM dd, yyyy HH:mm')"/>
</div>
</div>
<div t-name="gantt-pulse">
<div t-if="entry_type == 'task'"
class="o_user_timeline_pill o_user_timeline_task"
t-attf-style="--user-timeline-color: {{ display_color_hex }};">
<span class="o_user_timeline_badge">Task</span>
<span class="o_user_timeline_title" t-esc="name"/>
</div>
<div t-if="entry_type == 'leave'"
class="o_user_timeline_pill o_user_timeline_leave"
t-attf-style="--user-timeline-color: {{ display_color_hex }};">
<span class="o_user_timeline_badge" t-esc="is_public_holiday ? 'Holiday' : 'Leave'"/>
<span class="o_user_timeline_title" t-esc="name"/>
</div>
</div>
</templates>
<field name="name"/>
<field name="employee_id"/>
<field name="project_id"/>
<field name="task_id"/>
<field name="leave_type_id"/>
<field name="entry_type"/>
<field name="display_color_hex"/>
<field name="is_public_holiday"/>
<field name="focus_label"/>
</gantt>
</field>
</record>
<record id="action_user_timeline_entries" model="ir.actions.act_window">
<field name="name">Employee Timelines</field>
<field name="res_model">user.timeline.entry</field>
<field name="view_mode">gantt,calendar,list</field>
<field name="search_view_id" ref="view_user_timeline_entry_search"/>
<field name="context">{'search_default_group_employee': 1,'search_default_public_holidays_remove': 1,
'default_is_public_holiday': 0}
</field>
</record>
<record id="action_user_timeline_entries_gantt" model="ir.actions.act_window.view">
<field name="sequence" eval="10"/>
<field name="view_mode">gantt</field>
<field name="act_window_id" ref="action_user_timeline_entries"/>
<field name="view_id" ref="view_user_timeline_entry_gantt"/>
</record>
<record id="action_user_timeline_entries_calendar" model="ir.actions.act_window.view">
<field name="sequence" eval="20"/>
<field name="view_mode">calendar</field>
<field name="act_window_id" ref="action_user_timeline_entries"/>
<field name="view_id" ref="view_user_timeline_entry_calendar"/>
</record>
<record id="action_user_timeline_entries_list" model="ir.actions.act_window.view">
<field name="sequence" eval="30"/>
<field name="view_mode">list</field>
<field name="act_window_id" ref="action_user_timeline_entries"/>
<field name="view_id" ref="view_user_timeline_entry_list"/>
</record>
<menuitem id="menu_user_timeline_entries"
name="Employee Timelines"
parent="hr.menu_hr_employee_payroll"
action="action_user_timeline_entries"
groups="hr.group_hr_user"
sequence="15"/>
</odoo>