Project Task Extended Changes

This commit is contained in:
pranay 2025-11-13 12:36:54 +05:30
parent 2dbdb58127
commit 66077d1819
19 changed files with 1297 additions and 26 deletions

View File

@ -23,11 +23,16 @@ Key Features:
'project',
'hr_timesheet',
'base',
'analytic',
],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/data.xml',
'wizards/project_user_assign_wizard.xml',
'wizards/internal_team_members_wizard.xml',
'wizards/project_stage_update_wizard.xml',
'wizards/task_reject_reason_wizard.xml',
'view/teams.xml',
'view/task_stages.xml',
'view/project.xml',

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="action_toggle_pause" model="ir.actions.server">
<field name="name">Pause/Unpause</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="binding_model_id" ref="project.model_project_task"/>
<field name="binding_type">action</field>
<field name="state">code</field>
<field name="code">
if records:
action = records.action_toggle_pause()
</field>
</record>
<data noupdate="1">
<record id="default_projects_channel" model="discuss.channel">
<field name="name">Projects Channel</field>
<field name="description">Main channel for all project communications</field>
<field name="channel_type">channel</field>
<field name="group_public_id" eval="False"/>
</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_cancelled" model="project.task.type">-->
<!-- <field name="sequence">106</field>-->
<!-- <field name="name">Cancelled</field>-->
<!-- <field name="fold" eval="True"/>-->
<!-- <field name="user_id" eval="False"/>-->
<!-- </record>-->
<!-- <record id="task_type_hold" model="project.task.type">-->
<!-- <field name="sequence">107</field>-->
<!-- <field name="name">Hold</field>-->
<!-- <field name="fold" eval="True"/>-->
<!-- <field name="user_id" eval="False"/>-->
<!-- </record>-->
</data>
</odoo>

View File

@ -103,6 +103,34 @@ def post_init_hook(env):
"""
})
timesheet_approver_rule = env.ref('hr_timesheet.timesheet_line_rule_approver', raise_if_not_found=False)
if timesheet_approver_rule:
timesheet_approver_rule.write({
'domain_force': """
['&', '&',
('project_id', '!=', False),
('task_id', '!=', False),
'|',
'&',
('project_id.privacy_visibility', '=', 'followers'),
'|',
'|',
('project_id.project_lead', '=', user.id),
('project_id.user_id', '=', user.id),
'|',
'&',
('task_id.is_generic', '=', False),
('user_id', 'in', 'task_id.user_ids'),
'&',
('task_id.is_generic', '=', True),
('user_id.partner_id', 'in', 'project_id.message_partner_ids'),
'&', '&',
('project_id.privacy_visibility', '!=', 'followers'),
('task_id.is_generic', '=', False),
('user_id', 'in', 'task_id.user_ids')
]
"""
})
# Get all projects without sequence_name, sorted by creation date
projects = env['project.project'].search([('sequence_name', '=', False)], order='create_date asc')

View File

@ -1,4 +1,5 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class ProjectProject(models.Model):
@ -12,6 +13,100 @@ class ProjectProject(models.Model):
copy=False,
help="Sequence for tasks of this project"
)
discuss_channel_id = fields.Many2one(
'discuss.channel',
string="Project Channel",
domain="[('parent_channel_id', '=', default_projects_channel_id)]",
help="Select a channel for project communications. Channels must be sub-channels of the main Projects Channel."
)
default_projects_channel_id = fields.Many2one(
'discuss.channel',
default=lambda self: self._get_default_projects_channel(),
string="Default Projects Channel"
)
@api.model
def _get_default_projects_channel(self):
"""Get or create the default Projects Channel"""
channel = self.env['discuss.channel'].search([
('name', '=', 'Projects Channel'),
('channel_type', '=', 'channel')
], limit=1)
if not channel:
channel = self.env['discuss.channel'].create({
'name': 'Projects Channel',
'description': 'Main channel for all project communications',
'channel_type': 'channel',
})
return channel
def action_create_project_channel(self):
"""Create a new channel for this project under the Projects Channel"""
self.ensure_one()
if self.discuss_channel_id:
raise UserError(_("This project already has a channel assigned."))
# Create new channel
channel_vals = {
'name': self.name,
'description': _("Communication channel for project %s") % self.name,
'channel_type': 'channel',
'parent_channel_id': self.default_projects_channel_id.id,
}
new_channel = self.env['discuss.channel'].create(channel_vals)
self.discuss_channel_id = new_channel.id
# Add project members to the channel
self._add_project_members_to_channel()
return {
'type': 'ir.actions.act_window',
'res_model': 'discuss.channel',
'res_id': new_channel.id,
'view_mode': 'form',
'target': 'current',
'context': {'create': False}
}
def _add_project_members_to_channel(self):
"""Add all project members as followers of the channel"""
if not self.discuss_channel_id:
return
# Get all users related to this project
members_to_add = self.env['res.users']
# Add project members
if self.members_ids:
members_to_add |= self.members_ids
# Add project manager
if self.user_id:
members_to_add |= self.user_id
# Add project lead if exists
if hasattr(self, 'project_lead') and self.project_lead:
members_to_add |= self.project_lead
# Add members to channel
for member in members_to_add:
self.discuss_channel_id.add_members(member.partner_id.ids)
def write(self, vals):
"""Override write to update channel members when project members change"""
result = super().write(vals)
# If members changed, update channel members
if any(field in vals for field in ['members_ids', 'user_id', 'project_lead']):
for project in self:
if project.discuss_channel_id:
project._add_project_members_to_channel()
return result
@api.model
def _get_shared_project_sequence(self):
@ -35,8 +130,24 @@ class ProjectProject(models.Model):
for project in projects:
if not project.sequence_name:
project.sequence_name = sequence.next_by_id()
if project.discuss_channel_id:
project._add_project_members_to_channel()
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)
project_lead = fields.Many2one("res.users", string="Project Lead")
members_ids = fields.Many2many('res.users', 'project_user_rel', 'project_id',
'user_id', 'Project Members', help="""Project's
@ -46,6 +157,7 @@ class ProjectProject(models.Model):
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True,
domain=lambda self: [('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],)
type_ids = fields.Many2many(default=lambda self: self._default_type_ids())
def add_users(self):
return {

View File

@ -1,23 +1,477 @@
from odoo import api, fields, models, _
from markupsafe import Markup
from datetime import datetime
from odoo.exceptions import UserError, ValidationError
CLOSED_STATES = {
'1_done': 'Done',
'1_canceled': 'Cancelled',
}
class projectTask(models.Model):
_inherit = 'project.task'
_rec_name = 'name'
sequence_name = fields.Char("Sequence", copy=False)
is_generic = fields.Boolean(string='Generic',default=True, help='All the followers would be able to see this task if the generic is set to true else only the assigned users would have the access to it')
assigned_team = fields.Many2one("internal.teams")
is_generic = fields.Boolean(string='Generic', default=True, tracking=True,
help='All the followers would be able to see this task if the generic is set to true else only the assigned users would have the access to it')
assignees_timelines = fields.One2many('project.task.time.lines', 'task_id', string="Assignees Timelines",
tracking=True)
task_activity_log = fields.Html(string="Task Activity Log")
timelines_requested = fields.Boolean(tracking=True)
approval_status = fields.Selection([('submitted', 'Submitted'), ('approved', 'Approved'), ('refused', 'Refused')])
project_privacy_visibility = fields.Selection(related="project_id.privacy_visibility")
show_submission_button = fields.Boolean(compute="_compute_access_check")
show_approval_button = fields.Boolean(compute="_compute_access_check")
show_refuse_button = fields.Boolean(compute="_compute_access_check")
show_back_button = fields.Boolean(compute="_compute_access_check")
@api.onchange("assigned_team")
def onchange_assigned_team(self):
show_approval_flow = fields.Boolean(compute="_compute_show_approval_flow")
record_paused = fields.Boolean(default=False, tracking=True)
def _post_to_project_channel(self, message_body, mention_partners=None):
"""Post message to project's discuss channel with proper Odoo mention format"""
for task in self:
if not task.project_id:
continue
# Get the project channel or default projects channel
channel = task.project_id.discuss_channel_id or task.project_id.default_projects_channel_id
if channel:
# Format message with proper Odoo mentions
formatted_message = self._format_message_with_odoo_mentions(message_body, mention_partners)
# Post to channel - use Markup to ensure HTML is rendered properly
channel.message_post(
body=Markup(formatted_message),
message_type='comment',
subtype_xmlid='mail.mt_comment',
author_id=self.env.user.partner_id.id
)
def _format_message_with_odoo_mentions(self, message_body, mention_partners=None):
"""Format message with proper Odoo @mentions that will render correctly"""
if not mention_partners:
# Return plain message wrapped in a div for proper rendering
return f'<div>{message_body}</div>'
# Build the message with mentions
message_parts = []
message_parts.append('<div>')
message_parts.append(message_body)
# Add mentions at the end
for partner in mention_partners:
if partner and partner.name:
mention_html = f'<a href="#" data-oe-model="res.partner" data-oe-id="{partner.id}">@{partner.name}</a>'
message_parts.append(mention_html)
message_parts.append('</div>')
return ' '.join(message_parts)
def _create_odoo_mention(self, partner):
"""Create the proper Odoo mention format that renders correctly"""
if not partner:
return ""
return f'<a href="#" data-oe-model="res.partner" data-oe-id="{partner.id}">@{partner.name}</a>'
def _get_mention_partners(self, users):
"""Convert user records to partner records for mentioning"""
if not users:
return self.env['res.partner']
return users.mapped('partner_id')
@api.depends("project_id", "stage_id", "is_generic")
def _compute_show_approval_flow(self):
for rec in self:
if rec.assigned_team:
user_ids = rec.assigned_team.members_ids.ids
if rec.assigned_team.team_lead:
user_ids.append(rec.assigned_team.team_lead.id)
rec.user_ids = [(6, 0, user_ids)]
if rec.project_id.privacy_visibility == 'followers' and not rec.is_generic:
rec.show_approval_flow = True
else:
rec.user_ids = [(5, 0, 0)]
rec.show_approval_flow = False
def action_toggle_pause(self):
"""Toggle pause state for the record"""
for record in self:
current_user = self.env.user
if record.project_id:
if record.project_id.user_id != current_user and record.project_id.project_lead != current_user and not current_user.has_group(
'project.group_project_manager'):
raise UserError(_("Access denied: You do not have sufficient privileges to use this feature."))
record.record_paused = not record.record_paused
# Post to project channel
action = "paused" if record.record_paused else "resumed"
channel_message = _("Task %s has been %s by %s") % (
record.sequence_name or record.name,
action,
self.env.user.name
)
record._post_to_project_channel(channel_message)
# Add to activity log
record._add_activity_log(f"Task {action} by {self.env.user.name}")
@api.depends("assignees_timelines", "stage_id", "project_id", "approval_status")
def _compute_access_check(self):
for task in self:
task.show_submission_button = False
task.show_approval_button = False
task.show_refuse_button = False
task.show_back_button = False
user = self.env.user
project_manager = task.project_id.user_id
project_lead = task.project_id.project_lead
# Get current timeline for this stage
current_timeline = task.assignees_timelines.filtered(lambda s: s.stage_id == task.stage_id)
# Get next stage (if exists)
next_stage = task.project_id.type_ids.filtered(lambda s: s.sequence > task.stage_id.sequence).sorted(
key=lambda s: s.sequence)[:1]
# Compute buttons visibility
if current_timeline:
line = current_timeline[0]
assigned_to = line.assigned_to
responsible_lead = line.responsible_lead
if (
assigned_to
and assigned_to == user
and task.approval_status != "submitted"
and assigned_to != responsible_lead
):
task.show_submission_button = True
# a) Submitted + current user is responsible lead / project manager
if (
task.approval_status == "submitted"
and (responsible_lead == user or project_manager == user)
):
task.show_approval_button = True
task.show_refuse_button = True # both approve & refuse in review state
# b) No assigned user → directly approvable
elif not assigned_to and (responsible_lead == user or project_manager == user):
task.show_approval_button = True
# c) Assigned_to == responsible_lead → no submission needed, direct approve
elif (
assigned_to
and assigned_to == responsible_lead
and (user == assigned_to or user == project_manager)
):
task.show_approval_button = True
else:
# Allow project lead or project manager to approve directly
if user in [project_lead, project_manager]:
task.show_approval_button = True
if user in [project_manager] or user.has_group("project.group_project_manager"):
task.show_approval_button = True
task.show_back_button = True
is_first_stage = task.stage_id.sequence == min(task.project_id.type_ids.mapped('sequence'))
if is_first_stage:
task.show_back_button = False
is_last_stage = task.stage_id.sequence == max(task.project_id.type_ids.mapped('sequence'))
if is_last_stage:
task.show_submission_button = False
task.show_approval_button = False
task.show_refuse_button = False
def _get_current_datetime_formatted(self):
"""Helper method to get current datetime in '5-NOV-2025 1:20 PM' format"""
now = fields.Datetime.context_timestamp(self, datetime.now())
# Format: Day-MON-YEAR Hour:Minute AM/PM
formatted_date = now.strftime('%d-%b-%Y %I:%M %p').upper()
# Remove leading zero from day if present
if formatted_date[0] == '0':
formatted_date = formatted_date[1:]
return formatted_date
def _add_activity_log(self, activity_text):
"""Helper method to properly format HTML activity log"""
formatted_datetime = self._get_current_datetime_formatted()
for task in self:
log_entry = f"[{formatted_datetime}] {activity_text}"
if task.task_activity_log:
# Use Markup to safely combine HTML content
task.task_activity_log = Markup(task.task_activity_log) + Markup('<br>') + Markup(log_entry)
else:
task.task_activity_log = Markup(log_entry)
def back_button(self):
for task in self:
task.approval_status = False
prev_stage = task.project_id.type_ids.filtered(lambda s: s.sequence < task.stage_id.sequence)
prev_stage = prev_stage.sorted(key=lambda s: s.sequence, reverse=True)[:1] # Get next one
stage = task.assignees_timelines.filtered(lambda s: s.stage_id == prev_stage)
responsible_user = stage.assigned_to if stage and stage.assigned_to else (
task.project_id.project_lead if task.project_id.project_lead else False)
activity_log = "%s : %s Reverted the stage Back to %s" % (
task.stage_id.name,
self.env.user.employee_id.name,
prev_stage.name
)
task.stage_id = prev_stage
# Use the helper method to add activity log
task._add_activity_log(activity_log)
# Post to project channel with mention using proper Odoo format
if responsible_user:
channel_message = _("Task %s reverted from %s back to %s. %s please take action.") % (
task.sequence_name or task.name,
task.stage_id.name,
prev_stage.name,
self._create_odoo_mention(responsible_user.partner_id)
)
else:
channel_message = _("Task %s reverted from %s back to %s") % (
task.sequence_name or task.name,
task.stage_id.name,
prev_stage.name
)
task._post_to_project_channel(channel_message)
# Send chatter notification
if responsible_user:
task.message_post(
body=activity_log,
partner_ids=[responsible_user.partner_id.id],
message_type='notification',
subtype_xmlid='mail.mt_comment',
)
def submit_for_approval(self):
for task in self:
task.approval_status = "submitted"
stage = task.assignees_timelines.filtered(lambda s: s.stage_id == task.stage_id)
responsible_user = stage.responsible_lead if stage and stage.responsible_lead else False
activity_log = "%s : %s Submitted to %s for approval" % (
task.stage_id.name,
self.env.user.employee_id.name,
stage.responsible_lead.name if stage and stage.responsible_lead else ""
)
message_notes = "%s : %s submitted for approval" % (task.stage_id.name, task.sequence_name)
# Use the helper method to add activity log
task._add_activity_log(activity_log)
# Post to project channel with proper Odoo mention format
if responsible_user:
channel_message = _("Task %s submitted for approval at stage %s. %s please review.") % (
task.sequence_name or task.name,
task.stage_id.name,
self._create_odoo_mention(responsible_user.partner_id)
)
else:
channel_message = _("Task %s submitted for approval at stage %s") % (
task.sequence_name or task.name,
task.stage_id.name
)
task._post_to_project_channel(channel_message)
# Send chatter notification
if responsible_user:
task.message_post(
body=message_notes,
partner_ids=[responsible_user.partner_id.id],
message_type='notification',
subtype_xmlid='mail.mt_comment',
)
def proceed_further(self):
for task in self:
current_stage = task.stage_id
current_timeline = task.assignees_timelines.filtered(lambda s: s.stage_id == current_stage)
next_stage = task.assignees_timelines.filtered(lambda s: s.stage_id.sequence > current_stage.sequence)
next_stage = next_stage.sorted(key=lambda s: s.stage_id.sequence)[:1] # Get next one
n_stage = task.project_id.type_ids.filtered(lambda s: s.sequence > task.stage_id.sequence)
n_stage = n_stage.sorted(key=lambda s: s.sequence)[:1]
if n_stage:
task.stage_id = n_stage
task.approval_status = "approved"
activity_log = "%s: ✅ approved by %s and moved to %s" % (
current_stage.name,
self.env.user.employee_id.name,
n_stage.name)
# Use the helper method to add activity log
task._add_activity_log(activity_log)
user_notes = "%s: ✅ moved to %s and awaiting your completion" % (
task.sequence_name,
n_stage.name
)
next_user = next_stage.responsible_lead if next_stage.responsible_lead else task.project_id.user_id
if next_stage.assigned_to:
next_user = next_stage.assigned_to
# Post to project channel with proper Odoo mention format
if next_user:
channel_message = _("Task %s approved at stage %s and moved to %s. %s please proceed.") % (
task.sequence_name or task.name,
current_stage.name,
n_stage.name,
self._create_odoo_mention(next_user.partner_id)
)
else:
channel_message = _("Task %s approved at stage %s and moved to %s") % (
task.sequence_name or task.name,
current_stage.name,
n_stage.name
)
task._post_to_project_channel(channel_message)
task.message_post(
body=user_notes,
partner_ids=[next_user.partner_id.id],
message_type='notification',
subtype_xmlid='mail.mt_comment',
)
else:
task.approval_status = "approved"
notes = "%s: ✅ Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
activity_log = "%s: ✅ approved by %s" % (
current_stage.name,
self.env.user.employee_id.name)
# Use the helper method to add activity log
task._add_activity_log(activity_log)
# Post to project channel
channel_message = _("Task %s completed and approved at stage %s") % (
task.sequence_name or task.name,
current_stage.name
)
task._post_to_project_channel(channel_message)
task.message_post(body=notes, partner_ids=task.user_ids.partner_id.ids,
message_type='notification',
subtype_xmlid='mail.mt_comment', )
is_last_stage = task.stage_id.sequence == max(task.project_id.type_ids.mapped('sequence'))
if is_last_stage:
task.state = '1_done'
def reject_and_return(self, reason=None):
for task in self:
if not reason:
reason = ""
task.approval_status = "refused"
current_stage = task.stage_id
current_timeline = task.assignees_timelines.filtered(lambda s: s.stage_id == current_stage)
# Optional: find previous stage if you want to send back
stage = task.assignees_timelines.filtered(lambda s: s.stage_id == task.stage_id)
notes = "%s: ❌ %s rejected by %s" % (task.sequence_name, current_stage.name, self.env.user.employee_id.name)
activity_log = "%s: ❌ rejected by %s: %s" % (
current_stage.name,
self.env.user.employee_id.name,
reason)
# Use the helper method to add activity log
task._add_activity_log(activity_log)
# Post to project channel
channel_message = _("Task %s rejected at stage %s. Reason: %s") % (
task.sequence_name or task.name,
current_stage.name,
reason
)
task._post_to_project_channel(channel_message)
task.message_post(
body=notes,
message_type='notification',
subtype_xmlid='mail.mt_comment',
)
if stage:
responsible_user = stage.assigned_to if stage.assigned_to else stage.responsible_lead if stage.responsible_lead else task.project_id.user_id
# Post additional notification to responsible user with proper Odoo mention
if responsible_user:
user_channel_message = _("Task %s has been rejected and returned to you %s") % (
task.sequence_name or task.name,
self._create_odoo_mention(responsible_user.partner_id)
)
task._post_to_project_channel(user_channel_message)
task.message_post(
body="%s: Task has been rejected and returned to you." % (task.sequence_name),
partner_ids=[responsible_user.partner_id.id],
message_type='notification',
subtype_xmlid='mail.mt_comment',
)
def action_open_reject_wizard(self):
"""Open rejection wizard"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Reject Task"),
"res_model": "task.reject.reason.wizard",
"view_mode": "form",
"target": "new",
"context": {"default_task_id": self.id},
}
def request_timelines(self):
"""Populate task timelines with all relevant project stages."""
for task in self:
task.timelines_requested = True
# Clear existing timelines if needed
task.assignees_timelines.unlink()
# Fetch project stages
stages = task.project_id.type_ids.filtered(lambda s: not s.fold)
if not stages:
continue
timeline_vals = []
for stage in stages:
responsible_user = False
if stage.approval_by == 'assigned_team_lead' and stage.team_id.team_lead:
responsible_user = stage.team_id.team_lead.id
elif stage.approval_by == 'project_manager' and task.project_id.user_id:
responsible_user = task.project_id.user_id.id
elif stage.approval_by == 'project_lead' and getattr(task.project_id, 'project_lead', False):
responsible_user = task.project_id.project_lead.id
timeline_vals.append({
'stage_id': stage.id,
'team_id': stage.team_id.id if stage.team_id else False,
'responsible_lead': responsible_user,
'assigned_to': responsible_user,
'estimated_time': 0.0,
'task_id': task.id,
})
if timeline_vals:
self.env['project.task.time.lines'].create(timeline_vals)
# Post to project channel about timeline request
channel_message = _("Timelines requested for task %s") % (task.sequence_name or task.name)
task._post_to_project_channel(channel_message)
@api.model_create_multi
def create(self, vals_list):
@ -45,4 +499,103 @@ class projectTask(models.Model):
for task in tasks:
if task.project_id and task.project_id.task_sequence_id:
task.sequence_name = task.project_id.task_sequence_id.next_by_id()
# Post to project channel about task creation
if task.project_id:
channel_message = _("New task created: %s") % (task.sequence_name or task.name)
task._post_to_project_channel(channel_message)
return tasks
def button_update_assignees(self):
for task in self:
if task.assignees_timelines:
users_list = list(
set(task.assignees_timelines.responsible_lead.ids + task.assignees_timelines.assigned_to.ids + task.assignees_timelines.team_id.team_lead.ids))
task.user_ids = [(6, 0, users_list)]
# Post to project channel about assignee update
channel_message = _("Assignees updated for task %s") % (task.sequence_name or task.name)
task._post_to_project_channel(channel_message)
class projectTaskTimelines(models.Model):
_name = 'project.task.time.lines'
_sql_constraints = [
(
'unique_project_stage_task',
'unique(project_id, stage_id, task_id)',
'A timeline with the same Project, Stage, and Task already exists.'
),
]
stage_id = fields.Many2one('project.task.type', string="Stage", domain="[('id','in',stage_ids)]", required=True)
stage_sequence = fields.Integer(related="stage_id.sequence")
responsible_lead = fields.Many2one('res.users', string="Responsible Approver")
team_id = fields.Many2one("internal.teams", domain="[('id','in',allowed_team_ids)]")
team_all_member_ids = fields.Many2many('res.users'
# ,related="team_id.all_members_ids"
, compute="_compute_team_members"
)
assigned_to = fields.Many2one('res.users', string="Assigned To", domain="[('id','in',team_all_member_ids or [])]")
estimated_time = fields.Float(string="Estimated Time")
actual_time = fields.Float(string="Actual Time", readonly=True)
task_id = fields.Many2one("project.task")
project_id = fields.Many2one("project.project", related="task_id.project_id")
stage_ids = fields.Many2many(related="project_id.type_ids")
allowed_team_ids = fields.Many2many(
'internal.teams',
string="Allowed Teams",
compute="_compute_allowed_teams",
store=False
)
request_date = fields.Date(string="Request Date")
done_date = fields.Date(string="Done Date")
@api.depends('team_id', 'project_id')
def _compute_team_members(self):
for rec in self:
members = self.env['res.users']
if rec.team_id:
valid_members = rec.team_id.all_members_ids.filtered(lambda u: u.exists())
lead = rec.team_id.team_lead if rec.team_id.team_lead.exists() else False
rec.team_all_member_ids = list(set(valid_members.ids + ([lead.id] if lead else [])))
elif rec.project_id and rec.project_id.privacy_visibility == 'followers':
project_members = rec.project_id.members_ids.filtered(lambda u: u.exists())
partners = rec.project_id.message_partner_ids.mapped('user_ids').filtered(lambda u: u.exists())
project_user = rec.project_id.user_id if rec.project_id.user_id.exists() else False
project_lead = rec.project_id.project_lead if rec.project_id.project_lead.exists() else False
all_ids = (
project_members.ids +
partners.ids +
([project_user.id] if project_user else []) +
([project_lead.id] if project_lead else [])
)
rec.team_all_member_ids = list(set(all_ids))
else:
rec.team_all_member_ids = self.env['res.users'].sudo().search([
('active', '=', True),
('partner_id', '!=', False)
]).ids
@api.onchange("team_id")
def onchange_team_id(self):
for rec in self:
if rec.team_id and rec.team_id.team_lead:
rec.assigned_to = rec.team_id.team_lead.id
else:
rec.assigned_to = False
@api.depends('stage_id')
def _compute_allowed_teams(self):
for rec in self:
allowed_teams = self.env['internal.teams']
if rec.stage_id and rec.stage_id.team_id:
# Include the main team and its child teams
team = rec.stage_id.team_id
allowed_teams |= team
allowed_teams |= team.child_ids
rec.allowed_team_ids = allowed_teams

View File

@ -1,7 +1,31 @@
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')])
approval_by = fields.Selection([('assigned_team_lead','Assigned Team Lead'),('project_manager','Project Manager'),('project_lead','Project Lead / Manager')])
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,
},
}

View File

@ -14,13 +14,38 @@ class InternalTeams(models.Model):
parent_id = fields.Many2one('internal.teams', string="Parent Team",
domain="[('id', '!=', id)]")
child_ids = fields.One2many('internal.teams', 'parent_id', string="Child Teams")
active = fields.Boolean(default=True)
complete_name = fields.Char(string='Full Path', compute='_compute_complete_name', recursive=True)
# Computed field to include members + child team members and leads
all_members_ids = fields.Many2many(
'res.users', compute='_compute_all_members', string="All Members", store=False
)
@api.depends('team_name', 'parent_id.complete_name')
def _compute_complete_name(self):
for rec in self:
rec.complete_name = rec._get_full_name()
def _get_full_name(self, level=6):
""" Return the full name of ``self`` (up to a certain level). """
if level <= 0:
return '...'
if self.parent_id:
return self.parent_id._get_full_name(level - 1) + " / " + (self.team_name or "")
else:
return self.team_name
def add_internal_team_members(self):
return {
'name': 'Add Team Members',
'type': 'ir.actions.act_window',
'res_model': 'internal.team.members.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_user_ids': self.members_ids.ids}
}
@api.depends('members_ids', 'child_ids.members_ids', 'child_ids.team_lead')
def _compute_all_members(self):
for rec in self:

View File

@ -3,10 +3,19 @@ internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_pro
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,project.group_project_manager,1,0,0,0
project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1
project_internal_team_members_wizard,internal.team.members.wizard.manager,model_internal_team_members_wizard,base.group_user,1,1,1,1
project_project_stage_update_wizard,project.stage.update.wizard.manager,model_project_stage_update_wizard,base.group_user,1,1,1,1
access_project_project_supervisor,project.project,project.model_project_project,project_task_timesheet_extended.group_project_supervisor,1,1,1,0
access_project_project_stage_supervisor,project.project_stage.supervisor,project.model_project_project_stage,project_task_timesheet_extended.group_project_supervisor,1,1,1,0
access_project_task_type_supervisor,project.task.type supervisor,project.model_project_task_type,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
access_project_tags_supervisor,project.project_tags_supervisor,project.model_project_tags,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
access_project_task_time_lines_user,access_project_task_time_lines_user,model_project_task_time_lines,base.group_user,1,1,1,1
access_project_task_time_lines_manager,access_project_task_time_lines_manager,model_project_task_time_lines,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
3 internal_teams_manager internal.teams.manager model_internal_teams project.group_project_user 1 1 1 0
4 internal_teams_user internal.teams.user model_internal_teams base.group_user 1 0 0 0
5 project_user_assign_wizard_manager project.user.assign.wizard model_project_user_assign_wizard project.group_project_manager project_task_timesheet_extended.group_project_supervisor 1 1 1 1
6 access_project_project_supervisor project_user_assign_wizard_admin project.project project.user.assign.wizard.admin project.model_project_project model_project_user_assign_wizard project_task_timesheet_extended.group_project_supervisor project.group_project_manager 1 1 1 0 1
7 project_user_assign_wizard_user project.user.assign.wizard.user model_project_user_assign_wizard project.group_project_manager 1 0 0 0
8 project_user_task_reject_reason_wizard task.reject.reason.wizard.user model_task_reject_reason_wizard base.group_user 1 1 1 1
9 access_project_project_stage_supervisor project_internal_team_members_wizard project.project_stage.supervisor internal.team.members.wizard.manager project.model_project_project_stage model_internal_team_members_wizard project_task_timesheet_extended.group_project_supervisor base.group_user 1 1 1 0 1
10 project_project_stage_update_wizard project.stage.update.wizard.manager model_project_stage_update_wizard base.group_user 1 1 1 1
11 access_project_project_supervisor project.project project.model_project_project project_task_timesheet_extended.group_project_supervisor 1 1 1 0
12 access_project_project_stage_supervisor project.project_stage.supervisor project.model_project_project_stage project_task_timesheet_extended.group_project_supervisor 1 1 1 0
13 access_project_task_type_supervisor project.task.type supervisor project.model_project_task_type project_task_timesheet_extended.group_project_supervisor 1 1 1 1
14 access_project_task_type_supervisor access_project_tags_supervisor project.task.type supervisor project.project_tags_supervisor project.model_project_task_type project.model_project_tags project_task_timesheet_extended.group_project_supervisor 1 1 1 1
15 access_project_tags_supervisor access_project_task_time_lines_user project.project_tags_supervisor access_project_task_time_lines_user project.model_project_tags model_project_task_time_lines project_task_timesheet_extended.group_project_supervisor base.group_user 1 1 1 1
16 access_project_task_time_lines_manager access_project_task_time_lines_manager model_project_task_time_lines project.group_project_manager 1 1 1 1
17
18
19
20
21

View File

@ -65,6 +65,69 @@
<field name="perm_unlink" eval="0"/>
</record>
<!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">-->
<!-- <field name="name">timesheet: users: see own tasks</field>-->
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
<!-- <field name="domain_force">[-->
<!-- '&amp;','&amp;', '&amp;', '&amp;', '&amp;','&amp;',-->
<!-- ('project_id.privacy_visibility','=','followers'),-->
<!-- ('task_id', '!=', False),-->
<!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '!=', user.id),-->
<!-- ('project_id.user_id', '!=', user.id),-->
<!-- ('user_id','not in',[user.id]),-->
<!-- '|',-->
<!-- '&amp;',-->
<!-- ('task_id.is_generic', '=', False),-->
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
<!-- '&amp;',-->
<!-- ('task_id.is_generic', '=', True),-->
<!-- ('project_id.message_partner_ids', 'not in', [user.partner_id.id]),-->
<!-- ]</field>-->
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
<!-- <field name="perm_read" eval="0"/>-->
<!-- <field name="perm_unlink" eval="1"/>-->
<!-- <field name="perm_write" eval="1"/>-->
<!-- <field name="perm_create" eval="1"/>-->
<!-- </record>-->
<!-- <record model="ir.rule" id="timesheet_users_non_generic_timesheets">-->
<!-- <field name="name">timesheet: users: see own tasks</field>-->
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
<!-- <field name="domain_force">[-->
<!-- ('project_id.privacy_visibility','!=','followers'),-->
<!-- ('task_id', '!=', False),-->
<!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '!=', user.id),-->
<!-- ('project_id.user_id', '!=', user.id),-->
<!-- ('task_id.is_generic', '=', False),-->
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
<!-- ('user_id','not in',[user.id]),-->
<!-- ]</field>-->
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
<!-- <field name="perm_read" eval="0"/>-->
<!-- <field name="perm_unlink" eval="1"/>-->
<!-- <field name="perm_write" eval="1"/>-->
<!-- <field name="perm_create" eval="1"/>-->
<!-- </record>-->
<!-- <record model="ir.rule" id="timesheet_team_lead_normal_timesheets">-->
<!-- <field name="name">timesheet: Lead: see related tasks</field>-->
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
<!-- <field name="domain_force">[-->
<!-- '&amp;', '&amp;', '&amp;',-->
<!-- ('project_id', '!=', False),-->
<!-- ('project_id.project_lead', '=', user.id),-->
<!-- '|', ('task_id.is_generic', '=', True), ('task_id.is_generic', '=', False),-->
<!-- '|', ('task_id.user_ids', 'in', user.id), ('task_id.user_ids', 'not in', user.id)-->
<!-- ]-->
<!-- </field>-->
<!-- <field name="perm_read" eval="1"/>-->
<!-- <field name="perm_write" eval="1"/>-->
<!-- <field name="perm_create" eval="1"/>-->
<!-- <field name="perm_unlink" eval="0"/>-->
<!-- </record>-->
</data>
<data>

View File

@ -18,6 +18,38 @@
<xpath expr="//field[@name='user_id']" position="replace">
<field name="user_id" widget="many2one_avatar_user"/>
</xpath>
<xpath expr="//page[@name='settings']" position="inside">
<group>
<group name="group_project_channel_managment" string="Project Channel" col="1"
class="row mt16 o_settings_container">
<div>
<setting class="col-lg-12" id="discuss_channel_settings"
help="Determine the channel in which we are sending notifications">
<field name="discuss_channel_id"/>
<button name="action_create_project_channel"
string="Create Project Channel"
type="object"
class="btn-primary"
invisible = "discuss_channel_id"/>
</setting>
<field name="default_projects_channel_id" invisible="1"/>
</div>
</group>
</group>
</xpath>
<!-- <xpath expr="//field[@name='label_tasks']" position="before">-->
<!-- <group string="Project Channel">-->
<!-- <field name="discuss_channel_id" widget="many2one"-->
<!-- context="{'default_parent_id': default_projects_channel_id}"/>-->
<!-- <button name="action_create_project_channel"-->
<!-- string="Create Project Channel"-->
<!-- type="object"-->
<!-- class="btn-primary"-->
<!-- attrs="{'invisible': [('discuss_channel_id', '!=', False)]}"/>-->
<!-- <field name="default_projects_channel_id" invisible="1"/>-->
<!-- </group>-->
<!-- </xpath>-->
<page name="settings" position="after">
<page string="Team">
@ -31,7 +63,19 @@
</group>
</page>
<page name="task_stages" string="Task Stages">
<field name="type_ids"/>
<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"/>
<button name="create_or_update_data"
type="object"
string="Update"
class="btn-primary"/>
</list>
</field>
</page>
</page>
</field>

View File

@ -7,20 +7,89 @@
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<div class="alert alert-warning text-center" role="alert"
invisible="not record_paused">
<i class="fa fa-pause me-2"/>
<strong>THIS TASK IS CURRENTLY PAUSED</strong>
</div>
</xpath>
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="after">
<group>
<h1><field name="sequence_name" readonly="1"/></h1>
</group>
</xpath>
<xpath expr="//field[@name='user_ids']" position="before">
<field name="assigned_team"/>
</xpath>
<!-- <xpath expr="//field[@name='user_ids']" position="before">-->
<!-- <field name="assigned_team"/>-->
<!-- </xpath>-->
<xpath expr="//field[@name='user_ids']" position="after">
<field name="is_generic"/>
<field name="record_paused" invisible="1"/>
</xpath>
<xpath expr="//sheet/notebook" position="inside">
<page string="Assignees Timelines" invisible="not show_approval_flow">
<button name="button_update_assignees" type="object" string="Update Assignees" class="oe_highlight"/>
<field name="assignees_timelines" context="{'default_task_id': id}" >
<list editable="bottom">
<field name="stage_id"/>
<field name="responsible_lead"/>
<field name="team_id"/>
<field name="assigned_to"/>
<!-- <field name="team_all_member_ids" widget="many2many_tags"/>-->
<field name="estimated_time" widget="float_time"/>
<field name="actual_time" readonly="1" optional="hide" widget="float_time"/>
<field name="request_date" readonly="1" optional="hide"/>
<field name="done_date" readonly="1" optional="hide"/>
</list>
</field>
</page>
<page string="Task Activity Log" invisible="not show_approval_flow">
<field name="task_activity_log" widget="html" options="{'sanitize': False}" readonly="1" force_save="1"/>
</page>
</xpath>
<xpath expr="//header" position="inside">
<button type="object" name="request_timelines" string="Request Timelines" class="oe_highlight" invisible="not show_approval_flow or timelines_requested"/>
<button type="object" name="submit_for_approval" string="Request Approval" class="oe_highlight" invisible="not show_approval_flow or not show_submission_button or not timelines_requested"/>
<button type="object" name="proceed_further" string="Approve &amp; Proceed" class="oe_highlight" invisible="not show_approval_flow or not show_approval_button or not timelines_requested"/>
<button type="object" name="action_open_reject_wizard" string="Reject &amp; Return" class="oe_highlight" invisible="not show_approval_flow or not show_refuse_button or not timelines_requested"/>
<button type="object" name="back_button" string="Go Back" class="oe_highlight" invisible="not show_approval_flow or not show_back_button or not timelines_requested"/>
</xpath>
<xpath expr="//form" position="inside">
<field name="approval_status" invisible="1"/>
<field name="project_privacy_visibility" invisible="1"/>
<field name="show_approval_flow" invisible="1"/>
<field name="show_submission_button" invisible="1"/>
<field name="show_approval_button" invisible="1"/>
<field name="show_refuse_button" invisible="1"/>
<field name="show_back_button" invisible="1"/>
</xpath>
<xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="readonly">show_approval_flow or state in ['1_canceled','04_waiting_normal'] or record_paused</attribute>
</xpath>
<xpath expr="//div[hasclass('o_state_container')][1]" position="attributes">
<attribute name="invisible">record_paused or not active</attribute>
</xpath>
<xpath expr="//div[hasclass('o_state_container')][2]" position="attributes">
<attribute name="invisible">record_paused or active</attribute>
</xpath>
</field>
</record>
<record id="view_task_kanban_inherit" model="ir.ui.view">
<field name="name">project.task.kanban.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban"/>
<field name="arch" type="xml">
<xpath expr="//kanban" position="inside">
<!-- Paused Ribbon -->
<div t-if="record.record_paused.value"
class="ribbon ribbon-top-right">
<span>PAUSED</span>
</div>
</xpath>
</field>
</record>
<!-- <record id="project.action_view_my_task" model="ir.actions.act_window">-->
<!-- <field name="name">My Tasks</field>-->
<!-- <field name="res_model">project.task</field>-->

View File

@ -5,9 +5,9 @@
<field name="model">internal.teams</field>
<field name="arch" type="xml">
<list>
<field name="team_name"/>
<field name="complete_name"/>
<field name="team_lead"/>
<field name="members_ids" widget="many2many_tags"/>
<field name="all_members_ids" widget="many2many_tags"/>
</list>
</field>
</record>
@ -19,6 +19,7 @@
<form>
<sheet>
<group>
<field name="complete_name" invisible="1"/>
<field name="team_name"/>
<field name="team_lead"/>
<!-- ✅ added -->
@ -27,7 +28,9 @@
<notebook>
<page name="team_members" string="Team">
<field name="members_ids" widget="many2many">
<field name="members_ids" invisible="1"/>
<button name="add_internal_team_members" type="object" class="btn-primary" string="Update" style="margin-bottom: 10px;"/>
<field name="all_members_ids" widget="many2many">
<kanban quick_create="false" create="false" delete="true">
<field name="id"/>
<field name="name"/>
@ -53,10 +56,6 @@
</templates>
</kanban>
</field>
<!-- ✅ added: show combined team members including child team leads and members -->
<field name="all_members_ids" widget="many2many_tags" readonly="1"
string="Including Child Teams"/>
</page>
<!-- ✅ added: Child Teams tab -->
@ -76,6 +75,50 @@
</field>
</record>
<record id="view_internal_teams_search" model="ir.ui.view">
<field name="name">internal.teams.search</field>
<field name="model">internal.teams</field>
<field name="arch" type="xml">
<search string="Search Teams">
<field name="team_name" string="Team Name" filter_domain="[('team_name','ilike',self)]"/>
<field name="team_lead" string="Team Lead"/>
<field name="members_ids" string="Team Members"/>
<field name="parent_id" string="Parent Team"/>
<field name="complete_name" string="Full Path"/>
<filter string="Active Teams" name="active" domain="[('active','=',True)]"/>
<filter string="Inactive Teams" name="inactive" domain="[('active','=',False)]"/>
<filter string="Teams with Child Teams"
name="has_child_teams"
domain="[('child_ids','!=',False)]"/>
<filter string="Teams without Child Teams"
name="no_child_teams"
domain="[('child_ids','=',False)]"/>
<filter string="Root Teams (No Parent)"
name="root_teams"
domain="[('parent_id','=',False)]"/>
<filter string="Child Teams (Has Parent)"
name="child_teams"
domain="[('parent_id','!=',False)]"/>
<!-- Group By -->
<group expand="0" string="Group By">
<filter string="Parent Team"
name="group_by_parent"
context="{'group_by':'parent_id'}"/>
<filter string="Team Lead"
name="group_by_lead"
context="{'group_by':'team_lead'}"/>
<filter string="Active Status"
name="group_by_active"
context="{'group_by':'active'}"/>
</group>
</search>
</field>
</record>
<record id="internal_teams_action_tree" model="ir.actions.act_window">
<field name="name">Internal Teams</field>
<field name="res_model">internal.teams</field>

View File

@ -1 +1,4 @@
from . import project_user_assign_wizard
from . import internal_team_members_wizard
from . import project_stage_update_wizard
from . import task_reject_reason_wizard

View File

@ -0,0 +1,44 @@
from odoo import models, fields, api
import logging
_logger = logging.getLogger(__name__)
class InternalTeamMembersWizard(models.TransientModel):
_name = 'internal.team.members.wizard'
_description = 'Add Internal Team Members Wizard'
user_ids = fields.Many2many(
'res.users',
string='Select Members',
help="Select users to add to the team"
)
# def action_add_members(self):
# self.ensure_one()
# team_id = self.env['internal.teams'].browse(self._context.get('active_id'))
# if team_id:
# team_id.members_ids |= self.user_ids
# return {'type': 'ir.actions.act_window_close'}
def action_add_members(self):
self.ensure_one()
team_id = self.env['internal.teams'].browse(self._context.get('active_id'))
if team_id:
# Get current members
current_members = team_id.members_ids
selected_members = self.user_ids
# Find members to add and remove
to_add = selected_members - current_members
to_remove = current_members - selected_members
# Update the members list
team_id.members_ids = [(6, 0, selected_members.ids)]
# Optional: Log the changes
if to_add:
_logger.info(f"Added users {to_add.mapped('name')} to team {team_id.team_name}")
if to_remove:
_logger.info(f"Removed users {to_remove.mapped('name')} from team {team_id.team_name}")
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_internal_team_members_wizard_form" model="ir.ui.view">
<field name="name">Internal Team Members Wizard Form</field>
<field name="model">internal.team.members.wizard</field>
<field name="arch" type="xml">
<form string="Add Team Members">
<group>
<field name="user_ids" widget="many2many_tags" options="{'no_create': True}"/>
</group>
<footer>
<button name="action_add_members" string="Update" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_internal_team_members_wizard" model="ir.actions.act_window">
<field name="name">Add Team Members</field>
<field name="res_model">internal.team.members.wizard</field>
<field name="binding_view_types">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@ -0,0 +1,70 @@
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)
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
existing_stage = self.env['project.task.type'].search([
('name', '=', self.name),
('team_id', '=', self.team_id.id),
('approval_by', '=', self.approval_by),
('fold', '=', self.fold),
], limit=1)
if existing_stage:
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
})
# 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:
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'}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_project_stage_update_wizard_form" model="ir.ui.view">
<field name="name">project.stage.update.wizard.form</field>
<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"/>
<field name="approval_by"/>
<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"/>
</footer>
</form>
</field>
</record>
<record id="action_project_stage_update_wizard" model="ir.actions.act_window">
<field name="name">Project Stage Update</field>
<field name="res_model">project.stage.update.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@ -0,0 +1,22 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class TaskRejectReasonWizard(models.TransientModel):
_name = "task.reject.reason.wizard"
_description = "Task Rejection Reason Wizard"
reason = fields.Text(string="Rejection Reason", required=True)
task_id = fields.Many2one("project.task", string="Task", required=True)
def action_reject(self):
"""Trigger the rejection action on the selected task"""
self.ensure_one()
if not self.reason:
raise UserError(_("Please enter a reason for rejection."))
# Call the existing reject method on the task
self.task_id.reject_and_return(reason=self.reason)
return {
"type": "ir.actions.act_window_close"
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_task_reject_reason_wizard" model="ir.ui.view">
<field name="name">task.reject.reason.wizard.form</field>
<field name="model">task.reject.reason.wizard</field>
<field name="arch" type="xml">
<form string="Reject Task">
<group>
<field name="reason" placeholder="Enter the reason for rejection..."/>
</group>
<footer>
<button string="Reject" type="object" name="action_reject" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_task_reject_reason_wizard" model="ir.actions.act_window">
<field name="name">Reject Task</field>
<field name="res_model">task.reject.reason.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>