From a003e4d566b15d1d291204f9e070386acd627ce6 Mon Sep 17 00:00:00 2001 From: karuna Date: Tue, 14 Jul 2026 15:19:59 +0530 Subject: [PATCH 1/4] updation on project lead from single to many users --- .../project_task_timesheet_extended/hooks.py | 2 +- .../models/project.py | 43 +++++++++++++++++-- .../models/project_task.py | 28 +++++++----- .../security/project_stage_visibility.xml | 2 +- .../security/security.xml | 12 +++--- .../view/project.xml | 2 +- 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/addons_extensions/project_task_timesheet_extended/hooks.py b/addons_extensions/project_task_timesheet_extended/hooks.py index 7ccc66901..cc55ab674 100644 --- a/addons_extensions/project_task_timesheet_extended/hooks.py +++ b/addons_extensions/project_task_timesheet_extended/hooks.py @@ -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), '|', '&', diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py index 34bc07c7d..7c751df39 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project.py +++ b/addons_extensions/project_task_timesheet_extended/models/project.py @@ -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): @@ -340,7 +371,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,8 +1114,14 @@ 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 diff --git a/addons_extensions/project_task_timesheet_extended/models/project_task.py b/addons_extensions/project_task_timesheet_extended/models/project_task.py index ea70716d9..aef6b6633 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_task.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py @@ -279,7 +279,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 +542,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,7 +557,7 @@ 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') @@ -680,7 +684,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 +725,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") ) @@ -1845,7 +1850,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 +1868,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 +1901,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)) diff --git a/addons_extensions/project_task_timesheet_extended/security/project_stage_visibility.xml b/addons_extensions/project_task_timesheet_extended/security/project_stage_visibility.xml index 2b85be326..b8329f6ec 100644 --- a/addons_extensions/project_task_timesheet_extended/security/project_stage_visibility.xml +++ b/addons_extensions/project_task_timesheet_extended/security/project_stage_visibility.xml @@ -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]) ] diff --git a/addons_extensions/project_task_timesheet_extended/security/security.xml b/addons_extensions/project_task_timesheet_extended/security/security.xml index 644877c0c..a25b65853 100644 --- a/addons_extensions/project_task_timesheet_extended/security/security.xml +++ b/addons_extensions/project_task_timesheet_extended/security/security.xml @@ -109,7 +109,7 @@ [ '&', '&', '&', ('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), '|', '&', @@ -273,7 +273,7 @@ [ '|', '|', - ('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 @@ - + @@ -327,7 +327,7 @@ - + @@ -347,7 +347,7 @@ - + diff --git a/addons_extensions/project_task_timesheet_extended/view/project.xml b/addons_extensions/project_task_timesheet_extended/view/project.xml index de47e0371..e185bbf97 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project.xml @@ -18,7 +18,7 @@ - + From 1335c60b76ca379c56112c08b48eb7569799a9c9 Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Tue, 14 Jul 2026 15:22:48 +0530 Subject: [PATCH 2/4] project lead changes and multi company dependencies --- .../project_task_timesheet_extended/models/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py index 7c751df39..865bd99da 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project.py +++ b/addons_extensions/project_task_timesheet_extended/models/project.py @@ -1127,7 +1127,7 @@ class ProjectProject(models.Model): 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') From fd866fb866a2581f17a558f8a0ce7bc7acffaf25 Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Tue, 14 Jul 2026 16:11:54 +0530 Subject: [PATCH 3/4] project task timesheets related roles update --- .../models/project_roles_master.py | 73 ++++++++++++++----- .../view/project_roles_master.xml | 11 ++- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/addons_extensions/project_task_timesheet_extended/models/project_roles_master.py b/addons_extensions/project_task_timesheet_extended/models/project_roles_master.py index 7eca70cb9..1cac44bdf 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_roles_master.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_roles_master.py @@ -1,4 +1,5 @@ -from odoo import fields, models +from odoo import _, fields, models +from odoo.exceptions import UserError class ProjectRole(models.Model): @@ -21,14 +22,22 @@ class ProjectRole(models.Model): string='Description', help="Detailed description of the role responsibilities" ) - user_ids = fields.Many2many( - 'res.users', - 'project_role_user_rel', - 'role_id', - 'user_id', - string='Assigned Users', - help="Users assigned to this role" - ) + user_ids = fields.Many2many( + 'res.users', + 'project_role_user_rel', + 'role_id', + 'user_id', + 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, @@ -47,20 +56,46 @@ class ProjectRole(models.Model): help="Structured authority level of the role" ) - def action_update_users(self): - return { - 'type': 'ir.actions.act_window', - 'name': 'Add Users', + def action_update_users(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'Add Users', 'res_model': 'roles.user.assign.wizard', 'view_mode': 'form', 'view_id': self.env.ref('project_task_timesheet_extended.roles_user_assignment_form_view').id, 'target': 'new', 'context': {'default_members_ids': [(6, 0, self.user_ids.ids)], - }, - } - def action_view_users(self): - """Open users assigned to this role""" - self.ensure_one() + }, + } + + 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() action = { 'name': f'Users in {self.name} Role', 'type': 'ir.actions.act_window', @@ -97,4 +132,4 @@ class ProjectStage(models.Model): def _compute_user_ids(self): """Compute users from assigned roles""" for stage in self: - stage.user_ids = stage.role_ids.mapped('user_ids') \ No newline at end of file + stage.user_ids = stage.role_ids.mapped('user_ids') diff --git a/addons_extensions/project_task_timesheet_extended/view/project_roles_master.xml b/addons_extensions/project_task_timesheet_extended/view/project_roles_master.xml index 35631ade6..9bd278e02 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project_roles_master.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project_roles_master.xml @@ -31,6 +31,7 @@

@@ -50,6 +51,9 @@ + + + @@ -58,6 +62,11 @@ + + + + + @@ -202,4 +211,4 @@ - \ No newline at end of file + From e5e876ecb4cc10f54f15dac0ca0d1ecd2ff5fb20 Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Tue, 14 Jul 2026 18:30:46 +0530 Subject: [PATCH 4/4] project task timesheets sprints and kanban buttons --- .../models/project.py | 99 +++++++++++- .../models/project_sprint.py | 134 +++++++++++++---- .../models/project_task.py | 47 +++++- .../view/project.xml | 141 +++++++++++++++--- .../view/project_by_module.xml | 23 +++ .../view/project_task.xml | 120 +++++++++++++++ 6 files changed, 503 insertions(+), 61 deletions(-) diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py index 865bd99da..42e4b7bfd 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project.py +++ b/addons_extensions/project_task_timesheet_extended/models/project.py @@ -238,6 +238,27 @@ class ProjectProject(models.Model): "project_id", string="Project Sprints" ) + active_sprint_id = fields.Many2one( + "project.sprint", + compute="_compute_sprint_planning_metrics", + string="Active Sprint", + ) + sprint_count = fields.Integer( + compute="_compute_sprint_planning_metrics", + string="Sprints", + ) + sprint_backlog_task_count = fields.Integer( + compute="_compute_sprint_planning_metrics", + string="Backlog Tasks", + ) + sprint_planned_hours = fields.Float( + compute="_compute_sprint_planning_metrics", + string="Sprint Planned Hours", + ) + sprint_actual_hours = fields.Float( + compute="_compute_sprint_planning_metrics", + string="Sprint Actual Hours", + ) commit_step_ids = fields.One2many( 'project.commit.step', @@ -301,6 +322,45 @@ class ProjectProject(models.Model): hold_reason = fields.Text(string="Hold Reason", tracking=True) privacy_visibility = fields.Selection(default="followers") + @api.depends( + "sprint_ids", + "sprint_ids.status", + "sprint_ids.task_ids", + "sprint_ids.task_ids.sprint_estimated_hours", + "sprint_ids.task_ids.estimated_hours", + "sprint_ids.task_ids.actual_hours", + "task_ids.sprint_id", + ) + def _compute_sprint_planning_metrics(self): + for project in self: + active_sprint = project.sprint_ids.filtered(lambda sprint: sprint.status == "in_progress")[:1] + sprint_tasks = project.task_ids.filtered(lambda task: task.sprint_id) + project.active_sprint_id = active_sprint + project.sprint_count = len(project.sprint_ids) + project.sprint_backlog_task_count = len(project.task_ids.filtered(lambda task: not task.sprint_id)) + project.sprint_planned_hours = sum(task.sprint_estimated_hours or task.estimated_hours for task in sprint_tasks) + project.sprint_actual_hours = sum(sprint_tasks.mapped("actual_hours")) + + def action_open_sprint_backlog(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Sprint Backlog"), + "res_model": "project.task", + "view_mode": "kanban,list,form,calendar,pivot,graph,activity", + "domain": [("project_id", "=", self.id), ("sprint_id", "=", False)], + "context": { + "default_project_id": self.id, + "search_default_project_id": self.id, + }, + } + + def action_open_active_sprint_tasks(self): + self.ensure_one() + if not self.active_sprint_id: + raise UserError(_("There is no active sprint for this project.")) + return self.active_sprint_id.action_open_tasks() + def action_hold_unhold(self): for project in self: if project.project_state == 'hold': @@ -1179,8 +1239,8 @@ class ProjectTask(models.Model): def _default_sprint_id(self): """Return the current active (in-progress) sprint of the project.""" - if 'project_id' in self._context: - project_id = self._context.get('project_id') + project_id = self._context.get('default_project_id') or self._context.get('project_id') + if project_id: sprint = self.env['project.sprint'].search([ ('project_id', '=', project_id), ('status', '=', 'in_progress') @@ -1191,7 +1251,9 @@ class ProjectTask(models.Model): sprint_id = fields.Many2one( "project.sprint", string="Sprint", - default=_default_sprint_id + default=_default_sprint_id, + tracking=True, + index=True, ) require_sprint = fields.Boolean( @@ -1224,6 +1286,8 @@ class ProjectTask(models.Model): for task in self: if task.project_id and not task.project_id.require_sprint: task.sprint_id = False + elif task.sprint_id and task.sprint_id.project_id != task.project_id: + task.sprint_id = False else: if task.project_id and task.project_id.require_sprint: sprint = self.env['project.sprint'].search([ @@ -1232,10 +1296,35 @@ class ProjectTask(models.Model): ], limit=1) task.sprint_id = sprint.id + @api.onchange("sprint_id") + def _onchange_sprint_id_project(self): + for task in self: + if task.sprint_id and not task.project_id: + task.project_id = task.sprint_id.project_id + + @api.constrains("project_id", "sprint_id") + def _check_sprint_project(self): + for task in self: + if task.sprint_id and task.project_id and task.sprint_id.project_id != task.project_id: + raise ValidationError(_("Task sprint must belong to the same project as the task.")) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + project_id = vals.get("project_id") or self._context.get("default_project_id") + if project_id and "sprint_id" not in vals: + project = self.env["project.project"].browse(project_id) + if project.require_sprint: + sprint = self.env["project.sprint"].search([ + ("project_id", "=", project.id), + ("status", "=", "in_progress"), + ], limit=1) + if sprint: + vals["sprint_id"] = sprint.id + return super().create(vals_list) + def action_show_project_task_chatter(self): """Toggle visibility of project chatter""" for project in self: project.show_task_chatter = not project.show_task_chatter - - diff --git a/addons_extensions/project_task_timesheet_extended/models/project_sprint.py b/addons_extensions/project_task_timesheet_extended/models/project_sprint.py index 4c12e7bae..c026fc86d 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_sprint.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_sprint.py @@ -1,30 +1,112 @@ -from odoo import models, fields, api, _ - -class ProjectSprint(models.Model): - _name = "project.sprint" - _description = "Project Sprint" - _order = "date_start desc" - - project_id = fields.Many2one( - "project.project", - required=True, - ondelete="cascade" - ) - - sprint_name = fields.Char(string="Sprint Name", required=True) - date_start = fields.Date(string="Start Date", required=True) - date_end = fields.Date(string="End Date", required=True) - - allocated_hours = fields.Float(string="Allocated Hours") - - status = fields.Selection( - [ - ("draft", "Draft"), - ("in_progress", "In Progress"), +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError, UserError + +class ProjectSprint(models.Model): + _name = "project.sprint" + _description = "Project Sprint" + _rec_name = "sprint_name" + _order = "date_start desc, id desc" + + project_id = fields.Many2one( + "project.project", + required=True, + ondelete="cascade", + index=True, + ) + + sprint_name = fields.Char(string="Sprint Name", required=True) + date_start = fields.Date(string="Start Date", required=True) + date_end = fields.Date(string="End Date", required=True) + + allocated_hours = fields.Float(string="Capacity Hours") + task_ids = fields.One2many("project.task", "sprint_id", string="Sprint Tasks") + task_count = fields.Integer(compute="_compute_sprint_metrics", string="Tasks") + completed_task_count = fields.Integer(compute="_compute_sprint_metrics", string="Completed Tasks") + planned_hours = fields.Float(compute="_compute_sprint_metrics", string="Planned Hours") + actual_hours = fields.Float(compute="_compute_sprint_metrics", string="Actual Hours") + remaining_hours = fields.Float(compute="_compute_sprint_metrics", string="Remaining Hours") + progress = fields.Float(compute="_compute_sprint_metrics", string="Progress") + + status = fields.Selection( + [ + ("draft", "Draft"), + ("in_progress", "In Progress"), ("done", "Completed"), ], default="draft", string="Status" - ) - done_date = fields.Date(string="Done Date") - note = fields.Text(string="Note") + ) + done_date = fields.Date(string="Done Date") + note = fields.Text(string="Note") + + @api.depends( + "allocated_hours", + "task_ids", + "task_ids.state", + "task_ids.sprint_estimated_hours", + "task_ids.estimated_hours", + "task_ids.actual_hours", + ) + def _compute_sprint_metrics(self): + done_states = ("1_done", "1_canceled") + for sprint in self: + tasks = sprint.task_ids + sprint.task_count = len(tasks) + sprint.completed_task_count = len(tasks.filtered(lambda task: task.state in done_states)) + sprint.planned_hours = sum(task.sprint_estimated_hours or task.estimated_hours for task in tasks) + sprint.actual_hours = sum(tasks.mapped("actual_hours")) + sprint.remaining_hours = sprint.allocated_hours - sprint.planned_hours + sprint.progress = (sprint.completed_task_count / sprint.task_count * 100.0) if sprint.task_count else 0.0 + + @api.constrains("date_start", "date_end") + def _check_sprint_dates(self): + for sprint in self: + if sprint.date_start and sprint.date_end and sprint.date_start > sprint.date_end: + raise ValidationError(_("Sprint start date must be before the end date.")) + + @api.constrains("project_id", "status") + def _check_single_active_sprint(self): + for sprint in self.filtered(lambda rec: rec.status == "in_progress"): + active_sprint = self.search([ + ("project_id", "=", sprint.project_id.id), + ("status", "=", "in_progress"), + ("id", "!=", sprint.id), + ], limit=1) + if active_sprint: + raise ValidationError(_( + "Only one sprint can be in progress per project. " + "Please complete '%s' before starting another sprint." + ) % active_sprint.display_name) + + def action_start_sprint(self): + for sprint in self: + if not sprint.task_ids: + raise UserError(_("Add at least one task before starting the sprint.")) + sprint.status = "in_progress" + return True + + def action_complete_sprint(self): + open_tasks = self.mapped("task_ids").filtered(lambda task: task.state not in ("1_done", "1_canceled")) + if open_tasks: + raise UserError(_("Complete or cancel all sprint tasks before closing the sprint.")) + self.write({"status": "done", "done_date": fields.Date.context_today(self)}) + return True + + def action_reset_to_draft(self): + self.write({"status": "draft", "done_date": False}) + return True + + def action_open_tasks(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Sprint Tasks"), + "res_model": "project.task", + "view_mode": "kanban,list,form,calendar,pivot,graph,activity", + "domain": [("sprint_id", "=", self.id)], + "context": { + "default_project_id": self.project_id.id, + "default_sprint_id": self.id, + "search_default_project_id": self.project_id.id, + }, + } diff --git a/addons_extensions/project_task_timesheet_extended/models/project_task.py b/addons_extensions/project_task_timesheet_extended/models/project_task.py index aef6b6633..8f6cabc4e 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_task.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py @@ -28,6 +28,17 @@ class projectTask(models.Model): _rec_name = 'name' user_ids = fields.Many2many() + is_project_admin = fields.Boolean( + compute="_compute_is_project_admin" + ) + + @api.depends("project_id","project_id.user_id") + def _compute_is_project_admin(self): + project_manager = self.project_id.user_id + is_admin = self.env.user.has_group("project.group_project_manager") or self.env.user.has_group("base.group_system") or (self.env.user.id == project_manager.id) + # Replace the XML ID if your administrator group is different. + for rec in self: + rec.is_project_admin = is_admin involved_user_ids = fields.Many2many( 'res.users', @@ -132,6 +143,17 @@ class projectTask(models.Model): store=True, readonly=False ) + sprint_estimated_hours = fields.Float( + string="Sprint Estimated Hours", + tracking=True, + help="Planning estimate committed during sprint planning. This is kept separate from assignee timeline estimates." + ) + assignee_estimated_hours = fields.Float( + string="Assignee Estimated Hours", + compute="_compute_assignee_estimated_hours", + store=True, + help="Estimate calculated from the assignee timelines." + ) def write(self, vals): for rec in self: @@ -560,11 +582,21 @@ class projectTask(models.Model): or current_user in task.project_id.project_lead ) ) - @api.depends('assignees_timelines.estimated_time', 'show_approval_flow') + @api.depends('assignees_timelines.estimated_time') + def _compute_assignee_estimated_hours(self): + for task in self: + task.assignee_estimated_hours = sum(task.assignees_timelines.mapped('estimated_time')) + + @api.depends('assignees_timelines.estimated_time', 'show_approval_flow', 'sprint_estimated_hours') def _compute_estimated_hours(self): for task in self: - if task.show_approval_flow: - task.estimated_hours = sum(task.assignees_timelines.mapped('estimated_time')) + assignee_estimate = sum(task.assignees_timelines.mapped('estimated_time')) + if task.show_approval_flow and assignee_estimate: + task.estimated_hours = assignee_estimate + elif task.sprint_estimated_hours: + task.estimated_hours = task.sprint_estimated_hours + else: + task.estimated_hours = task.estimated_hours or 0.0 @api.depends('timesheet_ids.unit_amount') def _compute_actual_hours(self): @@ -1310,9 +1342,10 @@ class projectTask(models.Model): for task in self: # If user is not allowed to change stage if task.show_approval_flow and not task.can_edit_approval_flow_stages and not self.env.user.has_group("project.group_project_manager"): - raise UserError(_( - "You are not allowed to change the stage of this task because stage editing is restricted." - )) + if not task.is_project_admin: + raise UserError(_( + "You are not allowed to change the stage of this task because stage editing is restricted." + )) result = super(projectTask, self).write(vals) if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']): @@ -1942,5 +1975,3 @@ class projectTaskTimelines(models.Model): - - diff --git a/addons_extensions/project_task_timesheet_extended/view/project.xml b/addons_extensions/project_task_timesheet_extended/view/project.xml index e185bbf97..543dbe13a 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project.xml @@ -72,19 +72,30 @@ invisible="not assign_approval_flow or not show_back_button or not begin_approval_processing"/> - - - - - + + + + + - - - - + + + + + + + + + [('id', 'in', showable_stage_ids)] @@ -506,19 +517,105 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ diff --git a/addons_extensions/project_task_timesheet_extended/view/project_by_module.xml b/addons_extensions/project_task_timesheet_extended/view/project_by_module.xml index 69805958a..f2081893a 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project_by_module.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project_by_module.xml @@ -69,6 +69,29 @@ + + project.task.search.project.by.mytasks + project.task + + + + + + + + + + + + + + + + + + + + diff --git a/addons_extensions/project_task_timesheet_extended/view/project_task.xml b/addons_extensions/project_task_timesheet_extended/view/project_task.xml index 929ee2850..b94804679 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project_task.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project_task.xml @@ -27,6 +27,7 @@
@@ -257,6 +327,18 @@ + + + + + + + + + + + +
@@ -271,6 +353,18 @@ + + + + + + + + + + + +
@@ -285,6 +379,32 @@ + + + + + + + + + + + + + +
+ + + + project.task.list.sprint.inherit + project.task + + + + + +