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