Project Task Extended Changes
This commit is contained in:
parent
2dbdb58127
commit
66077d1819
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 & 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>
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
@ -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">[-->
|
||||
<!-- '&','&', '&', '&', '&','&',-->
|
||||
<!-- ('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]),-->
|
||||
<!-- '|',-->
|
||||
<!-- '&',-->
|
||||
<!-- ('task_id.is_generic', '=', False),-->
|
||||
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
|
||||
<!-- '&',-->
|
||||
<!-- ('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">[-->
|
||||
<!-- '&', '&', '&',-->
|
||||
<!-- ('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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -7,21 +7,90 @@
|
|||
<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 & 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 & 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="project.action_view_my_task" model="ir.actions.act_window">-->
|
||||
<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>-->
|
||||
<!-- <field name="view_mode">kanban,list,form,calendar,pivot,graph,activity</field>-->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
from . import project_user_assign_wizard
|
||||
from . import project_user_assign_wizard
|
||||
from . import internal_team_members_wizard
|
||||
from . import project_stage_update_wizard
|
||||
from . import task_reject_reason_wizard
|
||||
|
|
@ -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'}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue