Merge pull request 'feature/share_module' (#24) from feature/share_module into srivyn_test

Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/24
This commit is contained in:
pranay 2026-07-14 18:33:19 +05:30
commit 4a8452ae5f
11 changed files with 634 additions and 105 deletions

View File

@ -117,7 +117,7 @@ def post_init_hook(env):
('project_id.privacy_visibility', '=', 'followers'),
'|',
'|',
('project_id.project_lead', '=', user.id),
('project_id.project_lead', 'in', [user.id]),
('project_id.user_id', '=', user.id),
'|',
'&',

View File

@ -7,6 +7,37 @@ import pytz
class ProjectProject(models.Model):
_inherit = 'project.project'
def init(self):
super().init()
self.env.cr.execute("""
SELECT 1
FROM information_schema.columns
WHERE table_name = 'project_project'
AND column_name = 'project_lead'
""")
if not self.env.cr.fetchone():
return
self.env.cr.execute("""
SELECT 1
FROM information_schema.tables
WHERE table_name = 'project_project_lead_rel'
""")
if not self.env.cr.fetchone():
return
self.env.cr.execute("""
INSERT INTO project_project_lead_rel (project_id, user_id)
SELECT project.id, project.project_lead
FROM project_project project
WHERE project.project_lead IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM project_project_lead_rel rel
WHERE rel.project_id = project.id
AND rel.user_id = project.project_lead
)
""")
@api.constrains('name')
def _check_duplicate_project_name(self):
@ -207,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',
@ -270,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':
@ -340,7 +431,7 @@ class ProjectProject(models.Model):
if project.user_id:
users_list.append(project.user_id.id)
if project.project_lead:
users_list.append(project.project_lead.id)
users_list.extend(project.project_lead.ids)
if project.type_ids:
for task_stage in project.type_ids:
if task_stage.team_id:
@ -1083,14 +1174,20 @@ class ProjectProject(models.Model):
return self._get_default_task_stage_templates()
project_lead = fields.Many2one("res.users", string="Project Lead",
domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_lead').user_ids.ids)])
project_lead = fields.Many2many(
"res.users",
"project_project_lead_rel",
"project_id",
"user_id",
string="Project Lead",
domain=lambda self: [('id', 'in', self.env.ref('project_task_timesheet_extended.role_project_lead').user_ids.ids)],
)
members_ids = fields.Many2many('res.users', 'project_user_rel', 'project_id',
'user_id', 'Project Members', help="""Project's
members are users who can have an access to
the tasks related to this project."""
)
user_id = fields.Many2one('res.users', string='Project Manager', default=False, tracking=True, required = True,
user_id = fields.Many2one('res.users', string='Project Manager', default=False, tracking=True,
domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_manager').user_ids.ids),('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],)
# @api.constrains('user_id')
@ -1142,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')
@ -1154,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(
@ -1187,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([
@ -1195,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,4 +1,5 @@
from odoo import fields, models
from odoo import _, fields, models
from odoo.exceptions import UserError
class ProjectRole(models.Model):
@ -29,6 +30,14 @@ class ProjectRole(models.Model):
string='Assigned Users',
help="Users assigned to this role"
)
required_groups = fields.Many2many(
'res.groups',
'project_role_required_group_rel',
'role_id',
'group_id',
string='Required Groups',
help="Groups that should be granted to the assigned users for this role"
)
active = fields.Boolean(
string='Active',
default=True,
@ -58,6 +67,32 @@ class ProjectRole(models.Model):
'context': {'default_members_ids': [(6, 0, self.user_ids.ids)],
},
}
def action_grant_access(self):
"""Grant the selected required groups to all assigned users."""
for role in self:
role.check_access('write')
if not role.user_ids:
raise UserError(_("Please assign at least one user before granting access."))
if not role.required_groups:
raise UserError(_("Please select at least one required group before granting access."))
users = role.user_ids.sudo()
groups = role.required_groups.sudo()
for user in users:
user.write({'groups_id': [(4, group.id) for group in groups]})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Access Granted'),
'message': _('Required groups were added to the assigned users.'),
'type': 'success',
'sticky': False,
}
}
def action_view_users(self):
"""Open users assigned to this role"""
self.ensure_one()

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):
_name = "project.sprint"
_description = "Project Sprint"
_order = "date_start desc"
_rec_name = "sprint_name"
_order = "date_start desc, id desc"
project_id = fields.Many2one(
"project.project",
required=True,
ondelete="cascade"
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="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(
[
@ -28,3 +38,75 @@ class ProjectSprint(models.Model):
)
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:
@ -279,7 +301,11 @@ class projectTask(models.Model):
administrative_users = self.env['project.role'].search([('role_level','=','administrative')])
first_stage = self.project_id.type_ids.sorted(key=lambda r: r.sequence)[0]
create_access_users = first_stage.team_id.team_lead + first_stage.involved_user_ids + administrative_users.user_ids
if (self.project_id.user_id.id != self.env.user.id) and (self.project_id.project_lead.id != self.env.user.id) and self.env.user.id not in list(set(create_access_users.ids)):
if (
self.project_id.user_id.id != self.env.user.id
and self.env.user not in self.project_id.project_lead
and self.env.user.id not in list(set(create_access_users.ids))
):
raise ValidationError(
"Only Project Manager/Lead can assign/remove assignees"
)
@ -538,7 +564,7 @@ class projectTask(models.Model):
create_access_users = administrative_users.user_ids
if current_user.has_group("project.group_project_manager") or current_user == task.project_id.user_id or current_user == task.project_id.project_lead or (current_user.id in list(set(create_access_users.ids)) and task.stage_id.id == first_stage.id):
if current_user.has_group("project.group_project_manager") or current_user == task.project_id.user_id or current_user in task.project_id.project_lead or (current_user.id in list(set(create_access_users.ids)) and task.stage_id.id == first_stage.id):
task.has_supervisor_access = True
@ -553,14 +579,24 @@ class projectTask(models.Model):
and (
is_admin
or current_user == task.project_id.user_id
or current_user == 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):
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):
@ -680,7 +716,7 @@ class projectTask(models.Model):
record.can_edit_approval_flow_stages = True
current_user = self.env.user
if record.project_id:
if record.project_id.user_id != current_user and record.project_id.project_lead != current_user and not current_user.has_group(
if record.project_id.user_id != current_user and current_user not in record.project_id.project_lead and not current_user.has_group(
'project.group_project_manager'):
raise UserError(_("Access denied: You do not have sufficient privileges to use this feature."))
record.record_paused = not record.record_paused
@ -721,7 +757,8 @@ class projectTask(models.Model):
approval_not_required = False
can_skip_approval_stage = (
user in task.involved_user_ids
or user in [project_manager, project_lead]
or user == project_manager
or user in project_lead
or user.has_group("project.group_project_manager")
or user.has_group("base.group_system")
)
@ -1305,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']):
@ -1845,7 +1883,7 @@ class projectTaskTimelines(models.Model):
current_user = self.env.user
task.has_edit_access = False
if current_user.has_group(
"project.group_project_manager") or current_user == task.project_id.user_id or current_user == task.responsible_lead or current_user == task.project_id.project_lead:
"project.group_project_manager") or current_user == task.project_id.user_id or current_user == task.responsible_lead or current_user in task.project_id.project_lead:
task.has_edit_access = True
@api.depends("project_id", "task_id")
@ -1863,8 +1901,11 @@ class projectTaskTimelines(models.Model):
is_first_stage = stage_sequences and task.stage_id.sequence == min(stage_sequences)
is_project_user = current_user == project.user_id
is_authorized_user = current_user in [project.user_id, project.project_lead,
record.assigned_to, record.responsible_lead]
is_authorized_user = (
current_user == project.user_id
or current_user in project.project_lead
or current_user in (record.assigned_to | record.responsible_lead)
)
can_edit = (is_first_stage or is_project_user) and is_authorized_user
record.estimated_time_readonly = not can_edit
@ -1893,13 +1934,11 @@ class projectTaskTimelines(models.Model):
project_members = rec.project_id.members_ids.filtered(lambda u: u.exists())
partners = rec.project_id.message_partner_ids.mapped('user_ids').filtered(lambda u: u.exists())
project_user = rec.project_id.user_id if rec.project_id.user_id.exists() else False
project_lead = rec.project_id.project_lead if rec.project_id.project_lead.exists() else False
all_ids = (
project_members.ids +
partners.ids +
([project_user.id] if project_user else []) +
([project_lead.id] if project_lead else [])
rec.project_id.project_lead.ids
)
rec.team_all_member_ids = list(set(all_ids))
@ -1936,5 +1975,3 @@ class projectTaskTimelines(models.Model):

View File

@ -15,7 +15,7 @@
('involved_users', 'in', user.id),
'|',
('project_id.user_id', '=', user.id),
('project_id.project_lead', '=', user.id)
('project_id.project_lead', 'in', [user.id])
]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>

View File

@ -109,7 +109,7 @@
<field name="domain_force">[
'&amp;', '&amp;', '&amp;',
('project_id', '!=', False),
('project_id.project_lead', '=', user.id),
('project_id.project_lead', 'in', [user.id]),
'|', ('is_generic', '=', True), ('is_generic', '=', False),
'|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id)
]
@ -221,7 +221,7 @@
('project_id.privacy_visibility', '=', 'followers'),
'|',
'|',
('project_id.project_lead', '=', user.id),
('project_id.project_lead', 'in', [user.id]),
('project_id.user_id', '=', user.id),
'|',
'&amp;',
@ -273,7 +273,7 @@
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
<field name="domain_force">[
'|', '|',
('project_id.project_lead', '=', user.id),
('project_id.project_lead', 'in', [user.id]),
('user_id', '=', user.id),
('project_id.user_id', '=', user.id),
]
@ -302,7 +302,7 @@
<!-- ('project_id.privacy_visibility','=','followers'),-->
<!-- ('task_id', '!=', False),-->
<!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '!=', user.id),-->
<!-- ('project_id.project_lead', 'not in', [user.id]),-->
<!-- ('project_id.user_id', '!=', user.id),-->
<!-- ('user_id','not in',[user.id]),-->
<!-- '|',-->
@ -327,7 +327,7 @@
<!-- ('project_id.privacy_visibility','!=','followers'),-->
<!-- ('task_id', '!=', False),-->
<!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '!=', user.id),-->
<!-- ('project_id.project_lead', 'not in', [user.id]),-->
<!-- ('project_id.user_id', '!=', user.id),-->
<!-- ('task_id.is_generic', '=', False),-->
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
@ -347,7 +347,7 @@
<!-- <field name="domain_force">[-->
<!-- '&amp;', '&amp;', '&amp;',-->
<!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '=', user.id),-->
<!-- ('project_id.project_lead', 'in', [user.id]),-->
<!-- '|', ('task_id.is_generic', '=', True), ('task_id.is_generic', '=', False),-->
<!-- '|', ('task_id.user_ids', 'in', user.id), ('task_id.user_ids', 'not in', user.id)-->
<!-- ]-->

View File

@ -18,7 +18,7 @@
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="project_lead" widget="user_timeline"/>
<field name="project_lead" widget="many2many_avatar_user"/>
</xpath>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="user_id" widget="user_timeline"/>
@ -84,6 +84,17 @@
<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>
@ -507,16 +518,102 @@
</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%"/>
<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"/>
<field name="note" 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">

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

@ -31,6 +31,7 @@
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_update_users" type="object" string="Modify Users" class="btn-primary"/>
<button name="action_grant_access" type="object" string="Grant Access" class="btn-secondary"/>
</div>
<div class="oe_title">
<h1>
@ -50,6 +51,9 @@
<field name="description" placeholder="Enter role description..."/>
</page>
<page string="Assigned Users">
<!-- <group>-->
<!-- <field name="required_groups" widget="many2many_tags" options="{'no_create': True, 'no_quick_create': True}"/>-->
<!-- </group>-->
<field name="user_ids" can_create="False" can_write="False">
<list create="0" edit="0" delete="0">
<field name="name"/>
@ -58,6 +62,11 @@
</list>
</field>
</page>
<page string="Required Groups">
<group>
<field name="required_groups" widget="many2many_tags" options="{'no_create': True, 'no_quick_create': True}"/>
</group>
</page>
</notebook>
</sheet>
</form>

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>