project updates and changes

This commit is contained in:
pranaysaidurga 2026-05-05 11:55:32 +05:30
parent 1b21175e75
commit ce93d9601c
12 changed files with 434 additions and 265 deletions

View File

@ -109,42 +109,48 @@
</record>
</data>
<data noupdate="1">
<record id="task_type_backlog" model="project.task.type">
<field name="sequence">100</field>
<field name="name">Backlog</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
</record>
<record id="task_type_development" model="project.task.type">
<field name="sequence">101</field>
<field name="name">Development</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
</record>
<record id="task_type_code_review_and_merging" model="project.task.type">
<field name="sequence">102</field>
<field name="name">Code Review &amp; Git Merging</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
</record>
<record id="task_type_testing" model="project.task.type">
<field name="sequence">103</field>
<field name="name">Testing</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
</record>
<record id="task_type_deployment" model="project.task.type">
<field name="sequence">104</field>
<field name="name">Deployment</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
</record>
<record id="task_type_completed" model="project.task.type">
<field name="sequence">105</field>
<field name="name">Completed</field>
<field name="fold" eval="True"/>
<field name="user_id" eval="False"/>
</record>
<record id="task_type_backlog" model="project.task.type">
<field name="sequence">100</field>
<field name="name">Backlog</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
<field name="is_workflow_template" eval="True"/>
</record>
<record id="task_type_development" model="project.task.type">
<field name="sequence">101</field>
<field name="name">Development</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
<field name="is_workflow_template" eval="True"/>
</record>
<record id="task_type_code_review_and_merging" model="project.task.type">
<field name="sequence">102</field>
<field name="name">Code Review &amp; Git Merging</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
<field name="is_workflow_template" eval="True"/>
</record>
<record id="task_type_testing" model="project.task.type">
<field name="sequence">103</field>
<field name="name">Testing</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
<field name="is_workflow_template" eval="True"/>
</record>
<record id="task_type_deployment" model="project.task.type">
<field name="sequence">104</field>
<field name="name">Deployment</field>
<field name="fold" eval="False"/>
<field name="user_id" eval="False"/>
<field name="is_workflow_template" eval="True"/>
</record>
<record id="task_type_completed" model="project.task.type">
<field name="sequence">105</field>
<field name="name">Completed</field>
<field name="fold" eval="True"/>
<field name="user_id" eval="False"/>
<field name="is_workflow_template" eval="True"/>
</record>
<!-- <record id="task_type_cancelled" model="project.task.type">-->
<!-- <field name="sequence">106</field>-->
<!-- <field name="name">Cancelled</field>-->
@ -257,4 +263,4 @@
</record>
</data>
</odoo>
</odoo>

View File

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

View File

@ -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
project.show_task_chatter = not project.show_task_chatter

View File

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

View File

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

View File

@ -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)]
},
}
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,
},
}

View File

@ -113,23 +113,21 @@
<field name="activate"/>
</list>
</field>
</page>
<page name="task_stages" string="Task Stages" invisible="not is_project_editor">
<field name="type_ids" context="{'project_id': id}" options="{'no_open': True}">
<list edit="0" no_open="True">
<field name="sequence"/>
<field name="name"/>
<field name="team_id"/>
<field name="approval_by"/>
<field name="fold"/>
<field name="involved_user_ids" widget="many2many_tags"/>
<button name="create_or_update_data"
type="object"
string="Update"
class="btn-primary"/>
</list>
</field>
</page>
</page>
<page name="task_stages" string="Task Stages" invisible="not is_project_editor">
<field name="type_ids" context="{'default_project_id': id, 'project_stage_project_id': id}">
<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="approval_by" string="Approval Owner"/>
<field name="fold"/>
<field name="team_related_user_ids" invisible="1" column_invisible="1"/>
<field name="involved_user_ids" widget="many2many_tags" domain="[('id', 'in', team_related_user_ids)]"/>
<field name="is_workflow_template" invisible="1" column_invisible="1"/>
</list>
</field>
</page>
<page string="Team" name="team" invisible="not is_project_editor">
<group>
<button name="fetch_project_task_stage_users" type="object" string="Fetch Users from Related Stages" class="btn-primary"/>
@ -886,15 +884,15 @@
</field>
</record>
<!-- <record id="project_view_kanban_inherit" model="ir.ui.view">-->
<!-- <field name="name">project.view.kanban.inherit</field>-->
<!-- <field name="model">project.project</field>-->
<!-- <field name="inherit_id" ref="project.view_project_kanban"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//kanban" position="attributes">-->
<!-- <attribute name="context">{'view_type':'kanban'}</attribute>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
</odoo>
<!-- <record id="project_view_kanban_inherit" model="ir.ui.view">-->
<!-- <field name="name">project.view.kanban.inherit</field>-->
<!-- <field name="model">project.project</field>-->
<!-- <field name="inherit_id" ref="project.view_project_kanban"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//kanban" position="attributes">-->
<!-- <attribute name="context">{'view_type':'kanban'}</attribute>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
</odoo>

View File

@ -252,11 +252,13 @@
</div>
<div class="mt-2">
<span class="fa fa-folder-open me-1"/>
<span>
<t t-esc="record.project_ids.count"/>
Projects
</span>
<a name="action_view_projects" type="object">
<div>
<span class="fa fa-folder-open me-1"/>
<field name="project_count" class="o_value"/>
<span class="o_stat_text"> Projects</span>
</div>
</a>
</div>
<div class="mt-2">
@ -402,7 +404,7 @@
<record id="action_project_portfolio" model="ir.actions.act_window">
<field name="name">Project Portfolios</field>
<field name="res_model">project.portfolio</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_mode">kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first portfolio
@ -422,17 +424,30 @@
<menuitem id="menu_project_portfolio_root"
name="Portfolios"
parent="project.menu_main_pm"
sequence="30"
sequence="1"
action="action_project_portfolio"
groups="project.group_project_manager"/>
<menuitem
name="Projects"
id="project.menu_projects_group_stage"
parent="project.menu_main_pm"
action="project.open_view_project_all_group_stage"
groups="project.group_project_stages"
sequence="2"
/>
<menuitem id="menu_project_portfolio"
name="Project Portfolios"
parent="menu_project_portfolio_root"
action="action_project_portfolio"/>
action="action_project_portfolio"
active="0"/>
<menuitem id="menu_project_portfolio_performance"
name="Team Performance"
parent="menu_project_portfolio_root"
parent="project.menu_project_report"
sequence="0"
action="action_project_portfolio_employee_performance"/>
<!-- Inherit Project Form -->

View File

@ -116,8 +116,12 @@
<xpath expr="//kanban" position="attributes">
<attribute name="default_order">create_date desc</attribute>
<attribute name="action"></attribute>
<attribute name="on_create"></attribute>
</xpath>
</field>
</record>

View File

@ -1,16 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="task_type_tree_inherit" model="ir.ui.view">
<field name="name">project.task.type.list.inherit</field>
<field name="model">project.task.type</field>
<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="approval_by" optional="show"/>
<field name="involved_user_ids" optional="show"/>
</xpath>
</field>
</record>
</odoo>
<odoo>
<record id="task_type_form_inherit_project_workflow" model="ir.ui.view">
<field name="name">project.task.type.form.inherit.project.workflow</field>
<field name="model">project.task.type</field>
<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="approval_by"/>
<field name="team_related_user_ids" invisible="1"/>
<field name="involved_user_ids" widget="many2many_tags" domain="[('id', 'in', team_related_user_ids)]"/>
<field name="is_workflow_template" invisible="1"/>
</xpath>
</field>
</record>
<record id="task_type_tree_inherit" model="ir.ui.view">
<field name="name">project.task.type.list.inherit</field>
<field name="model">project.task.type</field>
<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="approval_by" optional="show"/>
<field name="team_related_user_ids" invisible="1"/>
<field name="involved_user_ids" optional="show" domain="[('id', 'in', team_related_user_ids)]"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -1,96 +1,61 @@
from odoo import models, fields, api
class ProjectStageUpdateWizard(models.TransientModel):
_name = 'project.stage.update.wizard'
_description = 'Project Stage Update Wizard'
project_id = fields.Many2one('project.project', string='Project', required=True)
stage_id = fields.Many2one('project.task.type', string='Current Stage', required=True)
name = fields.Char(string='Stage Name', related='stage_id.name', readonly=False)
team_id = fields.Many2one('internal.teams', string='Assigned to', readonly=False)
approval_by = fields.Selection([
('assigned_team_lead', 'Assigned Team Lead'),
('project_manager', 'Project Manager'),
('project_lead', 'Project Lead / Manager')
], readonly=False)
fold = fields.Boolean(string='Folded in Kanban', readonly=False)
involved_user_ids = fields.Many2many('res.users')
related_user_ids = fields.Many2many(related="team_id.all_members_ids")
@api.onchange('team_id')
def onchange_team_id(self):
for rec in self:
if rec.team_id and rec.team_id.all_members_ids:
if rec.team_id != rec.stage_id.team_id:
rec.involved_user_ids = [(6,0,rec.team_id.all_members_ids.ids)]
else:
rec.involved_user_ids = [(5, 0)]
def action_save_changes(self):
"""Create/update the stage and sync tasks and project links."""
self.ensure_one()
project = self.project_id
old_stage = self.stage_id
# Check if stage with same properties exists
stages = self.env['project.task.type'].search([
('name', '=', self.name),
('team_id', '=', self.team_id.id),
('approval_by', '=', self.approval_by),
('fold', '=', self.fold),
('involved_user_ids','=',self.involved_user_ids.ids)
], limit=1)
existing_stage = stages.filtered(
lambda s: set(s.involved_user_ids.ids) == set(self.involved_user_ids.ids)
)[:1]
if existing_stage:
existing_stage.sudo().write({
'project_ids': [(4, self.project_id.id)]
})
new_stage = existing_stage
else:
# Instead of copy(), create a clean new record without '(copy)'
new_stage = self.env['project.task.type'].create({
'name': self.name,
'team_id': self.team_id.id,
'approval_by': self.approval_by ,
'fold': self.fold,
'sequence': old_stage.sequence, # optional: keep same order
'involved_user_ids': [(6,0,self.involved_user_ids.ids)],
'project_ids':[(6,0,self.project_id.ids)]
})
# If new_stage is different from old_stage → update references
if new_stage.id != old_stage.id:
# Update project type_ids
type_ids = project.type_ids.ids
if old_stage.id in type_ids:
old_stage.sudo().write({
'project_ids':[(3,self.project_id.id)]
})
type_ids.remove(old_stage.id)
if new_stage.id not in type_ids:
type_ids.append(new_stage.id)
project.type_ids = [(6, 0, type_ids)]
# Update all tasks under this project with old stage
tasks = self.env['project.task'].search([
('project_id', '=', project.id),
('stage_id', '=', old_stage.id)
])
tasks.write({'stage_id': new_stage.id})
# If the old stage is no longer used in any project or task → remove it
remaining_projects = self.env['project.project'].search_count([('type_ids', 'in', old_stage.id)])
remaining_tasks = self.env['project.task'].search_count([('stage_id', '=', old_stage.id)])
if remaining_projects == 0 and remaining_tasks == 0:
old_stage.unlink()
return {'type': 'ir.actions.act_window_close'}
from odoo import models, fields, api
class ProjectStageUpdateWizard(models.TransientModel):
_name = 'project.stage.update.wizard'
_description = 'Project Stage Update Wizard'
project_id = fields.Many2one('project.project', string='Project', required=True)
stage_id = fields.Many2one('project.task.type', string='Current Stage', required=True)
name = fields.Char(string='Stage Name')
team_id = fields.Many2one('internal.teams', string='Assigned Team', readonly=False)
approval_by = fields.Selection([
('assigned_team_lead', 'Assigned Team Lead'),
('project_manager', 'Project Manager'),
('project_lead', 'Project Lead / Manager')
], string='Approval Owner', readonly=False)
fold = fields.Boolean(string='Folded in Kanban', readonly=False)
involved_user_ids = fields.Many2many('res.users', string='Related Users')
related_user_ids = fields.Many2many(related="team_id.all_members_ids")
@api.model
def default_get(self, fields_list):
values = super().default_get(fields_list)
stage = self.env['project.task.type'].browse(values.get('stage_id')).exists()
if stage:
values.setdefault('name', stage.name)
values.setdefault('team_id', stage.team_id.id)
values.setdefault('approval_by', stage.approval_by)
values.setdefault('fold', stage.fold)
values.setdefault('involved_user_ids', [(6, 0, stage.involved_user_ids.ids)])
return values
@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)]
def action_save_changes(self):
"""Update the project-owned stage configuration safely."""
self.ensure_one()
project = self.project_id
project._ensure_project_owned_task_stages()
stage = project.type_ids.filtered(lambda record: record.id == self.stage_id.id)[:1]
if not stage:
stage = project._clone_task_stage_for_project(self.stage_id)
project.type_ids = [(4, stage.id)]
stage.write({
'name': self.name,
'team_id': self.team_id.id,
'approval_by': self.approval_by,
'fold': self.fold,
'involved_user_ids': [(6, 0, self.involved_user_ids.ids)],
'is_workflow_template': False,
})
return {'type': 'ir.actions.act_window_close'}

View File

@ -5,16 +5,16 @@
<field name="model">project.stage.update.wizard</field>
<field name="arch" type="xml">
<form string="Update Stage">
<group>
<field name="project_id" invisible="1"/>
<field name="stage_id"/>
<field name="name"/>
<field name="team_id" required="approval_by == 'assigned_team_lead'"/>
<field name="approval_by"/>
<field name="involved_user_ids" widget="many2many_tags"/>
<field name="related_user_ids" invisible="1"/>
<field name="fold"/>
</group>
<group>
<field name="project_id" invisible="1"/>
<field name="stage_id"/>
<field name="name"/>
<field name="team_id" required="approval_by == 'assigned_team_lead'"/>
<field name="approval_by"/>
<field name="involved_user_ids" widget="many2many_tags" domain="[('id', 'in', related_user_ids)]"/>
<field name="related_user_ids" invisible="1"/>
<field name="fold"/>
</group>
<footer>
<button string="Save Changes" name="action_save_changes" type="object" class="btn-primary"/>
<button string="Cancel" special="cancel" class="btn-secondary"/>
@ -31,4 +31,4 @@
</record>
</odoo>
</odoo>