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
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+