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", "project_id",
string="Project Sprints" 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( commit_step_ids = fields.One2many(
'project.commit.step', 'project.commit.step',
@ -301,6 +322,45 @@ class ProjectProject(models.Model):
hold_reason = fields.Text(string="Hold Reason", tracking=True) hold_reason = fields.Text(string="Hold Reason", tracking=True)
privacy_visibility = fields.Selection(default="followers") 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): def action_hold_unhold(self):
for project in self: for project in self:
if project.project_state == 'hold': if project.project_state == 'hold':
@ -1179,8 +1239,8 @@ class ProjectTask(models.Model):
def _default_sprint_id(self): def _default_sprint_id(self):
"""Return the current active (in-progress) sprint of the project.""" """Return the current active (in-progress) sprint of the project."""
if 'project_id' in self._context: project_id = self._context.get('default_project_id') or self._context.get('project_id')
project_id = self._context.get('project_id') if project_id:
sprint = self.env['project.sprint'].search([ sprint = self.env['project.sprint'].search([
('project_id', '=', project_id), ('project_id', '=', project_id),
('status', '=', 'in_progress') ('status', '=', 'in_progress')
@ -1191,7 +1251,9 @@ class ProjectTask(models.Model):
sprint_id = fields.Many2one( sprint_id = fields.Many2one(
"project.sprint", "project.sprint",
string="Sprint", string="Sprint",
default=_default_sprint_id default=_default_sprint_id,
tracking=True,
index=True,
) )
require_sprint = fields.Boolean( require_sprint = fields.Boolean(
@ -1224,6 +1286,8 @@ class ProjectTask(models.Model):
for task in self: for task in self:
if task.project_id and not task.project_id.require_sprint: if task.project_id and not task.project_id.require_sprint:
task.sprint_id = False task.sprint_id = False
elif task.sprint_id and task.sprint_id.project_id != task.project_id:
task.sprint_id = False
else: else:
if task.project_id and task.project_id.require_sprint: if task.project_id and task.project_id.require_sprint:
sprint = self.env['project.sprint'].search([ sprint = self.env['project.sprint'].search([
@ -1232,10 +1296,35 @@ class ProjectTask(models.Model):
], limit=1) ], limit=1)
task.sprint_id = sprint.id 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): def action_show_project_task_chatter(self):
"""Toggle visibility of project chatter""" """Toggle visibility of project chatter"""
for project in self: for project in self:
project.show_task_chatter = not project.show_task_chatter project.show_task_chatter = not project.show_task_chatter

View File

@ -1,21 +1,31 @@
from odoo import models, fields, api, _ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
class ProjectSprint(models.Model): class ProjectSprint(models.Model):
_name = "project.sprint" _name = "project.sprint"
_description = "Project Sprint" _description = "Project Sprint"
_order = "date_start desc" _rec_name = "sprint_name"
_order = "date_start desc, id desc"
project_id = fields.Many2one( project_id = fields.Many2one(
"project.project", "project.project",
required=True, required=True,
ondelete="cascade" ondelete="cascade",
index=True,
) )
sprint_name = fields.Char(string="Sprint Name", required=True) sprint_name = fields.Char(string="Sprint Name", required=True)
date_start = fields.Date(string="Start Date", required=True) date_start = fields.Date(string="Start Date", required=True)
date_end = fields.Date(string="End Date", required=True) date_end = fields.Date(string="End Date", required=True)
allocated_hours = fields.Float(string="Allocated Hours") 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( status = fields.Selection(
[ [
@ -28,3 +38,75 @@ class ProjectSprint(models.Model):
) )
done_date = fields.Date(string="Done Date") done_date = fields.Date(string="Done Date")
note = fields.Text(string="Note") 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' _rec_name = 'name'
user_ids = fields.Many2many() 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( involved_user_ids = fields.Many2many(
'res.users', 'res.users',
@ -132,6 +143,17 @@ class projectTask(models.Model):
store=True, store=True,
readonly=False 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): def write(self, vals):
for rec in self: for rec in self:
@ -560,11 +582,21 @@ class projectTask(models.Model):
or current_user in task.project_id.project_lead 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): def _compute_estimated_hours(self):
for task in self: for task in self:
if task.show_approval_flow: assignee_estimate = sum(task.assignees_timelines.mapped('estimated_time'))
task.estimated_hours = 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') @api.depends('timesheet_ids.unit_amount')
def _compute_actual_hours(self): def _compute_actual_hours(self):
@ -1310,6 +1342,7 @@ class projectTask(models.Model):
for task in self: for task in self:
# If user is not allowed to change stage # 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"): if task.show_approval_flow and not task.can_edit_approval_flow_stages and not self.env.user.has_group("project.group_project_manager"):
if not task.is_project_admin:
raise UserError(_( raise UserError(_(
"You are not allowed to change the stage of this task because stage editing is restricted." "You are not allowed to change the stage of this task because stage editing is restricted."
)) ))
@ -1942,5 +1975,3 @@ class projectTaskTimelines(models.Model):

View File

@ -84,6 +84,17 @@
<field name="is_project_editor" invisible="1"/> <field name="is_project_editor" invisible="1"/>
<field name="enable_budget_summary" invisible="1"/> <field name="enable_budget_summary" invisible="1"/>
<field name="can_edit_stage_in_approval" 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>
<xpath expr="//field[@name='stage_id']" position="attributes"> <xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute> <attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute>
@ -507,16 +518,102 @@
</page> </page>
<page name="project_sprints" string="Sprints" invisible="not require_sprint"> <page name="project_sprints" string="Sprints" invisible="not require_sprint">
<field name="sprint_ids"> <group string="Sprint Planning">
<list editable="bottom"> <group>
<field name="sprint_name" width="20%"/> <field name="active_sprint_id" readonly="1"/>
<field name="date_start" width="20%"/> <field name="sprint_count" readonly="1"/>
<field name="date_end" width="20%"/> <field name="sprint_backlog_task_count" readonly="1"/>
<field name="allocated_hours" width="20%"/> </group>
<field name="status" width="20%"/> <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"/> <field name="done_date" optional="hide"/>
<field name="note" optional="hide"/>
</list> </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> </field>
</page> </page>
<page name="development" string="Development Details" invisible="not is_development_user"> <page name="development" string="Development Details" invisible="not is_development_user">

View File

@ -69,6 +69,29 @@
</field> </field>
</record> </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"> <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"> <xpath expr="//field[@name='user_ids']" position="replace">
<label for="user_ids" string="Assignees"/> <label for="user_ids" string="Assignees"/>
<div class="o_task_assignees_inline"> <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" 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"/> <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"/> <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="involved_user_ids" widget="many2many_avatar_user" invisible="is_generic"/>
<field name="is_generic" readonly="not has_supervisor_access"/> <field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/> <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="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>
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">--> <!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>--> <!-- <field name="estimated_hours"/>-->
@ -168,6 +176,8 @@
<!-- </div>--> <!-- </div>-->
<field name="estimated_hours" widget="timesheet_uom_no_toggle" <field name="estimated_hours" widget="timesheet_uom_no_toggle"
readonly="show_approval_flow and timelines_requested"/> 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="actual_hours" widget="timesheet_uom_no_toggle"/>
<field name="is_suggested_deadline_warning" /> <field name="is_suggested_deadline_warning" />
</xpath> </xpath>
@ -215,6 +225,13 @@
<filter name="task_approved" string="Approved" domain="[('approval_status', '=', 'approved')]"/> <filter name="task_approved" string="Approved" domain="[('approval_status', '=', 'approved')]"/>
<filter name="task_rejected" string="Rejected" domain="[('approval_status', '=', 'refused')]"/> <filter name="task_rejected" string="Rejected" domain="[('approval_status', '=', 'refused')]"/>
</xpath> </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"/>--> <!-- <xpath expr="//searchpanel" position="replace"/>-->
</field> </field>
</record> </record>
@ -228,6 +245,19 @@
<xpath expr="//field[@name='subtask_count']" position="after"> <xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/> <field name="task_display_id"/>
<field name="module_display_name"/> <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>
<xpath expr="//t[@t-name='card']//field[@name='name']" position="after"> <xpath expr="//t[@t-name='card']//field[@name='name']" position="after">
<div class="text-muted mt-1 small"> <div class="text-muted mt-1 small">
@ -238,6 +268,46 @@
<span>Module: </span> <span>Module: </span>
<field name="module_display_name"/> <field name="module_display_name"/>
</div> </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> </xpath>
</field> </field>
</record> </record>
@ -257,6 +327,18 @@
<xpath expr="//field[@name='subtask_count']" position="after"> <xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/> <field name="task_display_id"/>
<field name="module_display_name"/> <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> </xpath>
</field> </field>
</record> </record>
@ -271,6 +353,18 @@
<xpath expr="//field[@name='subtask_count']" position="after"> <xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/> <field name="task_display_id"/>
<field name="module_display_name"/> <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> </xpath>
</field> </field>
</record> </record>
@ -285,6 +379,32 @@
<xpath expr="//field[@name='subtask_count']" position="after"> <xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/> <field name="task_display_id"/>
<field name="module_display_name"/> <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> </xpath>
</field> </field>
</record> </record>