project task timesheets sprints and kanban buttons
This commit is contained in:
parent
fd866fb866
commit
e5e876ecb4
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 < 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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue