diff --git a/addons_extensions/module_selector_sidebar/__manifest__.py b/addons_extensions/module_selector_sidebar/__manifest__.py
index 53da8e514..5c49c808a 100644
--- a/addons_extensions/module_selector_sidebar/__manifest__.py
+++ b/addons_extensions/module_selector_sidebar/__manifest__.py
@@ -12,4 +12,4 @@
],
},
"installable": True,
-}
\ No newline at end of file
+}
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_task.py b/addons_extensions/project_task_timesheet_extended/models/project_task.py
index ae3b79e44..400319a2d 100644
--- a/addons_extensions/project_task_timesheet_extended/models/project_task.py
+++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py
@@ -1025,6 +1025,23 @@ class projectTask(models.Model):
if stage and stage.mail_template_id:
stage.mail_template_id.sudo().send_mail(self.id, force_send=True)
+ def _notify_reassigned_users(self, users, note):
+ self.ensure_one()
+ partners = users.mapped('partner_id')
+ if not partners:
+ return
+ task_name = self.display_name or self.name
+ body = Markup("
%s
%s
") % (
+ _("You have been re-assigned to task %s.") % task_name,
+ note,
+ )
+ self.with_context(mail_notify_force_send=True).message_notify(
+ partner_ids=partners.ids,
+ subject=_("Task Re-assigned: %s") % task_name,
+ body=body,
+ email_layout_xmlid="mail.mail_notification_light",
+ )
+
def _apply_task_stage_users(self, stage, users, reason=False, reassignment=False):
self.ensure_one()
if not stage:
@@ -1051,16 +1068,19 @@ class projectTask(models.Model):
self._post_to_project_channel(note, users.mapped('partner_id'))
self._send_stage_mail_template(stage)
if reassignment:
+ self._notify_reassigned_users(users, note)
self._append_reassignment_history(stage, users, reason)
def _apply_task_stage_user_lines(self, lines, reason=False, reassignment=False, update_assignees=True):
self.ensure_one()
self._ensure_task_timeline_rows()
selected_users = self.env['res.users']
+ replaced_users = self.env['res.users']
notes = []
for wizard_line in lines:
stage = wizard_line.stage_id
- users = wizard_line.user_ids
+ user = getattr(wizard_line, 'user_id', False)
+ users = user or getattr(wizard_line, 'user_ids', self.env['res.users'])
responsible_user = getattr(wizard_line, 'responsible_user_id', False)
if not stage or not users:
continue
@@ -1068,22 +1088,33 @@ class projectTask(models.Model):
timeline = self.assignees_timelines.filtered(lambda t: t.stage_id == stage)[:1]
primary_user = users[0]
if timeline:
+ if reassignment and timeline.assigned_to:
+ replaced_users |= timeline.assigned_to
timeline.sudo().write({
'assigned_to': primary_user.id,
'responsible_lead': responsible_user.id if responsible_user else timeline.responsible_lead.id or primary_user.id,
'team_id': stage.team_id.id if stage.team_id else timeline.team_id.id,
})
- notes.append(_("%s: %s") % (stage.name, ", ".join(users.mapped('name'))))
+ line_reason = getattr(wizard_line, 'reason', False) or reason
+ note = _("%s: %s") % (stage.name, ", ".join(users.mapped('name')))
+ if line_reason:
+ note += _(". Reason: %s") % line_reason
+ notes.append(note)
self._send_stage_mail_template(stage)
if reassignment:
- self._append_reassignment_history(stage, users, reason)
+ self._append_reassignment_history(stage, users, line_reason)
if not selected_users:
raise UserError(_("Please select at least one user."))
- if update_assignees:
- self.sudo().write({'user_ids': [(6, 0, selected_users.ids)]})
- self.sudo().write({'involved_user_ids': [(6, 0, selected_users.ids)]})
+ if reassignment:
+ assignees = (self.user_ids - replaced_users) | selected_users
+ self.sudo().write({'user_ids': [(6, 0, assignees.ids)]})
+ self._sync_involved_assignees_from_timelines()
+ else:
+ if update_assignees:
+ self.sudo().write({'user_ids': [(6, 0, selected_users.ids)]})
+ self.sudo().write({'involved_user_ids': [(6, 0, selected_users.ids)]})
action = _("Re-assigned") if reassignment else _("Timeline users selected")
note = _("%s: %s") % (action, "; ".join(notes))
if reason:
@@ -1096,6 +1127,8 @@ class projectTask(models.Model):
subtype_xmlid='mail.mt_comment',
)
self._post_to_project_channel(note, selected_users.mapped('partner_id'))
+ if reassignment:
+ self._notify_reassigned_users(selected_users, note)
def _move_to_selected_stage(self, stage):
self.ensure_one()
diff --git a/addons_extensions/project_task_timesheet_extended/security/security.xml b/addons_extensions/project_task_timesheet_extended/security/security.xml
index 86cfd666f..44d8b4fdc 100644
--- a/addons_extensions/project_task_timesheet_extended/security/security.xml
+++ b/addons_extensions/project_task_timesheet_extended/security/security.xml
@@ -38,6 +38,28 @@
+
+
+
+
+
+
+
+
+
+ [(1, '=', 1)]
+
+
+
+
+
+
+
+
+
+
+
+
company: Own Company
@@ -48,10 +70,10 @@
Manager: Own Projects
- [('user_id', '=', user.id)]
+ ['|', ('user_id', '=', user.id), ('user_id', '=', False)]
-
+
@@ -95,11 +117,33 @@
-
+
+
+
+
+
+
+
+
+
+
+ [(1, '=', 1)]
+
+
+
+
+
+
+
+
+
+
+
+
[
'&',
diff --git a/addons_extensions/project_task_timesheet_extended/static/src/css/delopyment.css b/addons_extensions/project_task_timesheet_extended/static/src/css/delopyment.css
index c451556fb..e76981969 100644
--- a/addons_extensions/project_task_timesheet_extended/static/src/css/delopyment.css
+++ b/addons_extensions/project_task_timesheet_extended/static/src/css/delopyment.css
@@ -62,22 +62,9 @@
max-width: calc(100% - 18px);
}
-.o_task_reassign_arrow {
- padding: 0;
- margin-left: 2px;
- min-width: 12px;
- width: 12px;
- height: 12px;
- line-height: 12px;
- color: #374151;
- vertical-align: middle;
-}
-
-.o_task_reassign_arrow .fa {
- font-size: 10px;
- line-height: 12px;
-}
-
-.o_task_reassign_arrow:hover {
- color: #017e84;
-}
+.o_task_reassign_button {
+ margin-left: 8px;
+ padding: 2px 10px;
+ line-height: 20px;
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/view/project_task.xml b/addons_extensions/project_task_timesheet_extended/view/project_task.xml
index 3cb84e356..919ec7660 100644
--- a/addons_extensions/project_task_timesheet_extended/view/project_task.xml
+++ b/addons_extensions/project_task_timesheet_extended/view/project_task.xml
@@ -29,7 +29,7 @@
-
+
@@ -293,7 +293,6 @@
-
diff --git a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py
index e770eb2d5..0622c2039 100644
--- a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py
+++ b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.py
@@ -63,7 +63,7 @@ class TaskBackStageWizard(models.TransientModel):
for wizard in self:
stages = wizard.task_id.project_id.type_ids
if wizard.task_id.stage_id:
- stages = stages.filtered(lambda s: s.id != wizard.task_id.stage_id.id)
+ stages = stages.filtered(lambda s: s.id != wizard.task_id.stage_id.id and s.sequence < wizard.task_id.stage_id.sequence)
wizard.available_stage_ids = [(6, 0, stages.ids)]
def action_confirm(self):
@@ -89,7 +89,7 @@ class TaskRequestTimelinesWizard(models.TransientModel):
(0, 0, {
"stage_id": line.stage_id.id,
"responsible_user_id": line.responsible_lead.id,
- "user_ids": [(6, 0, line.assigned_to.ids)],
+ "user_id": line.assigned_to.id,
})
for line in task.assignees_timelines.sorted(lambda l: l.stage_sequence)
if line.stage_id
@@ -98,7 +98,7 @@ class TaskRequestTimelinesWizard(models.TransientModel):
def action_apply(self):
self.ensure_one()
- selected_lines = self.line_ids.filtered(lambda line: line.stage_id and line.user_ids)
+ selected_lines = self.line_ids.filtered(lambda line: line.stage_id and line.user_id)
if not selected_lines:
raise UserError(_("Please select users for at least one stage."))
self.task_id.sudo().timelines_requested = True
@@ -114,18 +114,41 @@ class TaskRequestTimelinesWizardLine(models.TransientModel):
task_id = fields.Many2one(related="wizard_id.task_id", store=False)
stage_id = fields.Many2one("project.task.type", string="Stage")
responsible_user_id = fields.Many2one("res.users", string="Responsible User")
- user_ids = fields.Many2many("res.users", string="Assignees")
+ user_id = fields.Many2one("res.users", string="Assignee")
available_user_ids = fields.Many2many("res.users", compute="_compute_available_user_ids")
- @api.depends("stage_id", "task_id")
+ def _get_project_involved_users(self):
+ self.ensure_one()
+ users = self.env["res.users"]
+ task = self.task_id
+ project = task.project_id
+ if project:
+ users |= project.members_ids | project.user_id | project.project_lead | project.project_sponsor
+ if self.stage_id:
+ users |= self.stage_id.involved_user_ids
+ if self.stage_id.team_id:
+ users |= self.stage_id.team_id.team_lead | self.stage_id.team_id.all_members_ids
+ if task:
+ users |= task.user_ids | task.involved_user_ids
+ return users.filtered(lambda user: user.active and not user.share)
+
+ @api.depends(
+ "stage_id",
+ "stage_id.involved_user_ids",
+ "stage_id.team_id",
+ "stage_id.team_id.team_lead",
+ "stage_id.team_id.all_members_ids",
+ "task_id",
+ "task_id.user_ids",
+ "task_id.involved_user_ids",
+ "task_id.project_id.members_ids",
+ "task_id.project_id.user_id",
+ "task_id.project_id.project_lead",
+ "task_id.project_id.project_sponsor",
+ )
def _compute_available_user_ids(self):
for line in self:
- users = line.stage_id.involved_user_ids
- if line.stage_id.team_id:
- users |= line.stage_id.team_id.all_members_ids
- if line.task_id.project_id:
- users |= line.task_id.project_id.members_ids | line.task_id.project_id.user_id | line.task_id.project_id.project_lead
- line.available_user_ids = [(6, 0, users.filtered(lambda u: not u.share).ids)]
+ line.available_user_ids = [(6, 0, line._get_project_involved_users().ids)]
class TaskReassignAssigneesWizard(models.TransientModel):
@@ -133,10 +156,8 @@ class TaskReassignAssigneesWizard(models.TransientModel):
_description = "Task Re-Assign Assignees Wizard"
task_id = fields.Many2one("project.task", string="Task", required=True)
- reason = fields.Text(string="Re-assignment Reason", required=True)
stage_ids = fields.Many2many("project.task.type", string="Stages", required=True)
available_stage_ids = fields.Many2many("project.task.type", compute="_compute_available_stage_ids")
- user_ids = fields.Many2many("res.users", string="New Assignees", required=True)
available_user_ids = fields.Many2many("res.users", compute="_compute_available_user_ids")
line_ids = fields.One2many("task.reassign.assignees.wizard.line", "wizard_id", string="Selected Stage Assignees")
@@ -146,14 +167,12 @@ class TaskReassignAssigneesWizard(models.TransientModel):
task = self.env["project.task"].browse(values.get("task_id") or self.env.context.get("default_task_id"))
if task:
default_stage = self.env["project.task.type"].browse(self.env.context.get("default_stage_id")) or task.stage_id
- default_users = task.user_ids
values.setdefault("stage_ids", [(6, 0, default_stage.ids)])
- values.setdefault("user_ids", [(6, 0, default_users.ids)])
if "line_ids" in fields_list and default_stage:
values["line_ids"] = [
(0, 0, {
"stage_id": stage.id,
- "user_ids": [(6, 0, default_users.ids)],
+ "user_id": task.assignees_timelines.filtered(lambda line: line.stage_id == stage)[:1].assigned_to.id,
})
for stage in default_stage
]
@@ -173,34 +192,46 @@ class TaskReassignAssigneesWizard(models.TransientModel):
if stage.team_id:
users |= stage.team_id.all_members_ids
if wizard.task_id.project_id:
- users |= wizard.task_id.project_id.members_ids | wizard.task_id.project_id.user_id | wizard.task_id.project_id.project_lead
- wizard.available_user_ids = [(6, 0, users.filtered(lambda u: not u.share).ids)]
+ users |= wizard.task_id.project_id.members_ids | wizard.task_id.project_id.user_id | wizard.task_id.project_id.project_lead | wizard.task_id.project_id.project_sponsor
+ wizard.available_user_ids = [(6, 0, users.filtered(lambda u: u.active and not u.share).ids)]
- @api.onchange("stage_ids", "user_ids")
- def _onchange_stage_or_users(self):
+ def _get_default_user_for_stage(self, stage):
+ self.ensure_one()
+ timeline = self.task_id.assignees_timelines.filtered(lambda line: line.stage_id == stage)[:1]
+ return timeline.assigned_to.id if timeline else False
+
+ def _preferred_existing_line(self, stage):
+ self.ensure_one()
+ lines = self.line_ids.filtered(lambda line: line.stage_id == stage)
+ return lines.filtered(lambda line: line.user_id or line.reason)[:1] or lines[:1]
+
+ @api.onchange("stage_ids")
+ def _onchange_stage_ids(self):
+ Line = self.env["task.reassign.assignees.wizard.line"]
for wizard in self:
- existing_users_by_stage = {
- line.stage_id.id: line.user_ids.ids
- for line in wizard.line_ids
- if line.stage_id
- }
- common_user_ids = wizard.user_ids.ids
- wizard.line_ids = [(5, 0, 0)] + [
- (0, 0, {
- "stage_id": stage.id,
- "user_ids": [(6, 0, common_user_ids or existing_users_by_stage.get(stage.id, []))],
- })
- for stage in wizard.stage_ids.sorted(lambda s: s.sequence)
- ]
+ lines = Line
+ for stage in wizard.stage_ids.sorted(lambda s: s.sequence):
+ existing_line = wizard._preferred_existing_line(stage)
+ if existing_line:
+ lines |= existing_line
+ else:
+ lines |= Line.new({
+ "stage_id": stage.id,
+ "user_id": wizard._get_default_user_for_stage(stage),
+ })
+ wizard.line_ids = lines
def action_confirm(self):
self.ensure_one()
- selected_lines = self.line_ids.filtered(lambda line: line.stage_id and line.user_ids)
+ selected_lines = self.line_ids.filtered(lambda line: line.stage_id in self.stage_ids and line.user_id)
if not selected_lines:
raise UserError(_("Please select stages and new assignees."))
- self.task_id._apply_task_stage_user_lines(selected_lines, reason=self.reason, reassignment=True)
+ if any(not line.reason for line in selected_lines):
+ raise UserError(_("Please enter a reason for each selected stage."))
+ self.task_id._apply_task_stage_user_lines(selected_lines, reassignment=True)
return {"type": "ir.actions.act_window_close"}
+
class TaskReassignAssigneesWizardLine(models.TransientModel):
_name = "task.reassign.assignees.wizard.line"
_description = "Task Re-Assign Assignees Stage User Line"
@@ -208,15 +239,39 @@ class TaskReassignAssigneesWizardLine(models.TransientModel):
wizard_id = fields.Many2one("task.reassign.assignees.wizard", required=True, ondelete="cascade")
task_id = fields.Many2one(related="wizard_id.task_id", store=False)
stage_id = fields.Many2one("project.task.type", string="Stage")
- user_ids = fields.Many2many("res.users", string="New Assignees")
+ user_id = fields.Many2one("res.users", string="New Assignee")
+ reason = fields.Text(string="Reason")
available_user_ids = fields.Many2many("res.users", compute="_compute_available_user_ids")
- @api.depends("stage_id", "task_id")
+ def _get_project_involved_users(self):
+ self.ensure_one()
+ users = self.env["res.users"]
+ task = self.task_id
+ project = task.project_id
+ if project:
+ users |= project.members_ids | project.user_id | project.project_lead | project.project_sponsor
+ if self.stage_id:
+ users |= self.stage_id.involved_user_ids
+ if self.stage_id.team_id:
+ users |= self.stage_id.team_id.team_lead | self.stage_id.team_id.all_members_ids
+ if task:
+ users |= task.user_ids | task.involved_user_ids
+ return users.filtered(lambda user: user.active and not user.share)
+
+ @api.depends(
+ "stage_id",
+ "stage_id.involved_user_ids",
+ "stage_id.team_id",
+ "stage_id.team_id.team_lead",
+ "stage_id.team_id.all_members_ids",
+ "task_id",
+ "task_id.user_ids",
+ "task_id.involved_user_ids",
+ "task_id.project_id.members_ids",
+ "task_id.project_id.user_id",
+ "task_id.project_id.project_lead",
+ "task_id.project_id.project_sponsor",
+ )
def _compute_available_user_ids(self):
for line in self:
- users = line.stage_id.involved_user_ids
- if line.stage_id.team_id:
- users |= line.stage_id.team_id.all_members_ids
- if line.task_id.project_id:
- users |= line.task_id.project_id.members_ids | line.task_id.project_id.user_id | line.task_id.project_id.project_lead
- line.available_user_ids = [(6, 0, users.filtered(lambda u: not u.share).ids)]
+ line.available_user_ids = [(6, 0, line._get_project_involved_users().ids)]
diff --git a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml
index 855ce3bd1..172787ec9 100644
--- a/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml
+++ b/addons_extensions/project_task_timesheet_extended/wizards/task_reject_reason_wizard.xml
@@ -79,9 +79,9 @@
-
+
-
+