new timeline feature and bug fixes
This commit is contained in:
parent
723dcbe225
commit
a9a6c683d7
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
"""
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"/>-->
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from . import hr_employee
|
||||
from . import project_project
|
||||
from . import res_users
|
||||
from . import user_timeline_entry
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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] && props.record.data[props.name][1]">
|
||||
<span class="o_avatar o_m2o_avatar">
|
||||
<span t-if="props.record.data[props.name] === false && !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 && 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue