From ce93d9601ca84d883053455d451a3211c93671db Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Tue, 5 May 2026 11:55:32 +0530 Subject: [PATCH] project updates and changes --- .../data/data.xml | 80 ++++----- .../project_task_timesheet_extended/hooks.py | 15 +- .../models/project.py | 152 +++++++++++++++-- .../models/project_stages.py | 18 +- .../models/project_task.py | 6 +- .../models/task_stages.py | 112 ++++++++----- .../view/project.xml | 56 +++---- .../view/project_portfolio.xml | 33 +++- .../view/project_task_view.xml | 4 + .../view/task_stages.xml | 46 ++++-- .../wizards/project_stage_update_wizard.py | 155 +++++++----------- .../wizards/project_stage_update_wizard.xml | 22 +-- 12 files changed, 434 insertions(+), 265 deletions(-) diff --git a/addons_extensions/project_task_timesheet_extended/data/data.xml b/addons_extensions/project_task_timesheet_extended/data/data.xml index 6cec732c2..3cf6427e4 100644 --- a/addons_extensions/project_task_timesheet_extended/data/data.xml +++ b/addons_extensions/project_task_timesheet_extended/data/data.xml @@ -109,42 +109,48 @@ - - 100 - Backlog - - - - - 101 - Development - - - - - 102 - Code Review & Git Merging - - - - - 103 - Testing - - - - - 104 - Deployment - - - - - 105 - Completed - - - + + 100 + Backlog + + + + + + 101 + Development + + + + + + 102 + Code Review & Git Merging + + + + + + 103 + Testing + + + + + + 104 + Deployment + + + + + + 105 + Completed + + + + @@ -257,4 +263,4 @@ - \ No newline at end of file + diff --git a/addons_extensions/project_task_timesheet_extended/hooks.py b/addons_extensions/project_task_timesheet_extended/hooks.py index 9e67fae22..9e82a6274 100644 --- a/addons_extensions/project_task_timesheet_extended/hooks.py +++ b/addons_extensions/project_task_timesheet_extended/hooks.py @@ -162,9 +162,12 @@ def post_init_hook(env): project_tasks[task.project_id.id] = [] project_tasks[task.project_id.id].append(task) - # Assign sequence numbers to tasks - for project_id, task_list in project_tasks.items(): - project = env['project.project'].browse(project_id) - if project.task_sequence_id: - for task in task_list: - task.sequence_name = project.task_sequence_id.next_by_id() \ No newline at end of file + # Assign sequence numbers to tasks + for project_id, task_list in project_tasks.items(): + project = env['project.project'].browse(project_id) + if project.task_sequence_id: + for task in task_list: + task.sequence_name = project.task_sequence_id.next_by_id() + + # Normalize task stages so each project owns its workflow configuration. + env['project.project'].search([])._ensure_project_owned_task_stages() diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py index 68618eabf..dc4511682 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project.py +++ b/addons_extensions/project_task_timesheet_extended/models/project.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models, _ +from odoo import Command, api, fields, models, _ from odoo.exceptions import UserError, ValidationError from markupsafe import Markup from datetime import datetime, timedelta @@ -899,9 +899,137 @@ class ProjectProject(models.Model): for member in members_to_add: self.discuss_channel_id.add_members(member.partner_id.ids) + def _get_default_task_stage_templates(self): + template_xmlids = [ + 'project_task_timesheet_extended.task_type_backlog', + 'project_task_timesheet_extended.task_type_development', + 'project_task_timesheet_extended.task_type_code_review_and_merging', + 'project_task_timesheet_extended.task_type_testing', + 'project_task_timesheet_extended.task_type_deployment', + 'project_task_timesheet_extended.task_type_completed', + ] + stages = self.env['project.task.type'] + for xmlid in template_xmlids: + stage = self.env.ref(xmlid, raise_if_not_found=False) + if stage: + stages |= stage + return stages.sorted('sequence') + + def _clone_task_stage_for_project(self, stage): + self.ensure_one() + new_stage = self.env['project.task.type'].create(stage._prepare_project_owned_stage_vals(self)) + project_tasks = self.env['project.task'].search([ + ('project_id', '=', self.id), + ('stage_id', '=', stage.id), + ]) + if project_tasks: + project_tasks.write({'stage_id': new_stage.id}) + return new_stage + + def _ensure_project_owned_task_stages(self): + if len(self) > 1: + for project_id in self.ids: + self.browse(project_id)._ensure_project_owned_task_stages() + return + + self.ensure_one() + if not self.type_ids: + self.type_ids = [Command.set(self._get_default_task_stage_templates().ids)] + + owned_stage_ids = [] + for stage in self.type_ids.sorted('sequence'): + if stage._is_project_owned_stage(self): + owned_stage_ids.append(stage.id) + continue + cloned_stage = self._clone_task_stage_for_project(stage) + owned_stage_ids.append(cloned_stage.id) + + if self.type_ids.ids != owned_stage_ids: + super(ProjectProject, self).write({'type_ids': [Command.set(owned_stage_ids)]}) + + def _prepare_type_ids_commands(self, commands): + self.ensure_one() + if not commands: + return commands + + processed_commands = [] + stage_model = self.env['project.task.type'] + + for command in commands: + if not isinstance(command, (list, tuple)) or not command: + processed_commands.append(command) + continue + + operation = command[0] + + if operation == Command.CREATE: + values = dict(command[2] or {}) + values['project_ids'] = [Command.set(self.ids)] + values.setdefault('is_workflow_template', False) + processed_commands.append(Command.create(values)) + continue + + if operation == Command.UPDATE: + stage = stage_model.browse(command[1]).exists() + if not stage: + continue + if not stage._is_project_owned_stage(self): + original_stage = stage + stage = self._clone_task_stage_for_project(stage) + processed_commands.extend([ + Command.unlink(original_stage.id), + Command.link(stage.id), + ]) + processed_commands.append(Command.update(stage.id, command[2] or {})) + continue + + if operation == Command.LINK: + stage = stage_model.browse(command[1]).exists() + if stage and not stage._is_project_owned_stage(self): + stage = self._clone_task_stage_for_project(stage) + if stage: + processed_commands.append(Command.link(stage.id)) + continue + + if operation == Command.SET: + stage_ids = [] + for stage in stage_model.browse(command[2]).exists(): + if not stage._is_project_owned_stage(self): + stage = self._clone_task_stage_for_project(stage) + stage_ids.append(stage.id) + processed_commands.append(Command.set(stage_ids)) + continue + + if operation == Command.DELETE: + stage = stage_model.browse(command[1]).exists() + if stage and not stage._is_project_owned_stage(self): + processed_commands.append(Command.unlink(stage.id)) + else: + processed_commands.append(command) + continue + + processed_commands.append(command) + + return processed_commands + def write(self, vals): """Override write to update channel members when project members change""" - result = super().write(vals) + if 'type_ids' in vals: + if len(self) > 1: + results = [] + for project in self: + project_vals = dict(vals) + project_vals['type_ids'] = project._prepare_type_ids_commands(vals['type_ids']) + results.append(super(ProjectProject, project).write(project_vals)) + project._ensure_project_owned_task_stages() + result = all(results) + else: + vals = dict(vals) + vals['type_ids'] = self._prepare_type_ids_commands(vals['type_ids']) + result = super().write(vals) + self._ensure_project_owned_task_stages() + else: + result = super().write(vals) # If members changed, update channel members if any(field in vals for field in ['members_ids', 'user_id', 'project_lead']): @@ -944,6 +1072,7 @@ class ProjectProject(models.Model): projects = super().create(vals_list) sequence = self._get_shared_project_sequence() for project in projects: + project._ensure_project_owned_task_stages() if not project.sequence_name: project.sequence_name = sequence.next_by_id() if project.discuss_channel_id: @@ -951,18 +1080,7 @@ class ProjectProject(models.Model): return projects def _default_type_ids(self): - default_stage_ids = [ - self.env.ref('project_task_timesheet_extended.task_type_backlog').id, - self.env.ref('project_task_timesheet_extended.task_type_development').id, - self.env.ref('project_task_timesheet_extended.task_type_code_review_and_merging').id, - self.env.ref('project_task_timesheet_extended.task_type_testing').id, - self.env.ref('project_task_timesheet_extended.task_type_deployment').id, - self.env.ref('project_task_timesheet_extended.task_type_completed').id, - ] - - # self.env.ref('project_task_timesheet_extended.task_type_cancelled').id, - # self.env.ref('project_task_timesheet_extended.task_type_hold').id, - return self.env['project.task.type'].browse(default_stage_ids) + return self._get_default_task_stage_templates() project_lead = fields.Many2one("res.users", string="Project Lead", @@ -975,10 +1093,10 @@ class ProjectProject(models.Model): user_id = fields.Many2one('res.users', string='Project Manager', default=False, tracking=True, #required = 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', 'selected_employee_id') + @api.constrains('user_id') def _check_team_lead_before_members(self): for rec in self: - if rec.selected_employee_id and not rec.user_id: + if not rec.user_id: raise ValidationError("Assign Project Manager before adding members") type_ids = fields.Many2many(default=lambda self: self._default_type_ids()) @@ -1081,4 +1199,4 @@ class ProjectTask(models.Model): 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 \ No newline at end of file + project.show_task_chatter = not project.show_task_chatter diff --git a/addons_extensions/project_task_timesheet_extended/models/project_stages.py b/addons_extensions/project_task_timesheet_extended/models/project_stages.py index 954263560..b40559376 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_stages.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_stages.py @@ -8,8 +8,12 @@ import pytz class ProjectStages(models.Model): _inherit = "project.project.stage" - approval_by = fields.Selection([('project_manager', 'Project Manager'), ('project_sponsor', 'Project Sponsor')]) - + approval_by = fields.Selection([ + ('project_manager', 'Project Manager'), + ('project_lead', 'Project Lead'), + ('project_sponsor', 'Project Sponsor'), + ('manager_lead_or_sponsor', 'Project Authorizers'), + ]) class projectStagesApprovalFlow(models.Model): _name = 'project.stages.approval.flow' @@ -17,7 +21,7 @@ class projectStagesApprovalFlow(models.Model): stage_id = fields.Many2one('project.project.stage') stage_approval_by = fields.Selection(related='stage_id.approval_by') approval_by = fields.Many2one("res.users", domain="[('id','in',approval_by_users)]") - assigned_to = fields.Many2one("res.users", domain="[('id','in',related_stage_users)]") + assigned_to = fields.Many2one("res.users") related_stage_users = fields.Many2many("res.users",related="stage_id.user_ids") assigned_date = fields.Datetime() submission_date = fields.Datetime() @@ -27,7 +31,7 @@ class projectStagesApprovalFlow(models.Model): manager_level_edit_access = fields.Boolean(compute="_compute_manager_level_edit_access") activate = fields.Boolean(default=True) involved_users = fields.Many2many('res.users', 'project_stage_approval_user_rel', 'project_stage_approval_id', - 'user_id',string="Related Users",domain="[('id','in',related_stage_users)]") + 'user_id',string="Related Users") def _compute_manager_level_edit_access(self): for rec in self: @@ -45,8 +49,14 @@ class projectStagesApprovalFlow(models.Model): if rec.stage_approval_by == 'project_manager' and rec.project_id.user_id: pm_users = rec.project_id.user_id.ids + + elif rec.stage_approval_by == 'project_lead' and rec.project_id.project_lead: + pm_users = rec.project_id.project_lead.ids + elif rec.stage_approval_by == 'project_sponsor' and rec.project_id.project_sponsor: pm_users = rec.project_id.project_sponsor.ids + elif rec.stage_approval_by == 'manager_lead_or_sponsor': + pm_users = list(set(rec.project_id.user_id.ids + rec.project_id.project_lead.ids + rec.project_id.project_sponsor.ids)) else: pm_users = self.env['res.users'].sudo().search([ ('groups_id', 'in', group_pm.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 0417afca4..265fd0de5 100644 --- a/addons_extensions/project_task_timesheet_extended/models/project_task.py +++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py @@ -62,9 +62,9 @@ class projectTask(models.Model): def write(self, vals): # Allow stage update for multiple records if 'stage_id' in vals: - return super(ProjectTask, self).write(vals) + return super(projectTask, self).write(vals) - return super(ProjectTask, self).write(vals) + return super(projectTask, self).write(vals) @api.constrains('name') @@ -110,7 +110,7 @@ class projectTask(models.Model): ): raise UserError("Only Task Creator or Project Manager can edit Generic field.") - return super(ProjectTask, self).write(vals) + return super(projectTask, self).write(vals) @api.constrains('estimated_hours') def _check_estimated_hours(self): 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 b7cb90c99..9caef46e3 100644 --- a/addons_extensions/project_task_timesheet_extended/models/task_stages.py +++ b/addons_extensions/project_task_timesheet_extended/models/task_stages.py @@ -1,39 +1,73 @@ -from odoo import api, fields, models, _ -from odoo.exceptions import UserError - -class TaskStages(models.Model): - _inherit = 'project.task.type' - - team_id = fields.Many2one('internal.teams','Assigned to') - approval_by = fields.Selection([('assigned_team_lead','Assigned Team Lead'),('project_manager','Project Manager'),('project_lead','Project Lead / Manager')]) - involved_user_ids = fields.Many2many('res.users') - - @api.onchange('team_id') - def onchange_team_id(self): - for rec in self: - if rec.team_id and rec.team_id.all_members_ids: - rec.involved_user_ids = [(6,0,rec.team_id.all_members_ids.ids)] - - def create_or_update_data(self): - """Open wizard for updating this stage inside a project context.""" - self.ensure_one() - project_id = self.env.context.get('project_id') or self.env.context.get('active_id') - - if not project_id: - raise UserError(_("No project found in context.")) - - return { - 'type': 'ir.actions.act_window', - 'name': 'Edit Stage', - 'res_model': 'project.stage.update.wizard', - 'view_mode': 'form', - 'target': 'new', - 'context': { - 'default_project_id': project_id, - 'default_stage_id': self.id, - 'default_team_id': self.team_id.id if self.team_id else False, - 'default_approval_by': self.approval_by if self.approval_by else False, - 'default_fold': self.fold, - 'default_involved_user_ids': [(6,0,self.involved_user_ids.ids)] - }, - } \ No newline at end of file +from odoo import Command, api, fields, models, _ +from odoo.exceptions import UserError + +class TaskStages(models.Model): + _inherit = 'project.task.type' + + team_id = fields.Many2one('internal.teams', 'Assigned Team') + approval_by = fields.Selection( + [('assigned_team_lead', 'Assigned Team Lead'), ('project_manager', 'Project Manager'), ('project_lead', 'Project Lead / Manager')], + string='Approval Owner', + ) + involved_user_ids = fields.Many2many('res.users', string='Related Users') + team_related_user_ids = fields.Many2many(related='team_id.all_members_ids', string='Team Users') + is_workflow_template = fields.Boolean( + string='Workflow Template Stage', + default=False, + copy=False, + help='Technical flag used to keep template stages separate from project-owned stages.', + ) + + @api.onchange('team_id') + def onchange_team_id(self): + for rec in self: + if rec.team_id and rec.team_id.all_members_ids: + rec.involved_user_ids = [(6,0,rec.team_id.all_members_ids.ids)] + else: + rec.involved_user_ids = [(5, 0, 0)] + + def _is_project_owned_stage(self, project): + self.ensure_one() + return ( + not self.is_workflow_template + and self.project_ids == project + ) + + def _prepare_project_owned_stage_vals(self, project): + self.ensure_one() + return { + 'name': self.name, + 'sequence': self.sequence, + 'project_ids': [Command.set(project.ids)], + 'mail_template_id': self.mail_template_id.id, + 'fold': self.fold, + 'rating_template_id': self.rating_template_id.id, + 'auto_validation_state': self.auto_validation_state, + 'active': self.active, + 'user_id': False, + 'team_id': self.team_id.id, + 'approval_by': self.approval_by, + 'involved_user_ids': [Command.set(self.involved_user_ids.ids)], + 'is_workflow_template': False, + } + + def create_or_update_data(self): + """Open the stage in form mode with the active project context.""" + self.ensure_one() + project_id = self.env.context.get('project_id') or self.env.context.get('active_id') + + if not project_id: + raise UserError(_("No project found in context.")) + + return { + 'type': 'ir.actions.act_window', + 'name': _('Edit Task Stage'), + 'res_model': 'project.task.type', + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_project_id': project_id, + 'project_stage_project_id': project_id, + }, + } diff --git a/addons_extensions/project_task_timesheet_extended/view/project.xml b/addons_extensions/project_task_timesheet_extended/view/project.xml index 8cb4743f3..a233a7e7f 100644 --- a/addons_extensions/project_task_timesheet_extended/view/project.xml +++ b/addons_extensions/project_task_timesheet_extended/view/project.xml @@ -113,23 +113,21 @@ - - - - - - - - - - -