From 5bce5cca2ba0c07625ece4bc70b848e079fe8990 Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Mon, 20 Jul 2026 18:11:06 +0530 Subject: [PATCH] project internal users and roles changes --- .../models/project.py | 126 ++++++++++++++++++ .../models/project_portfolio.py | 111 ++++++++++++--- .../models/task_stages.py | 21 +++ .../security/ir.model.access.csv | 2 + .../view/project.xml | 26 +++- .../view/project_portfolio.xml | 31 ++++- .../view/task_stages.xml | 11 +- 7 files changed, 294 insertions(+), 34 deletions(-) diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py index 42e4b7bfd..698617fab 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project.py +++ b/addons_extensions/project_task_timesheet_extended/models/project.py @@ -80,6 +80,21 @@ class ProjectProject(models.Model): project_stages = fields.One2many('project.stages.approval.flow', 'project_id') assign_approval_flow = fields.Boolean(default=False) project_sponsor = fields.Many2one('res.users') + allowed_project_manager_user_ids = fields.Many2many( + 'res.users', + compute='_compute_portfolio_allowed_users', + string='Allowed Project Managers', + ) + allowed_project_lead_user_ids = fields.Many2many( + 'res.users', + compute='_compute_portfolio_allowed_users', + string='Allowed Project Leads', + ) + allowed_internal_team_ids = fields.Many2many( + 'internal.teams', + compute='_compute_portfolio_allowed_teams', + string='Allowed Internal Teams', + ) show_project_chatter = fields.Boolean(default=False) project_vision = fields.Text( string="Project Vision", @@ -1173,6 +1188,117 @@ class ProjectProject(models.Model): def _default_type_ids(self): return self._get_default_task_stage_templates() + def _get_role_users_for_portfolio(self, role_xmlid): + self.ensure_one() + role = self.env.ref(role_xmlid, raise_if_not_found=False) + if not role: + return self.env['res.users'] + if self.portfolio_id: + assignment = self.portfolio_id.role_assignment_ids.filtered( + lambda line: line.role_id == role + )[:1] + if assignment: + return assignment.user_ids + return role.user_ids + + def _get_portfolio_role_assignment(self, role_xmlid): + self.ensure_one() + role = self.env.ref(role_xmlid, raise_if_not_found=False) + if not role or not self.portfolio_id: + return self.env['project.portfolio.role.assignment'] + return self.portfolio_id.role_assignment_ids.filtered( + lambda line: line.role_id == role + )[:1] + + @api.depends( + 'portfolio_id', + 'portfolio_id.role_assignment_ids.role_id', + 'portfolio_id.role_assignment_ids.user_ids', + ) + def _compute_portfolio_allowed_users(self): + manager_groups = ( + self.env.ref('project.group_project_manager') + | self.env.ref('project_task_timesheet_extended.group_project_supervisor') + ) + for project in self: + manager_users = project._get_role_users_for_portfolio( + 'project_task_timesheet_extended.role_project_manager' + ) + project.allowed_project_manager_user_ids = manager_users.filtered( + lambda user: not user.share and bool(user.groups_id & manager_groups) + ) + project.allowed_project_lead_user_ids = project._get_role_users_for_portfolio( + 'project_task_timesheet_extended.role_project_lead' + ) + + @api.depends('portfolio_id', 'portfolio_id.internal_team_ids') + def _compute_portfolio_allowed_teams(self): + Team = self.env['internal.teams'] + fallback_teams = Team.search([('active', '=', True)]) + for project in self: + project.allowed_internal_team_ids = ( + project.portfolio_id.internal_team_ids + if project.portfolio_id and project.portfolio_id.internal_team_ids + else fallback_teams + ) + + @api.onchange('portfolio_id') + def _onchange_portfolio_id_scope_assignments(self): + for project in self: + manager_assignment = project._get_portfolio_role_assignment( + 'project_task_timesheet_extended.role_project_manager' + ) + lead_assignment = project._get_portfolio_role_assignment( + 'project_task_timesheet_extended.role_project_lead' + ) + if ( + manager_assignment + and project.user_id + and project.user_id not in project.allowed_project_manager_user_ids + ): + project.user_id = False + if lead_assignment and project.project_lead: + project.project_lead &= project.allowed_project_lead_user_ids + if project.portfolio_id.internal_team_ids: + for task_stage in project.type_ids: + if task_stage.team_id and task_stage.team_id not in project.allowed_internal_team_ids: + task_stage.team_id = False + task_stage.involved_user_ids = [Command.clear()] + + @api.constrains('portfolio_id', 'user_id', 'project_lead') + def _check_portfolio_role_users(self): + for project in self: + manager_assignment = project._get_portfolio_role_assignment( + 'project_task_timesheet_extended.role_project_manager' + ) + lead_assignment = project._get_portfolio_role_assignment( + 'project_task_timesheet_extended.role_project_lead' + ) + if ( + manager_assignment + and project.user_id + and project.user_id not in project.allowed_project_manager_user_ids + ): + raise ValidationError(_( + 'The project manager must be one of the users selected for the Project Manager role on this portfolio.' + )) + if lead_assignment and project.project_lead - project.allowed_project_lead_user_ids: + raise ValidationError(_( + 'Project leads must be selected from the Project Lead users configured on this portfolio.' + )) + + @api.constrains('portfolio_id', 'type_ids', 'type_ids.team_id') + def _check_portfolio_internal_teams(self): + for project in self: + if not project.portfolio_id.internal_team_ids: + continue + invalid_stages = project.type_ids.filtered( + lambda stage: stage.team_id and stage.team_id not in project.allowed_internal_team_ids + ) + if invalid_stages: + raise ValidationError(_( + 'Task stage teams must be selected from the internal teams configured on this portfolio.' + )) project_lead = fields.Many2many( "res.users", diff --git a/addons_extensions/project_task_timesheet_extended/models/project_portfolio.py b/addons_extensions/project_task_timesheet_extended/models/project_portfolio.py index ced4172b9..3ac044e98 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_portfolio.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_portfolio.py @@ -1,6 +1,7 @@ -from odoo import models, fields, api, _ -from datetime import date, timedelta -import json +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +from datetime import date, timedelta +import json class ProjectPortfolio(models.Model): @@ -23,11 +24,25 @@ class ProjectPortfolio(models.Model): description = fields.Text(string='Description') - project_ids = fields.One2many( - 'project.project', - 'portfolio_id', - string='Projects' - ) + project_ids = fields.One2many( + 'project.project', + 'portfolio_id', + string='Projects' + ) + internal_team_ids = fields.Many2many( + 'internal.teams', + 'project_portfolio_internal_team_rel', + 'portfolio_id', + 'team_id', + string='Internal Teams', + help='Internal teams available for projects linked to this portfolio.' + ) + role_assignment_ids = fields.One2many( + 'project.portfolio.role.assignment', + 'portfolio_id', + string='Role Assignments', + help='Portfolio-specific users available for each project role.' + ) company_id = fields.Many2one( 'res.company', @@ -533,19 +548,73 @@ class ProjectPortfolio(models.Model): } -class Project(models.Model): - _inherit = 'project.project' - - portfolio_id = fields.Many2one( - 'project.portfolio', - string='Portfolio' - ) - - - -class ProjectPortfolioEmployeePerformance(models.Model): +class Project(models.Model): + _inherit = 'project.project' + + portfolio_id = fields.Many2one( + 'project.portfolio', + string='Portfolio' + ) + + +class ProjectPortfolioRoleAssignment(models.Model): + _name = 'project.portfolio.role.assignment' + _description = 'Project Portfolio Role Assignment' + _order = 'role_id' + + portfolio_id = fields.Many2one( + 'project.portfolio', + string='Portfolio', + required=True, + ondelete='cascade', + ) + role_id = fields.Many2one( + 'project.role', + string='Role', + required=True, + ondelete='cascade', + ) + master_user_ids = fields.Many2many( + 'res.users', + related='role_id.user_ids', + string='Master Assigned Users', + readonly=True, + ) + user_ids = fields.Many2many( + 'res.users', + 'project_portfolio_role_user_rel', + 'assignment_id', + 'user_id', + string='Portfolio Users', + domain="[('id', 'in', master_user_ids)]", + help='Users selected for this role in this portfolio.' + ) + + _sql_constraints = [ + ( + 'portfolio_role_unique', + 'unique(portfolio_id, role_id)', + 'Each role can only be configured once per portfolio.', + ), + ] + + @api.onchange('role_id') + def _onchange_role_id(self): + for assignment in self: + assignment.user_ids &= assignment.role_id.user_ids + + @api.constrains('role_id', 'user_ids') + def _check_user_ids_in_role_master(self): + for assignment in self: + if assignment.user_ids - assignment.role_id.user_ids: + raise ValidationError(_( + 'Portfolio users must be selected from the users assigned on the role master.' + )) + + +class ProjectPortfolioEmployeePerformance(models.Model): _name = 'project.portfolio.employee.performance' - _description = 'Project Portfolio Employee Performance' + _description = 'Project Portfolio Employee Performance' _order = 'time_variance_percent desc' portfolio_id = fields.Many2one( @@ -633,4 +702,4 @@ class ProjectPortfolioEmployeePerformance(models.Model): @api.depends('total_estimated_hours', 'total_actual_hours') def _compute_time_variance(self): for record in self: - record.time_variance = record.total_actual_hours - record.total_estimated_hours \ No newline at end of file + record.time_variance = record.total_actual_hours - record.total_estimated_hours diff --git a/addons_extensions/project_task_timesheet_extended/models/task_stages.py b/addons_extensions/project_task_timesheet_extended/models/task_stages.py index 655d6ac8e..f0e8aee70 100644 --- a/addons_extensions/project_task_timesheet_extended/models/task_stages.py +++ b/addons_extensions/project_task_timesheet_extended/models/task_stages.py @@ -21,6 +21,11 @@ class TaskStages(models.Model): string='Team Users', compute='_compute_team_related_user_ids', ) + allowed_internal_team_ids = fields.Many2many( + 'internal.teams', + compute='_compute_allowed_internal_team_ids', + string='Allowed Internal Teams', + ) is_workflow_template = fields.Boolean( string='Workflow Template Stage', default=False, @@ -42,6 +47,22 @@ class TaskStages(models.Model): for rec in self: rec.team_related_user_ids = rec.team_id.all_members_ids | rec.team_id.team_lead + @api.depends('project_ids', 'project_ids.portfolio_id', 'project_ids.portfolio_id.internal_team_ids') + @api.depends_context('project_stage_project_id', 'default_project_id') + def _compute_allowed_internal_team_ids(self): + Team = self.env['internal.teams'] + fallback_teams = Team.search([('active', '=', True)]) + context_project = self.env['project.project'].browse( + self.env.context.get('project_stage_project_id') or self.env.context.get('default_project_id') + ) + for rec in self: + project = context_project or rec.project_ids[:1] + rec.allowed_internal_team_ids = ( + project.portfolio_id.internal_team_ids + if project and project.portfolio_id.internal_team_ids + else fallback_teams + ) + def _is_project_owned_stage(self, project): self.ensure_one() return ( diff --git a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv index 7ad8b014d..1b15496be 100644 --- a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv +++ b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv @@ -17,9 +17,11 @@ access_project_cancel_hold_wizard_supervisor,access.project.cancel.hold.wizard.s access_project_cancel_hold_wizard_manager,access.project.cancel.hold.wizard.manager,model_project_cancel_hold_wizard,project.group_project_manager,1,1,1,1 access_project_role_user,project.role.user,model_project_role,base.group_user,1,0,0,0 +access_project_portfolio_role_assignment_user,project.portfolio.role.assignment.user,model_project_portfolio_role_assignment,base.group_user,1,1,1,0 access_project_portfolio_user,project.portfolio.user,model_project_portfolio,base.group_user,1,1,1,0 access_project_portfolio_manager,project.portfolio.manager,model_project_portfolio,project.group_project_manager,1,1,1,1 +access_project_portfolio_role_assignment_manager,project.portfolio.role.assignment.manager,model_project_portfolio_role_assignment,project.group_project_manager,1,1,1,1 access_project_role_manager,project.role.manager,model_project_role,project.group_project_manager,1,1,1,1 diff --git a/addons_extensions/project_task_timesheet_extended/view/project.xml b/addons_extensions/project_task_timesheet_extended/view/project.xml index 543dbe13a..e905f1cae 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project.xml @@ -17,12 +17,18 @@ 0 - - - - - - + + + + + + + + +