from odoo import Command, api, fields, models, _ from odoo.exceptions import ValidationError class ProjectProject(models.Model): _inherit = 'project.project' team_line_ids = fields.One2many( 'project.team.line', 'project_id', string="Team Details" ) can_manage_team_lines = fields.Boolean( compute='_compute_can_manage_team_lines', string='Can Manage Team Lines' ) @api.depends('user_id', 'project_lead') def _compute_can_manage_team_lines(self): current_user = self.env.user for project in self: project.can_manage_team_lines = bool( self.env.is_superuser() or project.user_id == current_user or ('project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager")) ) @api.onchange('team_line_ids') def _onchange_team_line_ids(self): for project in self: users = project.team_line_ids.mapped('user_id') project.members_ids = [(6, 0, users.ids)] def _sync_members_from_team_lines(self): if self.env.context.get('skip_project_team_member_sync'): return for project in self: users = project.team_line_ids.mapped('user_id') if set(project.members_ids.ids) != set(users.ids): project.with_context(skip_project_team_member_sync=True).sudo().write({ 'members_ids': [Command.set(users.ids)], }) def _sync_team_lines_from_members(self): if self.env.context.get('skip_project_team_member_sync'): return TeamLine = self.env['project.team.line'].sudo().with_context(skip_project_team_member_sync=True) for project in self.sudo(): member_ids = set(project.members_ids.ids) kept_user_ids = set() lines_to_remove = self.env['project.team.line'].sudo() for line in project.team_line_ids.sorted('id'): user_id = line.user_id.id if not user_id or user_id not in member_ids or user_id in kept_user_ids: lines_to_remove |= line else: kept_user_ids.add(user_id) if lines_to_remove: lines_to_remove.with_context(skip_project_team_member_sync=True).unlink() for user_id in member_ids - kept_user_ids: TeamLine.create({ 'project_id': project.id, 'user_id': user_id, }) @api.model def _sync_all_team_lines_from_members(self): self.search([])._sync_team_lines_from_members() return True @api.model_create_multi def create(self, vals_list): projects = super().create(vals_list) for project, vals in zip(projects, vals_list): if 'team_line_ids' in vals: project._sync_members_from_team_lines() elif 'members_ids' in vals: project._sync_team_lines_from_members() return projects def write(self, vals): res = super().write(vals) if 'team_line_ids' in vals: self._sync_members_from_team_lines() elif 'members_ids' in vals: self._sync_team_lines_from_members() return res class ProjectTeamLine(models.Model): _name = 'project.team.line' _description = 'Project Team Line' _rec_name = 'project_id' project_id = fields.Many2one('project.project', ondelete='cascade') user_id = fields.Many2one('res.users') employee_id = fields.Many2one( 'hr.employee', compute="_compute_employee", store=True ) job_id = fields.Many2one( 'hr.job', related='employee_id.job_id', store=True ) start_date = fields.Date() end_date = fields.Date() status = fields.Selection([ ('not_started', 'Not Started'), ('in_progress', 'In Progress'), ('done', 'Completed') ], compute='_compute_status', inverse='_inverse_status', store=True, readonly=False) can_edit_assignment = fields.Boolean( compute='_compute_can_edit_assignment', string='Can Edit Assignment' ) # ------------------------ # COMPUTE EMPLOYEE # ------------------------ @api.depends('user_id') def _compute_employee(self): for rec in self: rec.employee_id = self.env['hr.employee'].search([ ('user_id', '=', rec.user_id.id) ], limit=1) @api.depends('start_date', 'end_date') def _compute_status(self): today = fields.Date.context_today(self) for rec in self: if rec.end_date and rec.end_date < today: rec.status = 'done' elif rec.start_date and rec.start_date > today: rec.status = 'not_started' else: rec.status = 'in_progress' @api.depends('project_id.user_id', 'project_id.project_lead') def _compute_can_edit_assignment(self): current_user = self.env.user for rec in self: project = rec.project_id rec.can_edit_assignment = bool( self.env.is_superuser() or (project and project.user_id == current_user) or (project and 'project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager")) ) def _inverse_status(self): # Allow manual edits to the stored computed field. # When start/end dates change later, compute will refresh it again. return True def _check_manager_access(self): if self.env.is_superuser(): return unauthorized = self.filtered(lambda rec: not rec.can_edit_assignment) if unauthorized: raise ValidationError(_("Only the related project manager can update team assignment dates or status.")) # ------------------------ # SYNC BENCH # ------------------------ def _sync_bench(self): # Bench data is read live from SQL view / computed fields, # so there is no separate sync model to refresh here. return True # ------------------------ # CREATE # ------------------------ @api.model_create_multi def create(self, vals_list): if not self.env.is_superuser(): for vals in vals_list: project_id = vals.get('project_id') if project_id: project = self.env['project.project'].browse(project_id) if not ( project.user_id == self.env.user or ('project_lead' in project._fields and project.project_lead == self.env.user) ): raise ValidationError(_("Only the related project manager can add team assignments.")) records = super().create(vals_list) records._sync_bench() records.mapped('project_id')._sync_members_from_team_lines() return records # ------------------------ # WRITE # ------------------------ def write(self, vals): if any(key in vals for key in ('status', 'start_date', 'end_date', 'user_id', 'project_id')): self._check_manager_access() projects = self.mapped('project_id') res = super().write(vals) self._sync_bench() if any(key in vals for key in ('user_id', 'project_id')): (projects | self.mapped('project_id'))._sync_members_from_team_lines() return res # ------------------------ # UNLINK # ------------------------ def unlink(self): projects = self.mapped('project_id') self._check_manager_access() res = super().unlink() self._sync_bench() projects._sync_members_from_team_lines() return res