diff --git a/addons_extensions/project_task_timesheet_extended/TASK_PROJECT_BY_MODULE.md b/addons_extensions/project_task_timesheet_extended/TASK_PROJECT_BY_MODULE.md
new file mode 100644
index 000000000..97553ebc7
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/TASK_PROJECT_BY_MODULE.md
@@ -0,0 +1,277 @@
+# Task Document: Project by Module
+
+## Requirement
+
+Add a **Project by Module** submenu in the Odoo **Project** menu.
+
+The menu should show **Project records grouped by Module**, so users can easily view projects module-wise.
+
+## Understanding
+
+Initially, the addon already had a `Module` field on `project.task`:
+
+```python
+model_id = fields.Many2one('project.module.source', string="Module")
+```
+
+But the requirement is to show **projects by module**, not tasks by module. Since `project.project` did not have a module field, a new field was added on the Project model.
+
+## Implementation Summary
+
+The implementation adds:
+
+- A `module_id` field on `project.project`
+- A Module field on the Project form
+- A Project search group-by filter for Module
+- A new Project menu item named **Project by Module**
+- A new action that opens `project.project` grouped by `module_id`
+- Manifest entry to load the new XML file
+
+## Files Changed
+
+### 1. `models/project.py`
+
+Added a new Many2one field on Project:
+
+```python
+module_id = fields.Many2one('project.module.source', string="Module")
+```
+
+Location:
+
+```python
+sequence_name = fields.Char("Project Number", copy=False, readonly=True)
+module_id = fields.Many2one('project.module.source', string="Module")
+task_sequence_id = fields.Many2one(
+ 'ir.sequence',
+ string="Task Sequence",
+ readonly=True,
+ copy=False,
+ help="Sequence for tasks of this project"
+)
+```
+
+### 2. `view/project_by_module.xml`
+
+Created a new XML file to handle form view, search view, action, and menu.
+
+Full code:
+
+```xml
+
+
+
+
+ project.project.form.module.inherit
+ project.project
+
+
+
+
+
+
+
+
+
+ project.project.search.project.by.module
+ project.project
+
+
+
+
+
+
+
+
+
+ Project by Module
+ project.project
+ kanban,list,form,calendar,activity
+
+ {'search_default_group_by_module': 1}
+
+
+ No projects found.
+
+
+ Create projects and set their Module to review projects module-wise.
+
+
+
+
+
+
+
+```
+
+### 3. `__manifest__.py`
+
+Added the new XML file in the data list:
+
+```python
+'view/project_by_module.xml',
+```
+
+Location:
+
+```python
+'view/project_task.xml',
+'view/project.xml',
+'view/project_portfolio.xml',
+'view/project_by_module.xml',
+'view/timesheets.xml',
+```
+
+## Expected Result
+
+After upgrading the module:
+
+1. Open a Project record.
+2. Set the **Module** field.
+3. Go to **Project > Project by Module**.
+4. Odoo opens Project records grouped module-wise.
+
+Example:
+
+```text
+PMT
+ Project A
+ Project B
+
+Finance
+ Project C
+
+ATS
+ Project D
+```
+
+## Module Upgrade Command
+
+Use the appropriate database name:
+
+```bash
+odoo-bin -d your_database -u project_task_timesheet_extended
+```
+
+## Validation Done
+
+- XML syntax checked successfully.
+- Python syntax checked successfully using `compile()`.
+- `py_compile` could not write bytecode because Windows denied write access to `models/__pycache__`, but the Python syntax itself is valid.
+
+## Additional Update: Portfolio on Project Card and Module on Task
+
+After the initial Project by Module work, two display changes were added.
+
+### 1. Portfolio visible on Project Kanban card
+
+Project records already have `portfolio_id`. The project kanban card was extended to show the portfolio name under the project title/metadata area.
+
+Code added in `view/project_by_module.xml`:
+
+```xml
+
+ project.project.kanban.portfolio.inherit
+ project.project
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 2. Task Module comes from Project Module
+
+The task module field now comes from the task's selected project, so the task automatically shows which module it belongs to.
+
+Code changed in `models/project_task.py`:
+
+```python
+model_id = fields.Many2one(
+ 'project.module.source',
+ string="Module",
+ related='project_id.module_id',
+ store=True,
+ readonly=True,
+)
+```
+
+The task form also displays the Module near the task title area and keeps the regular Module field read-only.
+
+Code added in `view/project_by_module.xml`:
+
+```xml
+
+ project.task.form.project.module.display
+ project.task
+
+
+
+
+ Module:
+
+
+
+
+ 1
+ {'no_open': True}
+
+
+
+```
+
+Expected behavior:
+
+- Project kanban cards show their Portfolio when assigned.
+- Task form shows the Module from the parent Project.
+- Users only need to set Module on Project; related tasks display it automatically.
+
+### 3. Task ID and Module visible on Task Kanban card
+
+The task kanban card was extended to show the generated task sequence and the module directly under the task title.
+
+Code added in `view/project_task.xml`:
+
+```xml
+
+ project.task.kanban.task.id.module
+ project.task
+
+
+
+
+
+
+
+
+ Task ID:
+
+
+
+ Module:
+
+
+
+
+
+```
+
+Expected behavior on task kanban cards:
+
+```text
+project task
+Task ID: PROJ-012/TASK-001
+Module: PMT
+```
diff --git a/addons_extensions/project_task_timesheet_extended/__manifest__.py b/addons_extensions/project_task_timesheet_extended/__manifest__.py
index 11c586806..290bd04ab 100644
--- a/addons_extensions/project_task_timesheet_extended/__manifest__.py
+++ b/addons_extensions/project_task_timesheet_extended/__manifest__.py
@@ -52,6 +52,7 @@ Key Features:
'view/project_task.xml',
'view/project.xml',
'view/project_portfolio.xml',
+ 'view/project_by_module.xml',
'view/timesheets.xml',
'view/pro_task_gantt.xml',
'view/user_availability.xml',
@@ -71,3 +72,4 @@ Key Features:
'auto_install': False,
'post_init_hook': 'post_init_hook',
}
+
diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py
index 08f65d2d7..34bc07c7d 100644
--- a/addons_extensions/project_task_timesheet_extended/models/project.py
+++ b/addons_extensions/project_task_timesheet_extended/models/project.py
@@ -25,6 +25,7 @@ class ProjectProject(models.Model):
sequence_name = fields.Char("Project Number", copy=False, readonly=True)
+ module_id = fields.Many2one('project.module.source', string="Module")
task_sequence_id = fields.Many2one(
'ir.sequence',
string="Task Sequence",
@@ -1199,3 +1200,5 @@ class ProjectTask(models.Model):
"""Toggle visibility of project chatter"""
for project in self:
project.show_task_chatter = not project.show_task_chatter
+
+
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 eb5202edd..1a1754b8e 100644
--- a/addons_extensions/project_task_timesheet_extended/models/project_task.py
+++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py
@@ -46,9 +46,20 @@ class projectTask(models.Model):
('normal', 'Normal'),
], compute='_compute_deadline_status')
- model_id = fields.Many2one('project.module.source', string="Module")
+ model_id = fields.Many2one('project.module.source', string="Module", related='project_id.module_id', store=True, readonly=True)
+ task_display_id = fields.Char(string="Task ID", compute="_compute_kanban_display_fields", compute_sudo=True)
+ module_display_name = fields.Char(string="Module", compute="_compute_kanban_display_fields", compute_sudo=True)
allocation_start_date = fields.Date(string="Allocation Start Date")
allocation_end_date = fields.Date(string="Allocation End Date")
+ reassignment_history = fields.Html(string="Reassignment History", readonly=True)
+
+
+ @api.depends('sequence_name', 'project_id', 'project_id.module_id', 'project_id.module_id.name', 'model_id', 'model_id.name')
+ def _compute_kanban_display_fields(self):
+ for task in self:
+ task.task_display_id = task.sequence_name or ("TASK-%03d" % task.id if task.id else "New Task")
+ module = task.model_id or task.project_id.module_id
+ task.module_display_name = module.name if module else "No Module"
@api.depends('date_deadline')
def _compute_deadline_status(self):
@@ -77,14 +88,8 @@ class projectTask(models.Model):
return super(projectTask, self).unlink()
def write(self, vals):
- # Allow stage update for multiple records
- if 'stage_id' in vals:
- result = super(projectTask, self).write(vals)
- if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
- self._sync_allocated_hours_from_allocation_dates()
- if any(field in vals for field in ['user_ids', 'is_generic']):
- self._sync_involved_assignees_from_timelines()
- return result
+ if 'stage_approval' in vals:
+ raise UserError(_("Are you sure you want to change Stage Approval?"))
result = super(projectTask, self).write(vals)
if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
@@ -193,7 +198,7 @@ class projectTask(models.Model):
all_internal_users = Users.search([('share', '=', False)])
for task in self:
- # no project → all internal
+ # no project → all internal
if not task.project_id:
task.assignee_domain_ids = all_internal_users
continue
@@ -769,55 +774,15 @@ class projectTask(models.Model):
task.task_activity_log = Markup(log_entry)
def back_button(self):
- for task in self:
- task.can_edit_approval_flow_stages = True
- 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',
- )
- task.can_edit_approval_flow_stages = False
-
-
+ self.ensure_one()
+ return {
+ "type": "ir.actions.act_window",
+ "name": _("Move Task Back"),
+ "res_model": "task.back.stage.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {"default_task_id": self.id},
+ }
def submit_for_approval(self):
for task in self:
task.can_edit_approval_flow_stages = True
@@ -876,7 +841,7 @@ class projectTask(models.Model):
task.stage_id = n_stage
task.approval_status = "approved"
- activity_log = "%s: ✅ approved by %s and moved to %s" % (
+ activity_log = "%s: ✅ approved by %s and moved to %s" % (
current_stage.name,
self.env.user.employee_id.name,
n_stage.name)
@@ -884,7 +849,7 @@ class projectTask(models.Model):
# Use the helper method to add activity log
task._add_activity_log(activity_log)
- user_notes = "%s: ✅ moved to %s and awaiting your completion" % (
+ user_notes = "%s: ✅ moved to %s and awaiting your completion" % (
task.sequence_name,
n_stage.name
)
@@ -917,9 +882,9 @@ class projectTask(models.Model):
)
else:
task.approval_status = "approved"
- notes = "%s: ✅ Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
+ notes = "%s: ✅ Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
- activity_log = "%s: ✅ approved by %s" % (
+ activity_log = "%s: ✅ approved by %s" % (
current_stage.name,
self.env.user.employee_id.name)
@@ -943,7 +908,7 @@ class projectTask(models.Model):
task.can_edit_approval_flow_stages = False
- def reject_and_return(self, reason=None):
+ def reject_and_return(self, reason=None, return_stage=False):
for task in self:
task.can_edit_approval_flow_stages = True
if not reason:
@@ -955,9 +920,9 @@ class projectTask(models.Model):
# 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)
+ 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" % (
+ activity_log = "%s: âÂÂÂÅ’ rejected by %s: %s" % (
current_stage.name,
self.env.user.employee_id.name,
reason)
@@ -979,6 +944,9 @@ class projectTask(models.Model):
subtype_xmlid='mail.mt_comment',
)
+ if return_stage:
+ task._move_to_selected_stage(return_stage)
+
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
@@ -1012,45 +980,166 @@ class projectTask(models.Model):
}
def request_timelines(self):
- """Populate task timelines with all relevant project stages."""
+ self.ensure_one()
+ return {
+ "type": "ir.actions.act_window",
+ "name": _("Request Timelines"),
+ "res_model": "task.request.timelines.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {
+ "default_task_id": self.id,
+ "default_stage_id": self.stage_id.id,
+ },
+ }
+ def _get_stage_responsible_user(self, stage):
+ self.ensure_one()
+ if stage.approval_by == 'assigned_team_lead' and stage.team_id.team_lead:
+ return stage.team_id.team_lead
+ if stage.approval_by == 'project_manager' and self.project_id.user_id:
+ return self.project_id.user_id
+ if stage.approval_by == 'project_lead' and self.project_id.project_lead:
+ return self.project_id.project_lead
+ return self.project_id.project_lead or self.project_id.user_id
+
+ def _ensure_task_timeline_rows(self):
+ Timeline = self.env['project.task.time.lines'].sudo()
for task in self:
- task.can_edit_approval_flow_stages = True
- 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({
+ line = task.assignees_timelines.filtered(lambda t: t.stage_id == stage)[:1]
+ if line:
+ continue
+ responsible_user = task._get_stage_responsible_user(stage)
+ Timeline.create({
'stage_id': stage.id,
'team_id': stage.team_id.id if stage.team_id else False,
- 'responsible_lead': responsible_user,
- 'assigned_to': responsible_user,
+ 'responsible_lead': responsible_user.id if responsible_user else False,
+ 'assigned_to': responsible_user.id if responsible_user else False,
'estimated_time': 0.0,
'task_id': task.id,
})
- if timeline_vals:
- self.env['project.task.time.lines'].create(timeline_vals)
- task._sync_involved_assignees_from_timelines()
+ def _send_stage_mail_template(self, stage):
+ self.ensure_one()
+ if stage and stage.mail_template_id:
+ stage.mail_template_id.sudo().send_mail(self.id, force_send=True)
- # 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)
- task.can_edit_approval_flow_stages = False
+ def _apply_task_stage_users(self, stage, users, reason=False, reassignment=False):
+ self.ensure_one()
+ if not stage:
+ raise UserError(_("Please select a stage."))
+ if not users:
+ raise UserError(_("Please select at least one user."))
+ self._ensure_task_timeline_rows()
+ line = self.assignees_timelines.filtered(lambda t: t.stage_id == stage)[:1]
+ primary_user = users[0]
+ if line:
+ line.sudo().write({
+ 'assigned_to': primary_user.id,
+ 'responsible_lead': line.responsible_lead.id or primary_user.id,
+ 'team_id': stage.team_id.id if stage.team_id else line.team_id.id,
+ })
+ self.sudo().write({'user_ids': [(6, 0, users.ids)]})
+ self.sudo().write({'involved_user_ids': [(6, 0, users.ids)]})
+ action = _("Re-assigned") if reassignment else _("Timeline users selected")
+ note = _("%s for stage %s: %s") % (action, stage.name, ", ".join(users.mapped('name')))
+ if reason:
+ note += _(". Reason: %s") % reason
+ self._add_activity_log(note)
+ self.message_post(body=note, partner_ids=users.mapped('partner_id').ids, message_type='notification', subtype_xmlid='mail.mt_comment')
+ self._post_to_project_channel(note, users.mapped('partner_id'))
+ self._send_stage_mail_template(stage)
+ if reassignment:
+ self._append_reassignment_history(stage, users, reason)
+
+ def _apply_task_stage_user_lines(self, lines, reason=False, reassignment=False):
+ self.ensure_one()
+ self._ensure_task_timeline_rows()
+ selected_users = self.env['res.users']
+ notes = []
+ for wizard_line in lines:
+ stage = wizard_line.stage_id
+ users = wizard_line.user_ids
+ if not stage or not users:
+ continue
+ selected_users |= users
+ timeline = self.assignees_timelines.filtered(lambda t: t.stage_id == stage)[:1]
+ primary_user = users[0]
+ if timeline:
+ timeline.sudo().write({
+ 'assigned_to': primary_user.id,
+ 'responsible_lead': 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'))))
+ self._send_stage_mail_template(stage)
+ if reassignment:
+ self._append_reassignment_history(stage, users, reason)
+
+ if not selected_users:
+ raise UserError(_("Please select at least one user."))
+
+ 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:
+ note += _(". Reason: %s") % reason
+ self._add_activity_log(note)
+ self.message_post(
+ body=note,
+ partner_ids=selected_users.mapped('partner_id').ids,
+ message_type='notification',
+ subtype_xmlid='mail.mt_comment',
+ )
+ self._post_to_project_channel(note, selected_users.mapped('partner_id'))
+
+ def _move_to_selected_stage(self, stage):
+ self.ensure_one()
+ if not stage:
+ raise UserError(_("Please select a stage."))
+ old_stage = self.stage_id
+ self.can_edit_approval_flow_stages = True
+ self.approval_status = False
+ self.stage_id = stage
+ self.can_edit_approval_flow_stages = False
+ line = self.assignees_timelines.filtered(lambda t: t.stage_id == stage)[:1]
+ responsible_user = line.assigned_to or line.responsible_lead or self._get_stage_responsible_user(stage)
+ note = _("%s moved task from %s to %s") % (self.env.user.name, old_stage.name if old_stage else "", stage.name)
+ self._add_activity_log(note)
+ partner_ids = responsible_user.partner_id.ids if responsible_user else []
+ self.message_post(body=note, partner_ids=partner_ids, message_type='notification', subtype_xmlid='mail.mt_comment')
+ self._post_to_project_channel(note, responsible_user.partner_id if responsible_user else False)
+ self._send_stage_mail_template(stage)
+
+ def _append_reassignment_history(self, stage, users, reason):
+ self.ensure_one()
+ formatted_datetime = self._get_current_datetime_formatted()
+ entry = "[%s] Stage: %s | Users: %s | Reason: %s" % (
+ formatted_datetime,
+ stage.name,
+ ", ".join(users.mapped('name')),
+ reason or "",
+ )
+ if self.reassignment_history:
+ self.reassignment_history = Markup(self.reassignment_history) + Markup(' ') + Markup(entry)
+ else:
+ self.reassignment_history = Markup(entry)
+
+ def action_open_reassign_wizard(self):
+ self.ensure_one()
+ return {
+ "type": "ir.actions.act_window",
+ "name": _("Re-Assign Assignees"),
+ "res_model": "task.reassign.assignees.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {
+ "default_task_id": self.id,
+ "default_stage_id": self.stage_id.id,
+ },
+ }
@api.model_create_multi
def create(self, vals_list):
@@ -1599,6 +1688,7 @@ class projectTaskTimelines(models.Model):
estimated_start_datetime = fields.Datetime(string="Estimated Start Date Time")
estimated_end_datetime = fields.Datetime(string="Estimated End Date Time")
+
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
@@ -1716,3 +1806,10 @@ class projectTaskTimelines(models.Model):
allowed_teams |= team
allowed_teams |= team.child_ids
rec.allowed_team_ids = allowed_teams
+
+
+
+
+
+
+
diff --git a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
index 91ecad012..7ad8b014d 100644
--- a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
+++ b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
@@ -95,3 +95,8 @@ access_project_resource_contract_period_manager,project.resource.contract.period
access_timesheets_user,timesheets user,model_account_analytic_line,base.group_user,1,1,1,0
access_stage_approval_wizard,stage.approval.wizard,model_stage_approval_wizard,,1,1,1,1
+access_task_back_stage_wizard,task.back.stage.wizard,model_task_back_stage_wizard,base.group_user,1,1,1,1
+access_task_request_timelines_wizard,task.request.timelines.wizard,model_task_request_timelines_wizard,base.group_user,1,1,1,1
+access_task_reassign_assignees_wizard,task.reassign.assignees.wizard,model_task_reassign_assignees_wizard,base.group_user,1,1,1,1
+access_task_request_timelines_wizard_line,task.request.timelines.wizard.line,model_task_request_timelines_wizard_line,base.group_user,1,1,1,1
+access_task_reassign_assignees_wizard_line,task.reassign.assignees.wizard.line,model_task_reassign_assignees_wizard_line,base.group_user,1,1,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 795b5b0b4..c451556fb 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
@@ -49,4 +49,35 @@
.badge {
font-weight: bold;
color: #2d8f2d;
+}
+.o_task_assignees_inline {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ width: 100%;
+}
+
+.o_task_assignees_inline .o_field_widget {
+ flex: 0 1 auto;
+ 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;
}
diff --git a/addons_extensions/project_task_timesheet_extended/view/project_by_module.xml b/addons_extensions/project_task_timesheet_extended/view/project_by_module.xml
new file mode 100644
index 000000000..c2183fdd4
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/view/project_by_module.xml
@@ -0,0 +1,85 @@
+
+
+
+
+ project.project.form.module.inherit
+ project.project
+
+
+
+
+
+
+
+
+
+ project.project.search.project.by.module
+ project.project
+
+
+
+
+
+
+
+
+
+ Project by Module
+ project.project
+ kanban,list,form,calendar,activity
+
+ {'search_default_group_by_module': 1}
+
+
+ No projects found.
+
+
+ Create projects and set their Module to review projects module-wise.
+