Merge pull request 'project internal users and roles changes' (#27) from feature/share_module into srivyn_test

Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/27
This commit is contained in:
pranay 2026-07-20 18:12:33 +05:30
commit c047dcb8c3
7 changed files with 294 additions and 34 deletions

View File

@ -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",

View File

@ -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
record.time_variance = record.total_actual_hours - record.total_estimated_hours

View File

@ -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 (

View File

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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
17 access_project_role_manager access_project_portfolio_manager project.role.manager project.portfolio.manager model_project_role model_project_portfolio project.group_project_manager 1 1 1 1
18 access_project_sprint_user access_project_portfolio_role_assignment_manager access.project.sprint.user project.portfolio.role.assignment.manager model_project_sprint model_project_portfolio_role_assignment project.group_project_user project.group_project_manager 1 1 1 1
19 access_project_sprint_manager access_project_role_manager access.project.sprint.manager project.role.manager model_project_sprint model_project_role project.group_project_manager 1 1 1 1
20 access_project_sprint_user access.project.sprint.user model_project_sprint project.group_project_user 1 1 1 1
21 access_project_architecture_design_user access_project_sprint_manager access_project_architecture_design_user access.project.sprint.manager model_project_architecture_design model_project_sprint project.group_project_user project.group_project_manager 1 1 1 1
22 access_project_architecture_design_manager access_project_architecture_design_user access_project_architecture_design_manager access_project_architecture_design_user model_project_architecture_design project.group_project_manager project.group_project_user 1 1 1 1
23 access_project_risk_user access_project_architecture_design_manager access_project_risk_user access_project_architecture_design_manager model_project_risk model_project_architecture_design project.group_project_user project.group_project_manager 1 1 1 1
24 access_project_risk_user access_project_risk_user model_project_risk project.group_project_user 1 1 1 1
25 access_project_risk_manager access_project_risk_manager model_project_risk project.group_project_manager 1 1 1 1
26 access_project_resource_cost_user project.resource.cost.user model_project_resource_cost project.group_project_user 1 1 1 1
27 access_project_resource_cost_manager project.resource.cost.manager model_project_resource_cost project.group_project_manager 1 1 1 1

View File

@ -17,12 +17,18 @@
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="project_lead" widget="many2many_avatar_user"/>
</xpath>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="user_id" widget="user_timeline"/>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="project_lead"
widget="many2many_avatar_user"
domain="[('id', 'in', allowed_project_lead_user_ids)]"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
</xpath>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="user_id"
widget="user_timeline"
domain="[('id', 'in', allowed_project_manager_user_ids)]"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
</xpath>
<xpath expr="//page[@name='settings']" position="inside">
<group>
<group name="group_project_channel_managment" string="Project Channel" col="1"
@ -85,6 +91,9 @@
<field name="enable_budget_summary" invisible="1"/>
<field name="can_edit_stage_in_approval" invisible="1"/>
<field name="active_sprint_id" invisible="1"/>
<field name="allowed_project_manager_user_ids" invisible="1"/>
<field name="allowed_project_lead_user_ids" invisible="1"/>
<field name="allowed_internal_team_ids" invisible="1"/>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<button class="oe_stat_button" type="object" name="action_open_sprint_backlog"
@ -130,7 +139,10 @@
<list editable="bottom" open_form_view="True" delete="0">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="team_id" string="Assigned Team"/>
<field name="team_id"
string="Assigned Team"
domain="[('id', 'in', parent.allowed_internal_team_ids)]"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
<field name="responsible_user_id" string="Responsible User" readonly="1"/>
<field name="approval_by" string="Approval Owner"/>
<field name="fold"/>

View File

@ -144,10 +144,33 @@
</group>
</group>
<notebook>
<page string="Projects" invisible="project_count == 0">
<field name="project_ids">
<list>
<notebook>
<page string="Internal Teams">
<field name="internal_team_ids"
widget="many2many_tags"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
</page>
<page string="Portfolio Roles">
<field name="role_assignment_ids">
<list editable="bottom">
<field name="role_id"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
<field name="master_user_ids"
widget="many2many_tags"
readonly="1"
optional="show"/>
<field name="user_ids"
widget="many2many_tags"
domain="[('id', 'in', master_user_ids)]"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
</list>
</field>
</page>
<page string="Projects" invisible="project_count == 0">
<field name="project_ids">
<list>
<field name="name"/>
<field name="partner_id"/>
<field name="user_id" widget="user_timeline"/>

View File

@ -6,7 +6,10 @@
<field name="inherit_id" ref="project.task_type_edit"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='fold']" position="after">
<field name="team_id"/>
<field name="allowed_internal_team_ids" invisible="1"/>
<field name="team_id"
domain="[('id', 'in', allowed_internal_team_ids)]"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
<field name="responsible_user_id"/>
<field name="approval_by"/>
<field name="team_related_user_ids" invisible="1"/>
@ -22,7 +25,11 @@
<field name="inherit_id" ref="project.task_type_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="team_id" optional="show"/>
<field name="allowed_internal_team_ids" invisible="1"/>
<field name="team_id"
optional="show"
domain="[('id', 'in', allowed_internal_team_ids)]"
options="{'no_create': True, 'no_create_edit': True, 'no_quick_create': True}"/>
<field name="responsible_user_id" optional="show"/>
<field name="approval_by" optional="show"/>
<field name="team_related_user_ids" invisible="1"/>