project task timesheets sprints and kanban buttons

This commit is contained in:
pranaysaidurga 2026-07-14 18:30:46 +05:30
parent fd866fb866
commit e5e876ecb4
6 changed files with 503 additions and 61 deletions

View File

@ -238,6 +238,27 @@ class ProjectProject(models.Model):
"project_id",
string="Project Sprints"
)
active_sprint_id = fields.Many2one(
"project.sprint",
compute="_compute_sprint_planning_metrics",
string="Active Sprint",
)
sprint_count = fields.Integer(
compute="_compute_sprint_planning_metrics",
string="Sprints",
)
sprint_backlog_task_count = fields.Integer(
compute="_compute_sprint_planning_metrics",
string="Backlog Tasks",
)
sprint_planned_hours = fields.Float(
compute="_compute_sprint_planning_metrics",
string="Sprint Planned Hours",
)
sprint_actual_hours = fields.Float(
compute="_compute_sprint_planning_metrics",
string="Sprint Actual Hours",
)
commit_step_ids = fields.One2many(
'project.commit.step',
@ -301,6 +322,45 @@ class ProjectProject(models.Model):
hold_reason = fields.Text(string="Hold Reason", tracking=True)
privacy_visibility = fields.Selection(default="followers")
@api.depends(
"sprint_ids",
"sprint_ids.status",
"sprint_ids.task_ids",
"sprint_ids.task_ids.sprint_estimated_hours",
"sprint_ids.task_ids.estimated_hours",
"sprint_ids.task_ids.actual_hours",
"task_ids.sprint_id",
)
def _compute_sprint_planning_metrics(self):
for project in self:
active_sprint = project.sprint_ids.filtered(lambda sprint: sprint.status == "in_progress")[:1]
sprint_tasks = project.task_ids.filtered(lambda task: task.sprint_id)
project.active_sprint_id = active_sprint
project.sprint_count = len(project.sprint_ids)
project.sprint_backlog_task_count = len(project.task_ids.filtered(lambda task: not task.sprint_id))
project.sprint_planned_hours = sum(task.sprint_estimated_hours or task.estimated_hours for task in sprint_tasks)
project.sprint_actual_hours = sum(sprint_tasks.mapped("actual_hours"))
def action_open_sprint_backlog(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Sprint Backlog"),
"res_model": "project.task",
"view_mode": "kanban,list,form,calendar,pivot,graph,activity",
"domain": [("project_id", "=", self.id), ("sprint_id", "=", False)],
"context": {
"default_project_id": self.id,
"search_default_project_id": self.id,
},
}
def action_open_active_sprint_tasks(self):
self.ensure_one()
if not self.active_sprint_id:
raise UserError(_("There is no active sprint for this project."))
return self.active_sprint_id.action_open_tasks()
def action_hold_unhold(self):
for project in self:
if project.project_state == 'hold':
@ -1179,8 +1239,8 @@ class ProjectTask(models.Model):
def _default_sprint_id(self):
"""Return the current active (in-progress) sprint of the project."""
if 'project_id' in self._context:
project_id = self._context.get('project_id')
project_id = self._context.get('default_project_id') or self._context.get('project_id')
if project_id:
sprint = self.env['project.sprint'].search([
('project_id', '=', project_id),
('status', '=', 'in_progress')
@ -1191,7 +1251,9 @@ class ProjectTask(models.Model):
sprint_id = fields.Many2one(
"project.sprint",
string="Sprint",
default=_default_sprint_id
default=_default_sprint_id,
tracking=True,
index=True,
)
require_sprint = fields.Boolean(
@ -1224,6 +1286,8 @@ class ProjectTask(models.Model):
for task in self:
if task.project_id and not task.project_id.require_sprint:
task.sprint_id = False
elif task.sprint_id and task.sprint_id.project_id != task.project_id:
task.sprint_id = False
else:
if task.project_id and task.project_id.require_sprint:
sprint = self.env['project.sprint'].search([
@ -1232,10 +1296,35 @@ class ProjectTask(models.Model):
], limit=1)
task.sprint_id = sprint.id
@api.onchange("sprint_id")
def _onchange_sprint_id_project(self):
for task in self:
if task.sprint_id and not task.project_id:
task.project_id = task.sprint_id.project_id
@api.constrains("project_id", "sprint_id")
def _check_sprint_project(self):
for task in self:
if task.sprint_id and task.project_id and task.sprint_id.project_id != task.project_id:
raise ValidationError(_("Task sprint must belong to the same project as the task."))
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
project_id = vals.get("project_id") or self._context.get("default_project_id")
if project_id and "sprint_id" not in vals:
project = self.env["project.project"].browse(project_id)
if project.require_sprint:
sprint = self.env["project.sprint"].search([
("project_id", "=", project.id),
("status", "=", "in_progress"),
], limit=1)
if sprint:
vals["sprint_id"] = sprint.id
return super().create(vals_list)
def action_show_project_task_chatter(self):
"""Toggle visibility of project chatter"""
for project in self:
project.show_task_chatter = not project.show_task_chatter

View File

@ -1,30 +1,112 @@
from odoo import models, fields, api, _
class ProjectSprint(models.Model):
_name = "project.sprint"
_description = "Project Sprint"
_order = "date_start desc"
project_id = fields.Many2one(
"project.project",
required=True,
ondelete="cascade"
)
sprint_name = fields.Char(string="Sprint Name", required=True)
date_start = fields.Date(string="Start Date", required=True)
date_end = fields.Date(string="End Date", required=True)
allocated_hours = fields.Float(string="Allocated Hours")
status = fields.Selection(
[
("draft", "Draft"),
("in_progress", "In Progress"),
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
class ProjectSprint(models.Model):
_name = "project.sprint"
_description = "Project Sprint"
_rec_name = "sprint_name"
_order = "date_start desc, id desc"
project_id = fields.Many2one(
"project.project",
required=True,
ondelete="cascade",
index=True,
)
sprint_name = fields.Char(string="Sprint Name", required=True)
date_start = fields.Date(string="Start Date", required=True)
date_end = fields.Date(string="End Date", required=True)
allocated_hours = fields.Float(string="Capacity Hours")
task_ids = fields.One2many("project.task", "sprint_id", string="Sprint Tasks")
task_count = fields.Integer(compute="_compute_sprint_metrics", string="Tasks")
completed_task_count = fields.Integer(compute="_compute_sprint_metrics", string="Completed Tasks")
planned_hours = fields.Float(compute="_compute_sprint_metrics", string="Planned Hours")
actual_hours = fields.Float(compute="_compute_sprint_metrics", string="Actual Hours")
remaining_hours = fields.Float(compute="_compute_sprint_metrics", string="Remaining Hours")
progress = fields.Float(compute="_compute_sprint_metrics", string="Progress")
status = fields.Selection(
[
("draft", "Draft"),
("in_progress", "In Progress"),
("done", "Completed"),
],
default="draft",
string="Status"
)
done_date = fields.Date(string="Done Date")
note = fields.Text(string="Note")
)
done_date = fields.Date(string="Done Date")
note = fields.Text(string="Note")
@api.depends(
"allocated_hours",
"task_ids",
"task_ids.state",
"task_ids.sprint_estimated_hours",
"task_ids.estimated_hours",
"task_ids.actual_hours",
)
def _compute_sprint_metrics(self):
done_states = ("1_done", "1_canceled")
for sprint in self:
tasks = sprint.task_ids
sprint.task_count = len(tasks)
sprint.completed_task_count = len(tasks.filtered(lambda task: task.state in done_states))
sprint.planned_hours = sum(task.sprint_estimated_hours or task.estimated_hours for task in tasks)
sprint.actual_hours = sum(tasks.mapped("actual_hours"))
sprint.remaining_hours = sprint.allocated_hours - sprint.planned_hours
sprint.progress = (sprint.completed_task_count / sprint.task_count * 100.0) if sprint.task_count else 0.0
@api.constrains("date_start", "date_end")
def _check_sprint_dates(self):
for sprint in self:
if sprint.date_start and sprint.date_end and sprint.date_start > sprint.date_end:
raise ValidationError(_("Sprint start date must be before the end date."))
@api.constrains("project_id", "status")
def _check_single_active_sprint(self):
for sprint in self.filtered(lambda rec: rec.status == "in_progress"):
active_sprint = self.search([
("project_id", "=", sprint.project_id.id),
("status", "=", "in_progress"),
("id", "!=", sprint.id),
], limit=1)
if active_sprint:
raise ValidationError(_(
"Only one sprint can be in progress per project. "
"Please complete '%s' before starting another sprint."
) % active_sprint.display_name)
def action_start_sprint(self):
for sprint in self:
if not sprint.task_ids:
raise UserError(_("Add at least one task before starting the sprint."))
sprint.status = "in_progress"
return True
def action_complete_sprint(self):
open_tasks = self.mapped("task_ids").filtered(lambda task: task.state not in ("1_done", "1_canceled"))
if open_tasks:
raise UserError(_("Complete or cancel all sprint tasks before closing the sprint."))
self.write({"status": "done", "done_date": fields.Date.context_today(self)})
return True
def action_reset_to_draft(self):
self.write({"status": "draft", "done_date": False})
return True
def action_open_tasks(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Sprint Tasks"),
"res_model": "project.task",
"view_mode": "kanban,list,form,calendar,pivot,graph,activity",
"domain": [("sprint_id", "=", self.id)],
"context": {
"default_project_id": self.project_id.id,
"default_sprint_id": self.id,
"search_default_project_id": self.project_id.id,
},
}

View File

@ -28,6 +28,17 @@ class projectTask(models.Model):
_rec_name = 'name'
user_ids = fields.Many2many()
is_project_admin = fields.Boolean(
compute="_compute_is_project_admin"
)
@api.depends("project_id","project_id.user_id")
def _compute_is_project_admin(self):
project_manager = self.project_id.user_id
is_admin = self.env.user.has_group("project.group_project_manager") or self.env.user.has_group("base.group_system") or (self.env.user.id == project_manager.id)
# Replace the XML ID if your administrator group is different.
for rec in self:
rec.is_project_admin = is_admin
involved_user_ids = fields.Many2many(
'res.users',
@ -132,6 +143,17 @@ class projectTask(models.Model):
store=True,
readonly=False
)
sprint_estimated_hours = fields.Float(
string="Sprint Estimated Hours",
tracking=True,
help="Planning estimate committed during sprint planning. This is kept separate from assignee timeline estimates."
)
assignee_estimated_hours = fields.Float(
string="Assignee Estimated Hours",
compute="_compute_assignee_estimated_hours",
store=True,
help="Estimate calculated from the assignee timelines."
)
def write(self, vals):
for rec in self:
@ -560,11 +582,21 @@ class projectTask(models.Model):
or current_user in task.project_id.project_lead
)
)
@api.depends('assignees_timelines.estimated_time', 'show_approval_flow')
@api.depends('assignees_timelines.estimated_time')
def _compute_assignee_estimated_hours(self):
for task in self:
task.assignee_estimated_hours = sum(task.assignees_timelines.mapped('estimated_time'))
@api.depends('assignees_timelines.estimated_time', 'show_approval_flow', 'sprint_estimated_hours')
def _compute_estimated_hours(self):
for task in self:
if task.show_approval_flow:
task.estimated_hours = sum(task.assignees_timelines.mapped('estimated_time'))
assignee_estimate = sum(task.assignees_timelines.mapped('estimated_time'))
if task.show_approval_flow and assignee_estimate:
task.estimated_hours = assignee_estimate
elif task.sprint_estimated_hours:
task.estimated_hours = task.sprint_estimated_hours
else:
task.estimated_hours = task.estimated_hours or 0.0
@api.depends('timesheet_ids.unit_amount')
def _compute_actual_hours(self):
@ -1310,9 +1342,10 @@ class projectTask(models.Model):
for task in self:
# If user is not allowed to change stage
if task.show_approval_flow and not task.can_edit_approval_flow_stages and not self.env.user.has_group("project.group_project_manager"):
raise UserError(_(
"You are not allowed to change the stage of this task because stage editing is restricted."
))
if not task.is_project_admin:
raise UserError(_(
"You are not allowed to change the stage of this task because stage editing is restricted."
))
result = super(projectTask, self).write(vals)
if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
@ -1942,5 +1975,3 @@ class projectTaskTimelines(models.Model):

View File

@ -72,19 +72,30 @@
invisible="not assign_approval_flow or not show_back_button or not begin_approval_processing"/>
</xpath>
<xpath expr="//form" position="inside">
<field name="begin_approval_processing" invisible="1"/>
<field name="showable_stage_ids" invisible="1"/>
<field name="assign_approval_flow" invisible="1"/>
<field name="manager_level_edit_access" invisible="1"/>
<xpath expr="//form" position="inside">
<field name="begin_approval_processing" invisible="1"/>
<field name="showable_stage_ids" invisible="1"/>
<field name="assign_approval_flow" invisible="1"/>
<field name="manager_level_edit_access" invisible="1"/>
<field name="show_submission_button" invisible="1"/>
<field name="show_approval_button" invisible="1"/>
<field name="show_refuse_button" invisible="1"/>
<field name="show_back_button" invisible="1"/>
<field name="is_project_editor" invisible="1"/>
<field name="enable_budget_summary" invisible="1"/>
<field name="can_edit_stage_in_approval" invisible="1"/>
</xpath>
<field name="is_project_editor" invisible="1"/>
<field name="enable_budget_summary" invisible="1"/>
<field name="can_edit_stage_in_approval" invisible="1"/>
<field name="active_sprint_id" invisible="1"/>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<button class="oe_stat_button" type="object" name="action_open_sprint_backlog"
icon="fa-list-ul" invisible="not require_sprint">
<field name="sprint_backlog_task_count" widget="statinfo" string="Backlog"/>
</button>
<button class="oe_stat_button" type="object" name="action_open_active_sprint_tasks"
icon="fa-play-circle" invisible="not require_sprint or not active_sprint_id">
<field name="active_sprint_id" widget="statinfo" string="Active Sprint"/>
</button>
</xpath>
<xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute>
</xpath>
@ -506,19 +517,105 @@
</field>
</page>
<page name="project_sprints" string="Sprints" invisible="not require_sprint">
<field name="sprint_ids">
<list editable="bottom">
<field name="sprint_name" width="20%"/>
<field name="date_start" width="20%"/>
<field name="date_end" width="20%"/>
<field name="allocated_hours" width="20%"/>
<field name="status" width="20%"/>
<field name="done_date" optional="hide"/>
<field name="note" optional="hide"/>
</list>
</field>
</page>
<page name="project_sprints" string="Sprints" invisible="not require_sprint">
<group string="Sprint Planning">
<group>
<field name="active_sprint_id" readonly="1"/>
<field name="sprint_count" readonly="1"/>
<field name="sprint_backlog_task_count" readonly="1"/>
</group>
<group>
<field name="sprint_planned_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
<field name="sprint_actual_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
</group>
</group>
<group>
<button name="action_open_sprint_backlog" type="object" string="Open Backlog" class="btn-secondary"/>
<button name="action_open_active_sprint_tasks" type="object" string="Open Active Sprint" class="btn-primary"
invisible="not active_sprint_id"/>
</group>
<field name="sprint_ids" context="{'default_project_id': id}">
<list>
<field name="sprint_name"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="allocated_hours" widget="timesheet_uom_no_toggle"/>
<field name="planned_hours" widget="timesheet_uom_no_toggle"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle"/>
<field name="remaining_hours" widget="timesheet_uom_no_toggle"
decoration-danger="remaining_hours &lt; 0"/>
<field name="task_count"/>
<field name="progress" widget="progressbar"/>
<field name="status" widget="badge"
decoration-muted="status == 'draft'"
decoration-info="status == 'in_progress'"
decoration-success="status == 'done'"/>
<button name="action_start_sprint" type="object" string="Start" class="btn-primary"
invisible="status != 'draft'"/>
<button name="action_complete_sprint" type="object" string="Complete" class="btn-secondary"
invisible="status != 'in_progress'"/>
<button name="action_open_tasks" type="object" string="Tasks" class="btn-link"/>
<field name="done_date" optional="hide"/>
</list>
<form string="Sprint">
<header>
<button name="action_start_sprint" type="object" string="Start Sprint" class="oe_highlight"
invisible="status != 'draft'"/>
<button name="action_complete_sprint" type="object" string="Complete Sprint" class="oe_highlight"
invisible="status != 'in_progress'"/>
<button name="action_reset_to_draft" type="object" string="Reset to Draft"
invisible="status == 'draft'"/>
<field name="status" widget="statusbar" statusbar_visible="draft,in_progress,done"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="object" name="action_open_tasks" icon="fa-tasks">
<field name="task_count" widget="statinfo" string="Tasks"/>
</button>
</div>
<group>
<group>
<field name="project_id" invisible="1"/>
<field name="sprint_name"/>
<field name="date_start"/>
<field name="date_end"/>
<field name="allocated_hours" widget="timesheet_uom_no_toggle"/>
</group>
<group>
<field name="task_count" readonly="1"/>
<field name="completed_task_count" readonly="1"/>
<field name="planned_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
<field name="remaining_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
<field name="progress" widget="progressbar" readonly="1"/>
<field name="done_date" readonly="1" invisible="not done_date"/>
</group>
</group>
<notebook>
<page string="Sprint Tasks">
<field name="task_ids" context="{'default_project_id': project_id, 'default_sprint_id': id}">
<list editable="bottom">
<field name="priority" widget="priority" nolabel="1"/>
<field name="name"/>
<field name="project_id" column_invisible="1"/>
<field name="stage_id"/>
<field name="user_ids" widget="many2many_avatar_user"/>
<field name="sprint_estimated_hours" widget="timesheet_uom_no_toggle"/>
<field name="assignee_estimated_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
<field name="estimated_hours" widget="timesheet_uom_no_toggle" readonly="1" optional="hide"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
<field name="state" widget="project_task_state_selection"/>
</list>
</field>
</page>
<page string="Notes">
<field name="note" nolabel="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</page>
<page name="development" string="Development Details" invisible="not is_development_user">
<group string="Documents">
<field name="development_document_ids">

View File

@ -69,6 +69,29 @@
</field>
</record>
<record id="project_view_task_search_form_my_tasks_inherit" model="ir.ui.view">
<field name="name">project.task.search.project.by.mytasks</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_search_form"/>
<field name="arch" type="xml">
<xpath expr="//search" position="inside">
<field name="model_id" string="Module" filter_domain="[('model_id.name', 'ilike', self)]"/>
<separator/>
<searchtab name="stage_id_records_tab" string="Stage">
<filter string="group_by_module" name="Stage" context="{'group_by': 'stage_id'}"/>
</searchtab>
<!-- <searchtab name="model_id_records_tab" string="Module">-->
<!-- <filter name="group_by_module" string="Module" context="{'group_by': 'model_id'}"/>-->
<!-- </searchtab>-->
</xpath>
</field>
</record>
<record id="project.action_view_my_task" model="ir.actions.act_window">
<field name="search_view_id" ref="project_task_timesheet_extended.project_view_task_search_form_my_tasks_inherit"/>
</record>
<record id="action_project_by_module" model="ir.actions.act_window">

View File

@ -27,6 +27,7 @@
<xpath expr="//field[@name='user_ids']" position="replace">
<label for="user_ids" string="Assignees"/>
<div class="o_task_assignees_inline">
<field name="is_project_admin" invisible="1" force_save="1"/>
<field name="user_ids" nolabel="1" widget="involved_assignee_avatar_user" domain="[('id', 'in', involved_user_ids)]" options="{'no_create': True, 'no_quick_create': True, 'no_create_edit': True}" invisible="is_generic"/>
<field name="user_ids" nolabel="1" class="o_task_user_field" options="{'no_open': True, 'no_quick_create': True}" widget="many2many_avatar_user" domain="[('id', 'in', assignee_domain_ids)]" invisible="not is_generic"/>
<button name="action_open_reassign_wizard" type="object" string="Re-Assign" title="Re-Assign Assignees" class="btn btn-secondary btn-sm o_task_reassign_button" invisible="record_paused or not show_reassign_button"/>
@ -66,7 +67,14 @@
<field name="involved_user_ids" widget="many2many_avatar_user" invisible="is_generic"/>
<field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/>
<field name="require_sprint" invisible="1"/>
<field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/>
<field name="sprint_id" invisible="not require_sprint"
domain="[('project_id', '=', project_id)]"
context="{'default_project_id': project_id}"
options="{'no_create_edit': True}"/>
<field name="sprint_estimated_hours" invisible="not require_sprint" widget="timesheet_uom_no_toggle"/>
<field name="assignee_estimated_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
</xpath>
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>-->
@ -168,6 +176,8 @@
<!-- </div>-->
<field name="estimated_hours" widget="timesheet_uom_no_toggle"
readonly="show_approval_flow and timelines_requested"/>
<field name="sprint_estimated_hours" widget="timesheet_uom_no_toggle" readonly="1" force_save="1"/>
<field name="assignee_estimated_hours" widget="timesheet_uom_no_toggle" readonly="1"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle"/>
<field name="is_suggested_deadline_warning" />
</xpath>
@ -215,6 +225,13 @@
<filter name="task_approved" string="Approved" domain="[('approval_status', '=', 'approved')]"/>
<filter name="task_rejected" string="Rejected" domain="[('approval_status', '=', 'refused')]"/>
</xpath>
<xpath expr="//field[@name='project_id']" position="after">
<field name="sprint_id" string="Sprint"/>
</xpath>
<xpath expr="//filter[@name='project']" position="after">
<filter string="Sprint" name="sprint" context="{'group_by': 'sprint_id'}"/>
<filter string="Sprint Backlog" name="sprint_backlog" domain="[('project_id.require_sprint', '=', True), ('sprint_id', '=', False)]"/>
</xpath>
<!-- <xpath expr="//searchpanel" position="replace"/>-->
</field>
</record>
@ -228,6 +245,19 @@
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
<field name="sprint_id"/>
<field name="sprint_estimated_hours"/>
<field name="assignee_estimated_hours"/>
<field name="estimated_hours"/>
<field name="show_approval_flow"/>
<field name="show_submission_button"/>
<field name="show_skip_approval_button"/>
<field name="show_approval_button"/>
<field name="show_refuse_button"/>
<field name="show_back_button"/>
<field name="timelines_requested"/>
<field name="record_paused"/>
<field name="is_project_admin"/>
</xpath>
<xpath expr="//t[@t-name='card']//field[@name='name']" position="after">
<div class="text-muted mt-1 small">
@ -238,6 +268,46 @@
<span>Module: </span>
<field name="module_display_name"/>
</div>
<div t-if="record.sprint_id.raw_value" class="text-muted small">
<span>Sprint: </span>
<field name="sprint_id"/>
</div>
<div t-if="record.sprint_estimated_hours.raw_value or record.assignee_estimated_hours.raw_value" class="d-flex flex-wrap gap-2 mt-2 small">
<span t-if="record.sprint_estimated_hours.raw_value" class="badge rounded-pill text-bg-light">
Sprint <field name="sprint_estimated_hours" widget="timesheet_uom_no_toggle"/>
</span>
<span t-if="record.assignee_estimated_hours.raw_value" class="badge rounded-pill text-bg-light">
Assignee <field name="assignee_estimated_hours" widget="timesheet_uom_no_toggle"/>
</span>
</div>
<div t-if="record.show_approval_flow.raw_value and !record.record_paused.raw_value and !record.is_project_admin.raw_value"
class="d-flex flex-wrap gap-1 mt-2">
<button t-if="record.show_submission_button.raw_value and !record.show_skip_approval_button.raw_value"
type="object" name="submit_for_approval"
class="btn btn-primary btn-sm">
Request Approval
</button>
<button t-if="record.show_skip_approval_button.raw_value"
type="object" name="submit_for_approval"
class="btn btn-primary btn-sm">
Next
</button>
<button t-if="record.show_approval_button.raw_value and record.timelines_requested.raw_value"
type="object" name="proceed_further"
class="btn btn-success btn-sm">
Approve
</button>
<button t-if="record.show_refuse_button.raw_value and record.timelines_requested.raw_value"
type="object" name="action_open_reject_wizard"
class="btn btn-outline-danger btn-sm">
Reject
</button>
<button t-if="record.show_back_button.raw_value and record.timelines_requested.raw_value"
type="object" name="back_button"
class="btn btn-outline-secondary btn-sm">
Back
</button>
</div>
</xpath>
</field>
</record>
@ -257,6 +327,18 @@
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
<field name="sprint_id"/>
<field name="sprint_estimated_hours"/>
<field name="assignee_estimated_hours"/>
<field name="estimated_hours"/>
<field name="show_approval_flow"/>
<field name="show_submission_button"/>
<field name="show_skip_approval_button"/>
<field name="show_approval_button"/>
<field name="show_refuse_button"/>
<field name="show_back_button"/>
<field name="timelines_requested"/>
<field name="record_paused"/>
</xpath>
</field>
</record>
@ -271,6 +353,18 @@
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
<field name="sprint_id"/>
<field name="sprint_estimated_hours"/>
<field name="assignee_estimated_hours"/>
<field name="estimated_hours"/>
<field name="show_approval_flow"/>
<field name="show_submission_button"/>
<field name="show_skip_approval_button"/>
<field name="show_approval_button"/>
<field name="show_refuse_button"/>
<field name="show_back_button"/>
<field name="timelines_requested"/>
<field name="record_paused"/>
</xpath>
</field>
</record>
@ -285,6 +379,32 @@
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
<field name="sprint_id"/>
<field name="sprint_estimated_hours"/>
<field name="assignee_estimated_hours"/>
<field name="estimated_hours"/>
<field name="show_approval_flow"/>
<field name="show_submission_button"/>
<field name="show_skip_approval_button"/>
<field name="show_approval_button"/>
<field name="show_refuse_button"/>
<field name="show_back_button"/>
<field name="timelines_requested"/>
<field name="record_paused"/>
</xpath>
</field>
</record>
<record id="project_task_list_sprint_inherit" model="ir.ui.view">
<field name="name">project.task.list.sprint.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.project_task_view_tree_main_base"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='milestone_id']" position="after">
<field name="sprint_id" optional="show" domain="[('project_id', '=', project_id)]"
context="{'default_project_id': project_id}"/>
<field name="sprint_estimated_hours" optional="show" widget="timesheet_uom_no_toggle"/>
<field name="assignee_estimated_hours" optional="hide" widget="timesheet_uom_no_toggle" readonly="1"/>
</xpath>
</field>
</record>