project enhancements

This commit is contained in:
karuna 2026-05-26 15:03:48 +05:30
parent 96493be796
commit 923304f759
10 changed files with 1040 additions and 222 deletions

View File

@ -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
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="project_project_form_module_inherit" model="ir.ui.view">
<field name="name">project.project.form.module.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project_task_timesheet_extended.project_project_inherit_form_view2"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='project_lead']" position="after">
<field name="module_id" options="{'no_open': True}"/>
</xpath>
</field>
</record>
<record id="project_project_search_project_by_module" model="ir.ui.view">
<field name="name">project.project.search.project.by.module</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_project_filter"/>
<field name="arch" type="xml">
<xpath expr="//search" position="inside">
<filter name="group_by_module" string="Module" context="{'group_by': 'module_id'}"/>
</xpath>
</field>
</record>
<record id="action_project_by_module" model="ir.actions.act_window">
<field name="name">Project by Module</field>
<field name="res_model">project.project</field>
<field name="view_mode">kanban,list,form,calendar,activity</field>
<field name="search_view_id" ref="project.view_project_project_filter"/>
<field name="context">{'search_default_group_by_module': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No projects found.
</p>
<p>
Create projects and set their Module to review projects module-wise.
</p>
</field>
</record>
<menuitem id="menu_project_by_module"
name="Project by Module"
parent="project.menu_main_pm"
action="action_project_by_module"
sequence="25"/>
</data>
</odoo>
```
### 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
<record id="project_project_kanban_portfolio_inherit" model="ir.ui.view">
<field name="name">project.project.kanban.portfolio.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban"/>
<field name="arch" type="xml">
<xpath expr="//kanban/templates" position="before">
<field name="portfolio_id"/>
</xpath>
<xpath expr="//span[@name='partner_name']" position="after">
<span name="portfolio_name" class="text-muted d-flex align-items-baseline" t-if="record.portfolio_id.value">
<span class="fa fa-folder-open me-2" aria-label="Portfolio" title="Portfolio"></span>
<field class="text-truncate" name="portfolio_id"/>
</span>
</xpath>
</field>
</record>
```
### 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
<record id="project_task_form_project_module_display" model="ir.ui.view">
<field name="name">project.task.form.project.module.display</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project_task_timesheet_extended.project_task_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="inside">
<div class="text-muted mt-1" invisible="not model_id">
<span>Module: </span>
<field name="model_id" readonly="1" nolabel="1" options="{'no_open': True}"/>
</div>
</xpath>
<xpath expr="//field[@name='model_id']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="options">{'no_open': True}</attribute>
</xpath>
</field>
</record>
```
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
<record id="project_task_kanban_task_id_module" model="ir.ui.view">
<field name="name">project.task.kanban.task.id.module</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="sequence_name"/>
<field name="model_id"/>
</xpath>
<xpath expr="//t[@t-name='card']//field[@name='name']" position="after">
<div class="text-muted mt-1 small" t-if="record.sequence_name.raw_value">
<span>Task ID: </span>
<field name="sequence_name"/>
</div>
<div class="text-muted small" t-if="record.model_id.raw_value">
<span>Module: </span>
<field name="model_id"/>
</div>
</xpath>
</field>
</record>
```
Expected behavior on task kanban cards:
```text
project task
Task ID: PROJ-012/TASK-001
Module: PMT
```

View File

@ -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',
}

View File

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

View File

@ -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('<br>') + 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

View File

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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
95
96
97
98
99
100
101
102

View File

@ -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;
}

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="project_project_form_module_inherit" model="ir.ui.view">
<field name="name">project.project.form.module.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project_task_timesheet_extended.project_project_inherit_form_view2"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='project_lead']" position="after">
<field name="module_id" options="{'no_open': True}"/>
</xpath>
</field>
</record>
<record id="project_project_search_project_by_module" model="ir.ui.view">
<field name="name">project.project.search.project.by.module</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_project_filter"/>
<field name="arch" type="xml">
<xpath expr="//search" position="inside">
<filter name="group_by_module" string="Module" context="{'group_by': 'module_id'}"/>
</xpath>
</field>
</record>
<record id="action_project_by_module" model="ir.actions.act_window">
<field name="name">Project by Module</field>
<field name="res_model">project.project</field>
<field name="view_mode">kanban,list,form,calendar,activity</field>
<field name="search_view_id" ref="project.view_project_project_filter"/>
<field name="context">{'search_default_group_by_module': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No projects found.
</p>
<p>
Create projects and set their Module to review projects module-wise.
</p>
</field>
</record>
<menuitem id="menu_project_by_module"
name="Project by Module"
parent="project.menu_main_pm"
action="action_project_by_module"
sequence="25"/>
<record id="project_project_kanban_portfolio_inherit" model="ir.ui.view">
<field name="name">project.project.kanban.portfolio.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban"/>
<field name="arch" type="xml">
<xpath expr="//kanban/templates" position="before">
<field name="portfolio_id"/>
</xpath>
<xpath expr="//span[@name='partner_name']" position="after">
<span name="portfolio_name" class="text-muted d-flex align-items-baseline" t-if="record.portfolio_id.value">
<span class="fa fa-folder-open me-2" aria-label="Portfolio" title="Portfolio"></span>
<field class="text-truncate" name="portfolio_id"/>
</span>
</xpath>
</field>
</record>
<record id="project_task_form_project_module_display" model="ir.ui.view">
<field name="name">project.task.form.project.module.display</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project_task_timesheet_extended.project_task_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="inside">
<div class="text-muted mt-1" invisible="not model_id">
<span>Module: </span>
<field name="model_id" readonly="1" nolabel="1" options="{'no_open': True}"/>
</div>
</xpath>
<xpath expr="//field[@name='model_id']" position="attributes">
<attribute name="readonly">1</attribute>
<attribute name="options">{'no_open': True}</attribute>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -24,13 +24,14 @@
<strong>THIS TASK IS CURRENTLY PAUSED</strong>
</div>
</xpath>
<xpath expr="//field[@name='user_ids']" position="attributes">
<attribute name="widget">involved_assignee_avatar_user</attribute>
<attribute name="domain">[('id', 'in', involved_user_ids)]</attribute>
<attribute name="invisible">is_generic</attribute>
<attribute name="options">{'no_create': True, 'no_quick_create': True, 'no_create_edit': True}</attribute>
<xpath expr="//field[@name='user_ids']" position="replace">
<label for="user_ids" string="Assignees"/>
<div class="o_task_assignees_inline">
<field name="user_ids" nolabel="1" widget="involved_assignee_avatar_user" domain="[('id', 'in', involved_user_ids)]" options="{'no_create': True, 'no_quick_create': True, 'no_create_edit': True}" invisible="is_generic"/>
<field name="user_ids" nolabel="1" class="o_task_user_field" options="{'no_open': True, 'no_quick_create': True}" widget="many2many_avatar_user" domain="[('id', 'in', assignee_domain_ids)]" invisible="not is_generic"/>
<button name="action_open_reassign_wizard" type="object" icon="fa-caret-down" title="Re-Assign Assignees" class="btn-link o_task_reassign_arrow" invisible="record_paused"/>
</div>
</xpath>
<xpath expr="//field[@name='timesheet_ids']" position="attributes">
<attribute name="context">
{
@ -60,13 +61,12 @@
<!-- <field name="assigned_team"/>-->
<!-- </xpath>-->
<xpath expr="//field[@name='user_ids']" position="after">
<field name="user_ids" class="o_task_user_field" options="{'no_open': True, 'no_quick_create': True}" widget="many2many_avatar_user" domain="[('id', 'in', assignee_domain_ids)]" invisible="not is_generic"/>
<field name="involved_user_ids" widget="many2many_avatar_user" invisible="is_generic"/>
<field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/>
<field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/>
<xpath expr="//div[hasclass('o_task_assignees_inline')]" position="after">
<field name="involved_user_ids" widget="many2many_avatar_user" invisible="is_generic"/>
<field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/>
<field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/>
</xpath>
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>-->
@ -95,6 +95,9 @@
</list>
</field>
</page>
<page string="Reassignment History" invisible="not reassignment_history">
<field name="reassignment_history" widget="html" readonly="1"/>
</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>
@ -191,28 +194,6 @@
</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>-->
<!-- <field name="context">{-->
<!-- 'search_default_open_tasks': 1,-->
<!-- 'all_task': 0,-->
<!-- 'default_user_ids': [(4, uid)],-->
<!-- 'default_is_custom':True-->
<!-- }</field>-->
<!-- <field name="search_view_id" ref="project.view_task_search_form"/>-->
<!-- <field name="domain">[('user_ids', 'in', uid)]</field>-->
<!-- <field name="help" type="html">-->
<!-- <p class="o_view_nocontent_smiling_face">-->
<!-- No tasks found. Let's create one!-->
<!-- </p>-->
<!-- <p>-->
<!-- Organize your tasks by dispatching them across the pipeline.<br/>-->
<!-- Collaborate efficiently by chatting in real-time or via email.-->
<!-- </p>-->
<!-- </field>-->
<!-- </record>-->
<record id="project.action_view_my_task" model="ir.actions.act_window">
<field name="domain">['|', ('user_ids', 'in', uid), ('involved_user_ids', 'in', uid)]</field>
@ -226,7 +207,100 @@
<xpath expr="//filter[@name='my_tasks']" position="attributes">
<attribute name="domain">['|', ('user_ids', 'in', uid), ('involved_user_ids', 'in', uid)]</attribute>
</xpath>
<xpath expr="//filter[@name='my_tasks']" position="after">
<filter name="task_need_approval" string="Need Approval" domain="[('approval_status', '=', 'submitted')]"/>
<filter name="task_approved" string="Approved" domain="[('approval_status', '=', 'approved')]"/>
<filter name="task_rejected" string="Rejected" domain="[('approval_status', '=', 'refused')]"/>
</xpath> </field>
</record>
<record id="project_task_kanban_task_id_module_base" model="ir.ui.view">
<field name="name">project.task.kanban.task.id.module.base</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban"/>
<field name="priority">80</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
</xpath>
<xpath expr="//t[@t-name='card']//field[@name='name']" position="after">
<div class="text-muted mt-1 small">
<span>Task ID: </span>
<field name="task_display_id"/>
</div>
<div t-if="record.module_display_name.value != 'No Module'" class="text-muted small">
<span>Module: </span>
<field name="module_display_name"/>
</div>
</xpath>
</field>
</record>
<record id="project_task_kanban_task_id_module_my_tasks" model="ir.ui.view">
<field name="name">project.task.kanban.task.id.module.my.tasks</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban_inherit_my_task"/>
<field name="mode">primary</field>
<field name="priority">80</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
</xpath>
</field>
</record>
<record id="project_task_kanban_task_id_module_all_tasks" model="ir.ui.view">
<field name="name">project.task.kanban.task.id.module.all.tasks</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban_inherit_all_task"/>
<field name="mode">primary</field>
<field name="priority">80</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
</xpath>
</field>
</record>
<record id="project_task_kanban_task_id_module_project_tasks" model="ir.ui.view">
<field name="name">project.task.kanban.task.id.module.project.tasks</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban_inherit_view_default_project"/>
<field name="mode">primary</field>
<field name="priority">80</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='subtask_count']" position="after">
<field name="task_display_id"/>
<field name="module_display_name"/>
</xpath>
</field>
</record>
<record id="project.open_view_my_task_list_kanban" model="ir.actions.act_window.view">
<field name="view_id" ref="project_task_timesheet_extended.project_task_kanban_task_id_module_my_tasks"/>
</record>
<record id="project.open_view_all_task_list_kanban" model="ir.actions.act_window.view">
<field name="view_id" ref="project_task_timesheet_extended.project_task_kanban_task_id_module_all_tasks"/>
</record>
<record id="project.project_task_kanban_action_view" model="ir.actions.act_window.view">
<field name="view_id" ref="project_task_timesheet_extended.project_task_kanban_task_id_module_project_tasks"/>
</record>
</data>
</odoo>
</odoo>

View File

@ -1,44 +1,216 @@
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"
}
class ProjectRejectReasonWizard(models.TransientModel):
_name = "project.reject.reason.wizard"
_description = "Project Rejection Reason Wizard"
reason = fields.Text(string="Rejection Reason", required=True)
project_id = fields.Many2one("project.project", string="Project", 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.project_id.reject_and_return(reason=self.reason)
return {
"type": "ir.actions.act_window_close"
}
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)
stage_id = fields.Many2one("project.task.type", string="Return To Stage", required=True)
available_stage_ids = fields.Many2many("project.task.type", compute="_compute_available_stage_ids")
@api.depends("task_id", "task_id.project_id", "task_id.stage_id")
def _compute_available_stage_ids(self):
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)
wizard.available_stage_ids = [(6, 0, stages.ids)]
def action_reject(self):
self.ensure_one()
if not self.reason:
raise UserError(_("Please enter a reason for rejection."))
if not self.stage_id:
raise UserError(_("Please select the stage where the task should be returned."))
self.task_id.reject_and_return(reason=self.reason, return_stage=self.stage_id)
return {"type": "ir.actions.act_window_close"}
class ProjectRejectReasonWizard(models.TransientModel):
_name = "project.reject.reason.wizard"
_description = "Project Rejection Reason Wizard"
reason = fields.Text(string="Rejection Reason", required=True)
project_id = fields.Many2one("project.project", string="Project", required=True)
def action_reject(self):
self.ensure_one()
if not self.reason:
raise UserError(_("Please enter a reason for rejection."))
self.project_id.reject_and_return(reason=self.reason)
return {"type": "ir.actions.act_window_close"}
class TaskBackStageWizard(models.TransientModel):
_name = "task.back.stage.wizard"
_description = "Task Back Stage Selection Wizard"
task_id = fields.Many2one("project.task", string="Task", required=True)
stage_id = fields.Many2one("project.task.type", string="Move To Stage", required=True)
available_stage_ids = fields.Many2many("project.task.type", compute="_compute_available_stage_ids")
@api.depends("task_id", "task_id.project_id", "task_id.stage_id")
def _compute_available_stage_ids(self):
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)
wizard.available_stage_ids = [(6, 0, stages.ids)]
def action_confirm(self):
self.ensure_one()
self.task_id._move_to_selected_stage(self.stage_id)
return {"type": "ir.actions.act_window_close"}
class TaskRequestTimelinesWizard(models.TransientModel):
_name = "task.request.timelines.wizard"
_description = "Task Request Timelines User Selection Wizard"
task_id = fields.Many2one("project.task", string="Task", required=True)
line_ids = fields.One2many("task.request.timelines.wizard.line", "wizard_id", string="Stage Assignees")
@api.model
def default_get(self, fields_list):
values = super().default_get(fields_list)
task = self.env["project.task"].browse(values.get("task_id") or self.env.context.get("default_task_id"))
if task and "line_ids" in fields_list:
task._ensure_task_timeline_rows()
values["line_ids"] = [
(0, 0, {
"stage_id": line.stage_id.id,
"user_ids": [(6, 0, line.assigned_to.ids)],
})
for line in task.assignees_timelines.sorted(lambda l: l.stage_sequence)
if line.stage_id
]
return values
def action_apply(self):
self.ensure_one()
selected_lines = self.line_ids.filtered(lambda line: line.stage_id and line.user_ids)
if not selected_lines:
raise UserError(_("Please select users for at least one stage."))
self.task_id.sudo().timelines_requested = True
self.task_id._apply_task_stage_user_lines(selected_lines, reassignment=False)
return {"type": "ir.actions.act_window_close"}
class TaskRequestTimelinesWizardLine(models.TransientModel):
_name = "task.request.timelines.wizard.line"
_description = "Task Request Timelines Stage User Line"
wizard_id = fields.Many2one("task.request.timelines.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", required=True)
user_ids = fields.Many2many("res.users", string="Assigned To")
available_user_ids = fields.Many2many("res.users", compute="_compute_available_user_ids")
@api.depends("stage_id", "task_id")
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)]
class TaskReassignAssigneesWizard(models.TransientModel):
_name = "task.reassign.assignees.wizard"
_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")
@api.model
def default_get(self, fields_list):
values = super().default_get(fields_list)
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)],
})
for stage in default_stage
]
return values
@api.depends("task_id")
def _compute_available_stage_ids(self):
for wizard in self:
wizard.available_stage_ids = [(6, 0, wizard.task_id.project_id.type_ids.ids)]
@api.depends("stage_ids", "task_id")
def _compute_available_user_ids(self):
for wizard in self:
users = self.env["res.users"]
for stage in wizard.stage_ids:
users |= stage.involved_user_ids
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)]
@api.onchange("stage_ids", "user_ids")
def _onchange_stage_or_users(self):
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)
]
def action_confirm(self):
self.ensure_one()
selected_lines = self.line_ids.filtered(lambda line: line.stage_id and line.user_ids)
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)
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"
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", required=True)
user_ids = fields.Many2many("res.users", string="New Assignees")
available_user_ids = fields.Many2many("res.users", compute="_compute_available_user_ids")
@api.depends("stage_id", "task_id")
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)]

View File

@ -1,48 +1,120 @@
<?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>
<record id="view_project_reject_reason_wizard" model="ir.ui.view">
<field name="name">project.reject.reason.wizard.form</field>
<field name="model">project.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_project_reject_reason_wizard" model="ir.actions.act_window">
<field name="name">Reject Task</field>
<field name="res_model">project.reject.reason.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<?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="task_id" invisible="1"/>
<field name="available_stage_ids" invisible="1"/>
<field name="reason" placeholder="Enter the reason for rejection..."/>
<field name="stage_id" domain="[('id', 'in', available_stage_ids)]" options="{'no_create': True, 'no_open': True}"/>
</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>
<record id="view_project_reject_reason_wizard" model="ir.ui.view">
<field name="name">project.reject.reason.wizard.form</field>
<field name="model">project.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_project_reject_reason_wizard" model="ir.actions.act_window">
<field name="name">Reject Task</field>
<field name="res_model">project.reject.reason.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="view_task_back_stage_wizard" model="ir.ui.view">
<field name="name">task.back.stage.wizard.form</field>
<field name="model">task.back.stage.wizard</field>
<field name="arch" type="xml">
<form string="Move Task Back">
<group>
<field name="task_id" invisible="1"/>
<field name="available_stage_ids" invisible="1"/>
<field name="stage_id" domain="[('id', 'in', available_stage_ids)]" options="{'no_create': True, 'no_open': True}"/>
</group>
<footer>
<button string="Move" type="object" name="action_confirm" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="view_task_request_timelines_wizard" model="ir.ui.view">
<field name="name">task.request.timelines.wizard.form</field>
<field name="model">task.request.timelines.wizard</field>
<field name="arch" type="xml">
<form string="Request Timelines">
<group>
<field name="task_id" invisible="1"/>
</group>
<field name="line_ids" nolabel="1">
<list editable="bottom" create="0" delete="0">
<field name="stage_id" readonly="1" options="{'no_open': True}"/>
<field name="available_user_ids" column_invisible="1"/>
<field name="user_ids" widget="many2many_tags" domain="[('id', 'in', available_user_ids)]" options="{'no_create': True, 'no_open': True}"/>
</list>
</field>
<footer>
<button string="Save" type="object" name="action_apply" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="view_task_reassign_assignees_wizard" model="ir.ui.view">
<field name="name">task.reassign.assignees.wizard.form</field>
<field name="model">task.reassign.assignees.wizard</field>
<field name="arch" type="xml">
<form string="Re-Assign Assignees">
<group>
<field name="task_id" invisible="1"/>
<field name="available_stage_ids" invisible="1"/>
<field name="available_user_ids" invisible="1"/>
<field name="reason" placeholder="Enter re-assignment reason..."/>
<field name="stage_ids" widget="many2many_tags" domain="[('id', 'in', available_stage_ids)]" options="{'no_create': True, 'no_open': True}"/>
<field name="user_ids" widget="many2many_tags" domain="[('id', 'in', available_user_ids)]" options="{'no_create': True, 'no_open': True}"/>
</group>
<field name="line_ids" nolabel="1" invisible="not stage_ids">
<list editable="bottom" create="0" delete="0">
<field name="stage_id" readonly="1" options="{'no_open': True}"/>
<field name="available_user_ids" column_invisible="1"/>
<field name="user_ids" widget="many2many_tags" domain="[('id', 'in', available_user_ids)]" options="{'no_create': True, 'no_open': True}"/>
</list>
</field>
<footer>
<button string="Re-Assign" type="object" name="action_confirm" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>