project enhancements
This commit is contained in:
parent
96493be796
commit
923304f759
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue