[IMP] project: add personal task stages and multiple assignments

Adds personal stages to tasks.
Users assigned to tasks will be able to have their own pipeline to
handle their tasks independently of the project's pipeline.

The assignee field on Tasks has also been changed to a Many2many to
support multiple assignees on one task.

The same table is used to store those information, essentially a triplet
(task, user, stage), to ensure that:
1) personal stages only apply to tasks to which you are assigned to.
2) synchronizing to make sure that you don't have a personal stage for a
   task on which you are not assigned anymore.

Alongside those changes, some minor changes have also been made:
- Modified the task's tree view.
- Renamed the Tasks menu to 'My Tasks'.
- The default view is now the kanban view for the 'My Tasks' action.
- project_id is not required anymore, tasks with no project are
  considered 'private', those tasks are only visible to those that are
  assigned to it.
- Added tracking of both user_ids and depend_on_ids in the chatter.

Closes odoo/odoo#74087

Task ID: 2398734
This commit is contained in:
William Braeckman
2021-09-01 07:17:40 +00:00
parent 65f7de37df
commit 7563b448da
33 changed files with 640 additions and 158 deletions
@@ -42,7 +42,7 @@
<filter string="My Team's Projects" name="my_team_projects" domain="[('project_id.user_id.employee_id.parent_id.user_id', '=', uid), ('project_id.user_id', '!=', uid), ('user_id', '!=', uid), ('user_id', '!=', False)]"/>
</xpath>
<xpath expr="//filter[@name='my_tasks']" position="after">
<filter string="My Team's Tasks" name="my_team_tasks" domain="[('task_id.user_id.employee_id.parent_id.user_id', '=', uid)]" />
<filter string="My Team's Tasks" name="my_team_tasks" domain="[('task_id.user_ids.employee_id.parent_id.user_id', '=', uid)]" />
</xpath>
</field>
</record>
@@ -147,7 +147,7 @@
<field name="model">project.task</field>
<field name="inherit_id" ref="project.project_sharing_project_task_view_tree" />
<field name="arch" type="xml">
<field name="user_id" position="after">
<field name="user_ids" position="after">
<field name="allow_subtasks" invisible="1"/>
<field name="allow_timesheets" invisible="1"/>
<field name="planned_hours" widget="timesheet_uom_no_toggle" sum="Initially Planned Hours" optional="hide" attrs="{'column_invisible': [('allow_timesheets', '=', False)]}"/>
+1 -1
View File
@@ -296,7 +296,7 @@
<field name="inherit_id" ref="project.view_task_search_form"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='my_tasks']" position='after'>
<filter string="My Team's Tasks" name="my_team_tasks" domain="[('user_id.employee_parent_id.user_id', '=', uid)]"/>
<filter string="My Team's Tasks" name="my_team_tasks" domain="[('user_ids.employee_parent_id.user_id', '=', uid)]"/>
</xpath>
<xpath expr="//filter[@name='late']" position='after'>
<filter string="Tasks in Overtime" name="overtime" domain="[('overtime', '&gt;', 0)]"/>
+101 -23
View File
@@ -88,10 +88,88 @@
<field name="stage_id" ref="project.project_project_stage_2"/>
</record>
<!-- Personal Stages: Mitchell Admin-->
<record id="project_personal_stage_admin_0" model="project.task.type">
<field name="sequence">1</field>
<field name="name">Inbox</field>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="project_personal_stage_admin_1" model="project.task.type">
<field name="sequence">2</field>
<field name="name">Today</field>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="project_personal_stage_admin_2" model="project.task.type">
<field name="sequence">3</field>
<field name="name">This Week</field>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="project_personal_stage_admin_3" model="project.task.type">
<field name="sequence">4</field>
<field name="name">This Month</field>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="project_personal_stage_admin_4" model="project.task.type">
<field name="sequence">5</field>
<field name="name">Later</field>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="project_personal_stage_admin_5" model="project.task.type">
<field name="sequence">6</field>
<field name="name">Done</field>
<field name="fold" eval="True"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<record id="project_personal_stage_admin_6" model="project.task.type">
<field name="sequence">7</field>
<field name="name">Canceled</field>
<field name="fold" eval="True"/>
<field name="user_id" ref="base.user_admin"/>
</record>
<!-- Personal Stages: Marc Demo -->
<record id="project_personal_stage_demo_0" model="project.task.type">
<field name="sequence">1</field>
<field name="name">Inbox</field>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="project_personal_stage_demo_1" model="project.task.type">
<field name="sequence">2</field>
<field name="name">Today</field>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="project_personal_stage_demo_2" model="project.task.type">
<field name="sequence">3</field>
<field name="name">This Week</field>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="project_personal_stage_demo_3" model="project.task.type">
<field name="sequence">4</field>
<field name="name">This Month</field>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="project_personal_stage_demo_4" model="project.task.type">
<field name="sequence">5</field>
<field name="name">Later</field>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="project_personal_stage_demo_5" model="project.task.type">
<field name="sequence">6</field>
<field name="name">Done</field>
<field name="fold" eval="True"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<record id="project_personal_stage_demo_6" model="project.task.type">
<field name="sequence">7</field>
<field name="name">Canceled</field>
<field name="fold" eval="True"/>
<field name="user_id" ref="base.user_demo"/>
</record>
<!-- Tasks -->
<record id="project_task_1" model="project.task">
<field name="planned_hours" eval="40.0"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Meeting Room Furnitures</field>
@@ -101,7 +179,7 @@
</record>
<record id="project_task_2" model="project.task">
<field name="planned_hours" eval="32.0"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Lunch Room: kitchen</field>
@@ -147,7 +225,7 @@
<record id="project_task_3" model="project.task">
<field name="planned_hours" eval="10.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Noise Reduction</field>
@@ -195,7 +273,7 @@
<record id="project_task_4" model="project.task">
<field name="planned_hours" eval="60.0"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Black Chairs for managers</field>
@@ -228,7 +306,7 @@
<record id="project_task_5" model="project.task">
<field name="planned_hours" eval="76.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Room 1: Decoration</field>
@@ -261,7 +339,7 @@
<record id="project_task_6" model="project.task">
<field name="planned_hours" eval="24.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Room 2: Decoration</field>
@@ -289,7 +367,7 @@
<record id="project_task_7" model="project.task">
<field name="planned_hours" eval="15.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Energy Certificate</field>
@@ -317,7 +395,7 @@
<record id="project_task_8" model="project.task">
<field name="planned_hours" eval="22.0"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">New portal system</field>
@@ -366,7 +444,7 @@
<record id="project_task_9" model="project.task">
<field name="planned_hours" eval="18.0"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Document management</field>
@@ -375,7 +453,7 @@
</record>
<record id="project_task_10" model="project.task">
<field name="planned_hours" eval="38.0"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Social network integration</field>
@@ -404,7 +482,7 @@
<record id="project_task_11" model="project.task">
<field name="planned_hours" eval="16.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">User interface improvements</field>
@@ -435,7 +513,7 @@
<record id="project_task_12" model="project.task">
<field name="planned_hours" eval="40.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Planning and budget</field>
@@ -447,7 +525,7 @@
<record id="project_task_19" model="project.task">
<field name="planned_hours">24.0</field>
<field name="stage_id" ref="project_stage_3"/>
<field name="user_id" eval="False"/>
<field name="user_ids" eval="False"/>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Basic outline</field>
<field name="tag_ids" eval="[(6, 0, [
@@ -493,7 +571,7 @@
<record id="project_task_20" model="project.task">
<field name="planned_hours">42.0</field>
<field name="user_id" eval="False"/>
<field name="user_ids" eval="False"/>
<field name="stage_id" ref="project_stage_0"/>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Create new components</field>
@@ -502,7 +580,7 @@
<record id="project_task_21" model="project.task">
<field name="planned_hours">14.0</field>
<field name="user_id" eval="False"/>
<field name="user_ids" eval="False"/>
<field name="stage_id" ref="project_stage_1"/>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Useablity review</field>
@@ -532,7 +610,7 @@
<record id="project_task_22" model="project.task">
<field name="planned_hours">12.0</field>
<field name="stage_id" ref="project_stage_1"/>
<field name="user_id" eval="False"/>
<field name="user_ids" eval="False"/>
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Customer analysis + Architecture</field>
@@ -562,7 +640,7 @@
<field name="sequence">17</field>
<field name="planned_hours">8.0</field>
<field name="stage_id" ref="project_stage_2"/>
<field name="user_id" eval="False"/>
<field name="user_ids" eval="False"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Modifications asked by the customer</field>
@@ -610,7 +688,7 @@
<record id="project_task_25" model="project.task">
<field name="sequence">20</field>
<field name="planned_hours">20.0</field>
<field name="user_id" eval="False"/>
<field name="user_ids" eval="False"/>
<field name="project_id" ref="project.project_project_1"/>
<field name="name">Office planning</field>
<field name="stage_id" ref="project_stage_0"/>
@@ -620,7 +698,7 @@
<record id="project_task_26" model="project.task">
<field name="sequence">20</field>
<field name="planned_hours">35.0</field>
<field name="user_id" eval="False"/>
<field name="user_ids" eval="False"/>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Unit Testing</field>
<field name="stage_id" ref="project_stage_2"/>
@@ -665,7 +743,7 @@
<record id="project_task_30" model="project.task">
<field name="planned_hours" eval="40.0"/>
<field name="user_id" ref="base.user_demo"/>
<field name="user_ids" eval="[(4, ref('base.user_demo'))]"/>
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_3"/>
<field name="name">Entry Hall</field>
@@ -674,7 +752,7 @@
</record>
<record id="project_task_31" model="project.task">
<field name="planned_hours" eval="10.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_3"/>
<field name="name">Check Lift</field>
@@ -684,7 +762,7 @@
</record>
<record id="project_task_32" model="project.task">
<field name="planned_hours" eval="24.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_3"/>
<field name="name">Room 1: Paint</field>
@@ -696,7 +774,7 @@
</record>
<record id="project_task_33" model="project.task">
<field name="planned_hours" eval="76.0"/>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_3"/>
<field name="name">Bathroom</field>
+2
View File
@@ -5,6 +5,8 @@ from . import analytic_account
from . import project_milestone
from . import project_project_stage
from . import project_task_recurrence
# `project_task_stage_personal` has to be loaded before `project`
from . import project_task_stage_personal
from . import project
from . import project_collaborator
from . import project_update
+131 -19
View File
@@ -43,7 +43,7 @@ PROJECT_TASK_WRITABLE_FIELDS = {
'name',
'partner_id',
'partner_email',
'user_id',
'user_ids',
'date_deadline',
'tag_ids',
'sequence',
@@ -53,7 +53,6 @@ PROJECT_TASK_WRITABLE_FIELDS = {
'parent_id',
}
class ProjectTaskType(models.Model):
_name = 'project.task.type'
_description = 'Task Stage'
@@ -97,6 +96,8 @@ class ProjectTaskType(models.Model):
is_closed = fields.Boolean('Closing Stage', help="Tasks in this stage are considered as closed.")
disabled_rating_warning = fields.Text(compute='_compute_disabled_rating_warning')
user_id = fields.Many2one('res.users', 'Stage Owner', index=True)
def unlink_wizard(self, stage_view=False):
self = self.with_context(active_test=False)
# retrieves all the projects with a least 1 task in that stage
@@ -846,6 +847,10 @@ class Task(models.Model):
stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
return stages.browse(stage_ids)
@api.model
def _read_group_personal_stage_type_ids(self, stages, domain, order):
return stages.search(['|', ('id', 'in', stages.ids), ('user_id', '=', self.env.user.id)])
active = fields.Boolean(default=True)
name = fields.Char(string='Title', tracking=True, required=True, index=True)
description = fields.Html(string='Description')
@@ -886,10 +891,24 @@ class Task(models.Model):
planned_hours = fields.Float("Initially Planned Hours", help='Time planned to achieve this task (including its sub-tasks).', tracking=True)
subtask_planned_hours = fields.Float("Sub-tasks Planned Hours", compute='_compute_subtask_planned_hours',
help="Sum of the time planned of all the sub-tasks linked to this task. Usually less than or equal to the initially planned time of this task.")
user_id = fields.Many2one('res.users',
string='Assigned to',
default=lambda self: self.env.uid,
index=True, tracking=True)
# Tracking of this field is done in the write function
user_ids = fields.Many2many('res.users', relation='project_task_user_rel', column1='task_id', column2='user_id',
string='Assignees', default=lambda self: self.env.user)
# Second Many2many containing the actual personal stage for the current user
# See project_task_stage_personal.py for the model defininition
personal_stage_type_ids = fields.Many2many('project.task.type', 'project_task_user_rel', column1='task_id', column2='stage_id',
ondelete='restrict', group_expand='_read_group_personal_stage_type_ids',
domain="[('user_id', '=', user.id)]", depends=['user_ids'], string='Personal Stage')
# Personal Stage computed from the user
personal_stage_id = fields.Many2one('project.task.stage.personal', string='Personal Stage State', compute_sudo=False,
compute='_compute_personal_stage_id', help="The current user's personal stage.")
# This field is actually a related field on personal_stage_id.stage_id
# However due to the fact that personal_stage_id is computed, the orm throws out errors
# saying the field cannot be searched.
personal_stage_type_id = fields.Many2one('project.task.type', string='Personal User Stage',
compute='_compute_personal_stage_type_id', inverse='_inverse_personal_stage_type_id', store=False,
search='_search_personal_stage_type_id',
help="The current user's personal task stage.")
partner_id = fields.Many2one('res.partner',
string='Customer',
compute='_compute_partner_id', recursive=True, store=True, readonly=False, tracking=True,
@@ -908,7 +927,6 @@ class Task(models.Model):
'res.company', string='Company', compute='_compute_company_id', store=True, readonly=False,
required=True, copy=True, default=_default_company_id)
color = fields.Integer(string='Color Index')
user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False)
attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments",
help="Attachments that don't come from a message.")
# In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
@@ -932,9 +950,11 @@ class Task(models.Model):
working_days_close = fields.Float(compute='_compute_elapsed', string='Working Days to Close', store=True, group_operator="avg")
# customer portal: include comment and incoming emails in communication history
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])
is_private = fields.Boolean(compute='_compute_is_private')
# Task Dependencies fields
allow_task_dependencies = fields.Boolean(related='project_id.allow_task_dependencies')
# Tracking of this field is done in the write function
depend_on_ids = fields.Many2many('project.task', relation="task_dependencies_rel", column1="task_id", column2="depends_on_id", string="Blocked By", domain="[('allow_task_dependencies', '=', True), ('id', '!=', id)]")
# recurrence fields
@@ -1028,6 +1048,64 @@ class Task(models.Model):
def SELF_WRITABLE_FIELDS(self):
return PROJECT_TASK_WRITABLE_FIELDS
@api.depends('project_id', 'parent_id')
def _compute_is_private(self):
# Modify accordingly, this field is used to display the lock on the task's kanban card
for task in self:
task.is_private = not task.project_id and not task.parent_id
@api.depends_context('uid')
@api.depends('user_ids')
def _compute_personal_stage_id(self):
# An user may only access his own 'personal stage' and there can only be one pair (user, task_id)
personal_stages = self.env['project.task.stage.personal'].search([('user_id', '=', self.env.uid), ('task_id', 'in', self.ids)])
self.personal_stage_id = False
for personal_stage in personal_stages:
personal_stage.task_id.personal_stage_id = personal_stage
@api.depends('personal_stage_id')
def _compute_personal_stage_type_id(self):
for task in self:
task.personal_stage_type_id = task.personal_stage_id.stage_id
def _inverse_personal_stage_type_id(self):
for task in self:
task.personal_stage_id.stage_id = task.personal_stage_type_id
@api.model
def _search_personal_stage_type_id(self, operator, value):
return [('personal_stage_type_ids', operator, value)]
@api.model
def _get_default_personal_stage_create_vals(self, user_id):
return [
{'sequence': 1, 'name': _('Inbox'), 'user_id': user_id, 'fold': False},
{'sequence': 2, 'name': _('Today'), 'user_id': user_id, 'fold': False},
{'sequence': 3, 'name': _('This Week'), 'user_id': user_id, 'fold': False},
{'sequence': 4, 'name': _('This Month'), 'user_id': user_id, 'fold': False},
{'sequence': 5, 'name': _('Later'), 'user_id': user_id, 'fold': False},
{'sequence': 6, 'name': _('Done'), 'user_id': user_id, 'fold': True},
{'sequence': 7, 'name': _('Canceled'), 'user_id': user_id, 'fold': True},
]
def _populate_missing_personal_stages(self):
# Assign the default personal stage for those that are missing
personal_stages_without_stage = self.env['project.task.stage.personal'].sudo().search([('task_id', 'in', self.ids), ('stage_id', '=', False)])
if personal_stages_without_stage:
user_ids = personal_stages_without_stage.user_id
personal_stage_by_user = defaultdict(lambda: self.env['project.task.stage.personal'])
for personal_stage in personal_stages_without_stage:
personal_stage_by_user[personal_stage.user_id] |= personal_stage
for user_id in user_ids:
stage = self.env['project.task.type'].sudo().search([('user_id', '=', user_id.id)], limit=1)
# In the case no stages have been found, we create the default stages for the user
if not stage:
stages = self.env['project.task.type'].sudo().with_context(lang=user_id.partner_id.lang, default_project_id=False).create(
self._get_default_personal_stage_create_vals(user_id.id)
)
stage = stages[0]
personal_stage_by_user[user_id].sudo().write({'stage_id': stage.id})
@api.constrains('depend_on_ids')
def _check_no_cyclic_dependencies(self):
if not self._check_m2m_recursion('depend_on_ids'):
@@ -1449,8 +1527,8 @@ class Task(models.Model):
default_project_id=project_id
).default_get(['stage_id']).get('stage_id')
vals["stage_id"] = default_stage[project_id]
# user_id change: update date_assign
if vals.get('user_id'):
# user_ids change: update date_assign
if vals.get('user_ids'):
vals['date_assign'] = fields.Datetime.now()
# Stage change: Update date_end if folded stage and date_last_stage_update
if vals.get('stage_id'):
@@ -1475,6 +1553,7 @@ class Task(models.Model):
}
self = self.with_context(ctx).sudo()
tasks = super(Task, self).create(vals_list)
tasks._populate_missing_personal_stages()
if is_portal_user:
# since we use sudo to create tasks, we need to check
# if the portal user could really create the tasks based on the ir rule.
@@ -1506,8 +1585,8 @@ class Task(models.Model):
# reset kanban state when changing stage
if 'kanban_state' not in vals:
vals['kanban_state'] = 'normal'
# user_id change: update date_assign
if vals.get('user_id') and 'date_assign' not in vals:
# user_ids change: update date_assign
if vals.get('user_ids') and 'date_assign' not in vals:
vals['date_assign'] = now
# recurrence fields
@@ -1542,7 +1621,45 @@ class Task(models.Model):
if portal_can_write:
tasks = tasks.sudo()
# X2Many Field Tracking
# Extract to a separate function if necessary
x2m_tracked_fields = {'user_ids', 'depend_on_ids'}
x2m_vals_common_fields = vals.keys() & x2m_tracked_fields
x2m_tracking_values = dict()
# Structured like so
# {
# task: {
# field_name: (value, display_value)
# }
# }
if not self._context.get('mail_notrack') and x2m_vals_common_fields:
# Compute the value before the changes
for task in self:
task_values = x2m_tracking_values.setdefault(task, {})
for field in x2m_vals_common_fields:
task_values[field] = (task[field], ', '.join(record.display_name for record in task[field]))
result = super(Task, tasks).write(vals)
if x2m_tracking_values:
for task, tracking_values in x2m_tracking_values.items():
# Compile the different changes
MailTracking = self.env['mail.tracking.value']
tracking_value_ids = []
# Use mail.tracking.value to track our changes, this is to use the same format as the default tracking one
# we just hack the record to think it is a text field and compile the data ourself beforehand
for field in tracking_values:
if task[field] != tracking_values[field][0]:
field_desc = task._fields[field]._description_string(self.env)
old_value = tracking_values[field][1]
new_value = ', '.join(record.display_name for record in task[field])
tracking_value_ids.append((0, 0, MailTracking.create_tracking_values(
old_value, new_value, field, {'type': 'char', 'string': field_desc}, 100, self._name)))
if tracking_value_ids:
task._message_log(tracking_value_ids=tracking_value_ids)
if 'user_ids' in vals:
tasks._populate_missing_personal_stages()
# rating on stage
if 'stage_id' in vals and vals.get('stage_id'):
self.filtered(lambda x: x.project_id.rating_active and x.project_id.rating_status == 'stage')._send_task_rating_mail(force_send=True)
@@ -1568,11 +1685,6 @@ class Task(models.Model):
# Subtasks
# ---------------------------------------------------
@api.depends('parent_id.user_id')
def _compute_user_id(self):
for task in self:
task.user_id = task.user_id or task.parent_id.user_id or self.env.uid
@api.depends('parent_id', 'project_id', 'display_project_id')
def _compute_partner_id(self):
""" Compute the partner_id when the tasks have no partner_id.
@@ -1673,7 +1785,7 @@ class Task(models.Model):
project_user_group_id = self.env.ref('project.group_project_user').id
new_group = ('group_project_user', lambda pdata: pdata['type'] == 'user' and project_user_group_id in pdata['groups'], {})
if not self.user_id and not self.stage_id.fold:
if not self.user_ids and not self.stage_id.fold:
take_action = self._notify_get_action_link('assign', **local_msg_vals)
project_actions = [{'url': take_action, 'title': _('I take it')}]
new_group[2]['actions'] = project_actions
@@ -1720,7 +1832,7 @@ class Task(models.Model):
# want the gateway user to be responsible if no other responsible is
# found.
create_context = dict(self.env.context or {})
create_context['default_user_id'] = False
create_context['default_user_ids'] = False
if custom_values is None:
custom_values = {}
defaults = {
@@ -1783,7 +1895,7 @@ class Task(models.Model):
return super(Task, self)._message_post_after_hook(message, msg_vals)
def action_assign_to_me(self):
self.write({'user_id': self.env.user.id})
self.write({'user_ids': [(4, self.env.user.id)]})
# If depth == 1, return only direct children
# If depth == 3, return children to third generation
@@ -209,7 +209,7 @@ class ProjectTaskRecurrence(models.Model):
field: value[0] if isinstance(value, tuple) else value for field, value in task_values.items()
}
create_values['stage_id'] = task.project_id.type_ids[0].id if task.project_id.type_ids else task.stage_id.id
create_values['user_id'] = False
create_values['user_ids'] = False
return create_values
def _create_next_task(self):
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ProjectTaskStagePersonal(models.Model):
_name = 'project.task.stage.personal'
_description = 'Personal Task Stage'
_table = 'project_task_user_rel'
_rec_name = 'stage_id'
task_id = fields.Many2one('project.task', required=True, ondelete='cascade', index=True)
user_id = fields.Many2one('res.users', required=True, ondelete='cascade', index=True)
stage_id = fields.Many2one('project.task.type', domain="[('user_id', '=', user_id)]", ondelete='restrict')
_sql_constraints = [
('project_personal_stage_unique', 'UNIQUE (task_id, user_id)', 'A task can only have a single personal stage per user.'),
]
+3 -2
View File
@@ -55,7 +55,7 @@ class ReportProjectTaskUser(models.Model):
t.date_end as date_end,
t.date_last_stage_update as date_last_stage_update,
t.date_deadline as date_deadline,
t.user_id,
tu.user_id,
t.project_id,
t.priority,
t.name as name,
@@ -82,7 +82,7 @@ class ReportProjectTaskUser(models.Model):
t.date_end,
t.date_deadline,
t.date_last_stage_update,
t.user_id,
tu.user_id,
t.project_id,
t.priority,
t.name,
@@ -98,6 +98,7 @@ class ReportProjectTaskUser(models.Model):
CREATE view %s as
%s
FROM project_task t
LEFT JOIN project_task_user_rel tu on t.id=tu.task_id
WHERE t.active = 'true'
%s
""" % (self._table, self._select(), self._group_by()))
@@ -54,7 +54,7 @@
<field name="date_deadline"/>
<field name="date_last_stage_update"/>
<filter string="My Projects" name="own_projects" domain="[('project_id.user_id', '=', uid)]"/>
<filter string="My Tasks" name="my_tasks" domain="[('task_id.user_id', '=', uid)]"/>
<filter string="My Tasks" name="my_tasks" domain="[('task_id.user_ids', 'in', uid)]"/>
<separator/>
<filter string="Starred" name="starred" domain="[('task_id.priority', 'in', [0, 1])]"/>
<separator/>
@@ -59,7 +59,7 @@ WITH all_moves_stage_task AS (
COALESCE(LAG(mm.date) OVER (PARTITION BY mm.res_id ORDER BY mm.id), pt.create_date) as date_begin,
mm.date as date_end,
mtv.old_value_integer as stage_id,
pt.user_id,
usr_rel.user_id,
pt.date_assign,
pt.date_deadline,
pt.partner_id
@@ -71,6 +71,7 @@ WITH all_moves_stage_task AS (
JOIN ir_model_fields imf ON mtv.field = imf.id
AND imf.model = 'project.task'
AND imf.name = 'stage_id'
JOIN project_task_user_rel usr_rel ON pt.id=usr_rel.task_id
WHERE pt.active
--We compute the last reached stage
@@ -82,7 +83,7 @@ WITH all_moves_stage_task AS (
COALESCE(md.date, pt.create_date) as date_begin,
(CURRENT_DATE + interval '1 month')::date as date_end,
pt.stage_id,
pt.user_id,
usr_rel.user_id,
pt.date_assign,
pt.date_deadline,
pt.partner_id
@@ -98,6 +99,7 @@ WITH all_moves_stage_task AS (
AND mm.model = 'project.task'
ORDER BY mm.id DESC
FETCH FIRST ROW ONLY) md ON TRUE
JOIN project_task_user_rel usr_rel ON pt.id=usr_rel.task_id
WHERE pt.active
)
SELECT (task_id*10^7 + 10^6 + to_char(d, 'YYMMDD')::integer)::bigint as id,
+2 -1
View File
@@ -4,7 +4,7 @@ access_project_project_manager,project.project,model_project_project,project.gro
access_project_project_stage,project.project_stage,model_project_project_stage,base.group_user,1,0,0,0
access_project_project_stage_manager,project.project_stage.manager,model_project_project_stage,project.group_project_manager,1,1,1,1
access_project_task_type_user,project.task.type.user,model_project_task_type,base.group_user,1,0,0,0
access_project_task_type_project_user,project.task.type.project.user,model_project_task_type,project.group_project_user,1,0,0,0
access_project_task_type_project_user,project.task.type.project.user,model_project_task_type,project.group_project_user,1,1,1,1
access_project_task_type_manager,project.task.type manager,model_project_task_type,project.group_project_manager,1,1,1,1
access_project_task_type_portal,task_type_portal,project.model_project_task_type,base.group_portal,1,0,0,0
access_project_task,project.task,model_project_task,project.group_project_user,1,1,1,1
@@ -39,3 +39,4 @@ access_project_milestone_project_manager,project.milestone.project.manager,model
access_project_collaborator_manager,project.collaborator.manager,model_project_collaborator,project.group_project_manager,1,1,1,1
access_project_collaborator_portal,project.collaborator.portal,model_project_collaborator,base.group_portal,1,0,0,0
access_project_share_manager,project.share.wizard.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0
access_project_personal_stage,project.personal.stage.user,model_project_task_stage_personal,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_project_project_stage project.project_stage model_project_project_stage base.group_user 1 0 0 0
5 access_project_project_stage_manager project.project_stage.manager model_project_project_stage project.group_project_manager 1 1 1 1
6 access_project_task_type_user project.task.type.user model_project_task_type base.group_user 1 0 0 0
7 access_project_task_type_project_user project.task.type.project.user model_project_task_type project.group_project_user 1 0 1 0 1 0 1
8 access_project_task_type_manager project.task.type manager model_project_task_type project.group_project_manager 1 1 1 1
9 access_project_task_type_portal task_type_portal project.model_project_task_type base.group_portal 1 0 0 0
10 access_project_task project.task model_project_task project.group_project_user 1 1 1 1
39 access_project_collaborator_manager project.collaborator.manager model_project_collaborator project.group_project_manager 1 1 1 1
40 access_project_collaborator_portal project.collaborator.portal model_project_collaborator base.group_portal 1 0 0 0
41 access_project_share_manager project.share.wizard.manager model_project_share_wizard project.group_project_manager 1 1 1 0
42 access_project_personal_stage project.personal.stage.user model_project_task_stage_personal base.group_user 1 1 1 1
+43 -1
View File
@@ -89,7 +89,7 @@
'|',
('message_partner_ids', 'in', [user.partner_id.id]),
# to subscribe check access to the record, follower is not enough at creation
('user_id', '=', user.id)
('user_ids', 'in', user.id)
]</field>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
@@ -101,12 +101,54 @@
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
</record>
<record model="ir.rule" id="task_type_manager_rule">
<field name="name">Project/Task Type: manager sees all</field>
<field name="model_id" ref="model_project_task_type"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
</record>
<record model="ir.rule" id="task_type_visibility_rule">
<field name="name">Project/Task Type: see own or unowned stages</field>
<field name="model_id" ref="model_project_task_type"/>
<field name="domain_force">[('user_id', 'in', (False, user.id))]</field>
</record>
<record model="ir.rule" id="task_type_own_write_rule">
<field name="name">Project/Task Type: write own stages</field>
<field name="model_id" ref="model_project_task_type"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
<field name="groups" eval="[(4,ref('project.group_project_user'))]"/>
</record>
<record model="ir.rule" id="report_project_task_user_report_comp_rule">
<field name="name">Task Analysis multi-company</field>
<field name="model_id" ref="model_report_project_task_user"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record id="ir_rule_project_personal_stage_my" model="ir.rule">
<field name="name">Project: See my own personal stage</field>
<field name="model_id" ref="project.model_project_task_stage_personal"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
</record>
<record id="ir_rule_private_task" model="ir.rule">
<field name="name">Project: See private tasks</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[
('project_id.privacy_visibility', '!=', 'followers'),
'|', '|', ('project_id', '!=', False),
('parent_id', '!=', False),
('user_ids', 'in', user.id),
]</field>
<field name="groups" eval="[(4,ref('project.group_project_user'))]"/>
</record>
<!-- Portal -->
<record id="project_project_rule_portal" model="ir.rule">
<field name="name">Project: portal users: portal and following</field>
+100 -1
View File
@@ -4,9 +4,11 @@ import KanbanController from 'web.KanbanController';
import KanbanRenderer from 'web.KanbanRenderer';
import KanbanView from 'web.KanbanView';
import KanbanColumn from 'web.KanbanColumn';
import viewRegistry from 'web.view_registry';
import KanbanRecord from 'web.KanbanRecord';
import KanbanModel from 'web.KanbanModel';
import viewRegistry from 'web.view_registry';
import { ProjectControlPanel } from '@project/js/project_control_panel';
import viewUtils from 'web.viewUtils';
// PROJECTS
@@ -70,6 +72,30 @@ const ProjectTaskKanbanRenderer = KanbanRenderer.extend({
config: Object.assign({}, KanbanRenderer.prototype.config, {
KanbanColumn: ProjectTaskKanbanColumn,
}),
/**
* Allows record drag when grouping by `personal_stage_type_ids`
*
* @override
*/
_setState() {
this._super(...arguments);
const groupedBy = this.state.groupedBy[0];
const groupByFieldName = viewUtils.getGroupByField(groupedBy);
const field = this.state.fields[groupByFieldName] || {};
const fieldInfo = this.state.fieldsInfo.kanban[groupByFieldName] || {};
const grouped_by_date = ["date", "datetime"].includes(field.type);
const grouped_by_m2m = field.type === "many2many";
const readonly = !!field.readonly || !!fieldInfo.readonly;
const draggable = !readonly && (!grouped_by_m2m || groupByFieldName == 'personal_stage_type_ids') &&
(!grouped_by_date || fieldInfo.allowGroupRangeValue);
Object.assign(this.columnOptions, {
draggable,
});
}
});
export const ProjectKanbanController = KanbanController.extend({
@@ -93,8 +119,81 @@ export const ProjectKanbanController = KanbanController.extend({
},
});
const ProjectTaskKanbanModel = KanbanModel.extend({
/**
* Upon updating `personal_stage_type_ids` we actually want to update the `personal_stage_type_id` field.
*
* @override
* @private
*/
moveRecord: function (recordID, groupID, parentID) {
var self = this;
var parent = this.localData[parentID];
var new_group = this.localData[groupID];
var changes = {};
var groupedFieldName = viewUtils.getGroupByField(parent.groupedBy[0]);
var groupedField = parent.fields[groupedFieldName];
// for a date/datetime field, we take the last moment of the group as the group value
if (['date', 'datetime'].includes(groupedField.type)) {
changes[groupedFieldName] = viewUtils.getGroupValue(new_group, groupedFieldName);
} else if (groupedField.type === 'many2one') {
changes[groupedFieldName] = {
id: new_group.res_id,
display_name: new_group.value,
};
} else if (groupedField.type === 'selection') {
var value = _.findWhere(groupedField.selection, {1: new_group.value});
changes[groupedFieldName] = value && value[0] || false;
} else if (groupedField.type == 'many2many' && groupedFieldName == 'personal_stage_type_ids') {
changes['personal_stage_type_id'] = {
id: new_group.res_id,
display_name: new_group.value,
}
} else {
changes[groupedFieldName] = new_group.value;
}
// Manually updates groups data. Note: this is done before the actual
// save as it might need to perform a read group in some cases so those
// updated data might be overridden again.
var record = self.localData[recordID];
var resID = record.res_id;
// Remove record from its current group
var old_group;
for (var i = 0; i < parent.data.length; i++) {
old_group = self.localData[parent.data[i]];
var index = _.indexOf(old_group.data, recordID);
if (index >= 0) {
old_group.data.splice(index, 1);
old_group.count--;
if (!old_group.activeFilter || old_group.activeFilter.value === record.data[parent.progressBar.field]) {
// Here, the record leaving the old group matches its domain,
// so we must decrease the domainCount too.
old_group.domainCount--;
}
old_group.res_ids = _.without(old_group.res_ids, resID);
self._updateParentResIDs(old_group);
break;
}
}
// Add record to its new group
new_group.data.push(recordID);
new_group.res_ids.push(resID);
new_group.count++;
return this.notifyChanges(recordID, changes).then(function () {
return self.save(recordID);
}).then(function () {
record.parentID = new_group.id;
return [old_group.id, new_group.id];
});
},
})
const ProjectKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Model: ProjectTaskKanbanModel,
Controller: ProjectKanbanController,
Renderer: ProjectTaskKanbanRenderer,
ControlPanel: ProjectControlPanel,
@@ -34,3 +34,8 @@
color: $o-main-text-color;
}
}
.o_form_project_tasks .o_project_task_project_field input::placeholder {
color: darkred;
font-style: italic;
}
@@ -62,3 +62,22 @@
}
}
}
.o_kanban_project_tasks .o_field_many2manytags, .o_kanban_tags{
margin: 0px;
}
.o_project_m2m_avatar {
width: 20px;
height: 20px;
margin-right: -1px;
display: inline-block;
img {
border-radius: 50%;
width: 20px;
height: 20px;
object-fit: cover;
margin-right: 4px;
}
}
+1
View File
@@ -14,4 +14,5 @@ from . import test_project_update_flow
from . import test_project_update_ui
from . import test_portal
from . import test_multicompany
from . import test_personal_stages
from . import test_task_dependencies
+4 -4
View File
@@ -235,13 +235,13 @@ class TestProjectPortalCommon(TestProjectCommon):
'groups_id': [(6, 0, [])]})
self.task_3 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test3', 'user_id': self.user_portal.id, 'project_id': self.project_pigs.id})
'name': 'Test3', 'user_ids': self.user_portal, 'project_id': self.project_pigs.id})
self.task_4 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test4', 'user_id': self.user_public.id, 'project_id': self.project_pigs.id})
'name': 'Test4', 'user_ids': self.user_public, 'project_id': self.project_pigs.id})
self.task_5 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test5', 'user_id': False, 'project_id': self.project_pigs.id})
'name': 'Test5', 'user_ids': False, 'project_id': self.project_pigs.id})
self.task_6 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Test5', 'user_id': False, 'project_id': self.project_pigs.id})
'name': 'Test5', 'user_ids': False, 'project_id': self.project_pigs.id})
class TestPortalProject(TestProjectPortalCommon):
+2 -2
View File
@@ -179,12 +179,12 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
Task = cls.env['project.task'].with_context({'mail_create_nolog': True, 'tracking_disable': True})
cls.task_1 = Task.create({
'name': 'Task 1 in Project A',
'user_id': cls.user_employee_company_a.id,
'user_ids': cls.user_employee_company_a,
'project_id': cls.project_company_a.id
})
cls.task_2 = Task.create({
'name': 'Task 2 in Project B',
'user_id': cls.user_employee_company_b.id,
'user_ids': cls.user_employee_company_b,
'project_id': cls.project_company_b.id
})
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
from odoo.tests import tagged
from .test_project_base import TestProjectCommon
@tagged('-at_install', 'post_install', 'personal_stages')
class TestPersonalStages(TestProjectCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_stages = cls.env['project.task.type'].search([('user_id', '=', cls.user_projectuser.id)])
cls.manager_stages = cls.env['project.task.type'].search([('user_id', '=', cls.user_projectmanager.id)])
def test_personal_stage_base(self):
# Project User is assigned to task_1 he should be able to see a personal stage
self.task_1.with_user(self.user_projectuser)._compute_personal_stage_id()
self.assertTrue(self.task_1.with_user(self.user_projectuser).personal_stage_type_id,
'Project User is assigned to task 1, he should have a personal stage assigned.')
self.task_1.with_user(self.user_projectmanager)._compute_personal_stage_id()
self.assertFalse(self.env['project.task'].browse(self.task_1.id).with_user(self.user_projectmanager).personal_stage_type_id,
'Project Manager is not assigned to task 1, he should not have a personal stage assigned.')
# Now assign a second user to our task_1
self.task_1.user_ids += self.user_projectmanager
self.assertTrue(self.task_1.with_user(self.user_projectmanager).personal_stage_type_id,
'Project Manager has now been assigned to task 1 and should have a personal stage assigned.')
self.task_1.with_user(self.user_projectmanager)._compute_personal_stage_id()
task_1_manager_stage = self.task_1.with_user(self.user_projectmanager).personal_stage_type_id
self.task_1.with_user(self.user_projectuser)._compute_personal_stage_id()
self.task_1.with_user(self.user_projectuser).personal_stage_type_id = self.user_stages[1]
self.assertEqual(self.task_1.with_user(self.user_projectuser).personal_stage_type_id, self.user_stages[1],
'Assigning another personal stage to the task should have changed it for user 1.')
self.task_1.with_user(self.user_projectmanager)._compute_personal_stage_id()
self.assertEqual(self.task_1.with_user(self.user_projectmanager).personal_stage_type_id, task_1_manager_stage,
'Modifying the personal stage of Project User should not have affected the personal stage of Project Manager.')
self.task_2.with_user(self.user_projectmanager).personal_stage_type_id = self.manager_stages[1]
self.assertEqual(self.task_1.with_user(self.user_projectmanager).personal_stage_type_id, task_1_manager_stage,
'Modifying the personal stage on task 2 for Project Manager should not have affected the stage on task 1.')
def test_personal_stage_search(self):
self.task_2.user_ids += self.user_projectuser
# Make sure both personal stages are different
self.task_1.with_user(self.user_projectuser).personal_stage_type_id = self.user_stages[0]
self.task_2.with_user(self.user_projectuser).personal_stage_type_id = self.user_stages[1]
tasks = self.env['project.task'].with_user(self.user_projectuser).search([('personal_stage_type_id', '=', self.user_stages[0].id)])
self.assertTrue(tasks, 'The search result should not be empty.')
for task in tasks:
self.assertEqual(task.personal_stage_type_id, self.user_stages[0],
'The search should only have returned task that are in the inbox personal stage.')
def test_personal_stage_read_group(self):
self.task_1.user_ids += self.user_projectmanager
self.task_1.with_user(self.user_projectmanager).personal_stage_type_id = self.manager_stages[1]
#Makes sure the personal stage for project manager is saved in the database
self.env['project.task'].flush()
read_group_user = self.env['project.task'].with_user(self.user_projectuser).read_group(
[('user_ids', '=', self.user_projectuser.id)], fields=['sequence:avg'], groupby=['personal_stage_type_ids'])
# Check that the result is at least a bit coherent
self.assertEqual(len(self.user_stages), len(read_group_user),
'read_group should return %d groups' % len(self.user_stages))
# User has only one task assigned the sum of all counts should be 1
total = 0
for group in read_group_user:
total += group['personal_stage_type_ids_count']
self.assertEqual(1, total,
'read_group should not have returned more tasks than the user is assigned to.')
read_group_manager = self.env['project.task'].with_user(self.user_projectmanager).read_group(
[('user_ids', '=', self.user_projectmanager.id)], fields=['sequence:avg'], groupby=['personal_stage_type_ids'])
self.assertEqual(len(self.manager_stages), len(read_group_manager),
'read_group should return %d groups' % len(self.user_stages))
total = 0
total_stage_0 = 0
total_stage_1 = 0
for group in read_group_manager:
total += group['personal_stage_type_ids_count']
# Check that we have a task in both stages
if group['personal_stage_type_ids'][0] == self.manager_stages[0].id:
total_stage_0 += 1
elif group['personal_stage_type_ids'][0] == self.manager_stages[1].id:
total_stage_1 += 1
self.assertEqual(2, total,
'read_group should not have returned more tasks than the user is assigned to.')
self.assertEqual(1, total_stage_0)
self.assertEqual(1, total_stage_1)
+2 -2
View File
@@ -60,11 +60,11 @@ class TestProjectCommon(TransactionCase):
# Already-existing tasks in Pigs
cls.task_1 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Pigs UserTask',
'user_id': cls.user_projectuser.id,
'user_ids': cls.user_projectuser,
'project_id': cls.project_pigs.id})
cls.task_2 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Pigs ManagerTask',
'user_id': cls.user_projectmanager.id,
'user_ids': cls.user_projectmanager,
'project_id': cls.project_pigs.id})
# Test 'Goats' project, same as 'Pigs', but with 2 stages
+3 -3
View File
@@ -112,7 +112,7 @@ class TestProjectFlow(TestProjectCommon):
parent_task = Task.create({
'name': 'Mother Task',
'user_id': self.user_projectuser.id,
'user_ids': self.user_projectuser,
'project_id': self.project_pigs.id,
'partner_id': self.partner_2.id,
'planned_hours': 12,
@@ -120,7 +120,7 @@ class TestProjectFlow(TestProjectCommon):
another_parent_task = Task.create({
'name': 'Another Mother Task',
'user_id': self.user_projectuser.id,
'user_ids': self.user_projectuser,
'project_id': self.project_pigs.id,
'partner_id': self.partner_3.id,
'planned_hours': 0,
@@ -210,7 +210,7 @@ class TestProjectFlow(TestProjectCommon):
Task = self.env['project.task'].with_context({'tracking_disable': True})
first_task = Task.create({
'name': 'first task',
'user_id': self.user_projectuser.id,
'user_ids': self.user_projectuser,
'project_id': self.project_pigs.id,
'partner_id': self.partner_2.id,
})
+4 -4
View File
@@ -42,12 +42,12 @@ class TestProjectSharingCommon(TestProjectCommon):
cls.task_cow = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Cow UserTask',
'user_id': cls.user_projectuser.id,
'user_ids': cls.user_projectuser,
'project_id': cls.project_cows.id,
})
cls.task_portal = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Portal UserTask',
'user_id': cls.user_projectuser.id,
'user_ids': cls.user_projectuser,
'project_id': cls.project_portal.id,
})
@@ -123,7 +123,7 @@ class TestProjectSharing(TestProjectSharingCommon):
task = form.save()
self.assertEqual(task.name, 'Test')
self.assertEqual(task.project_id, self.project_portal)
self.assertEqual(task.user_id, self.user_portal)
self.assertEqual(task.user_ids, self.user_portal)
# 3.1) Try to change the project of the new task with this user.
with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
form.project_id = self.project_cows
@@ -172,4 +172,4 @@ class TestProjectSharing(TestProjectSharingCommon):
form.save()
self.assertEqual(task.child_ids.name, 'Test Subtask')
self.assertEqual(task.child_ids.project_id, self.project_cows)
self.assertEqual(task.child_ids.user_id, self.user_portal)
self.assertEqual(task.child_ids.user_ids, self.user_portal)
@@ -10,10 +10,6 @@ class TestProjectSubtasks(TestProjectCommon):
"""
Create a task in the default task form should take the project set in the form or the default project in the context
"""
with self.assertRaises(AssertionError, msg="Should not accept a form without project. Project is required"):
with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form:
task_form.name = 'Test Task 1'
with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form:
task_form.name = 'Test Task 1'
task_form.project_id = self.project_pigs
@@ -33,10 +29,6 @@ class TestProjectSubtasks(TestProjectCommon):
"""
Create a task in the task form 2 should take the project set in the form or the default project in the context
"""
with self.assertRaises(AssertionError, msg="Should not accept a form without project. Project is required"):
with Form(self.env['project.task'].with_context({'tracking_disable': True}), view="project.view_task_form2") as task_form:
task_form.name = 'Test Task 1'
with Form(self.env['project.task'].with_context({'tracking_disable': True}), view="project.view_task_form2") as task_form:
task_form.name = 'Test Task 1'
task_form.project_id = self.project_pigs
@@ -20,7 +20,7 @@ class TestTaskDependencies(TestProjectCommon):
})
cls.task_3 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Pigs UserTask 2',
'user_id': cls.user_projectuser.id,
'user_ids': cls.user_projectuser,
'project_id': cls.project_pigs.id,
})
@@ -97,7 +97,8 @@ class TestTaskDependencies(TestProjectCommon):
self.env['res.config.settings'].create({
'group_project_task_dependencies': True
}).execute()
self.task_1.write({
# `depend_on_ids` is tracked
self.task_1.with_context(mail_notrack=True).write({
'depend_on_ids': [Command.link(self.task_2.id)]
})
self.cr.precommit.clear()
@@ -103,8 +103,8 @@
<em class="font-weight-normal text-muted"><span t-field="tasks[0].sudo().project_id.label_tasks"/> in stage:</em>
<span class="text-truncate" t-field="tasks[0].sudo().stage_id.name"/></th>
<th t-if="groupby == 'user'">
<em class="font-weight-normal text-muted" t-if="tasks[0].sudo().user_id"><span t-field="tasks[0].sudo().project_id.label_tasks"/> assigned to:</em>
<span class="text-truncate" t-field="tasks[0].sudo().user_id.name"/></th>
<em class="font-weight-normal text-muted" t-if="tasks[0].sudo().user_ids"><span t-field="tasks[0].sudo().project_id.label_tasks"/> assigned to:</em>
<span class="text-truncate" t-esc="', '.join(tasks[0].sudo().mapped('user_ids.name'))"/></th>
<th t-if="groupby == 'customer'">
<em class="font-weight-normal text-muted" t-if="tasks[0].sudo().partner_id"><span t-field="tasks[0].sudo().project_id.label_tasks"/> for customer:</em>
<span class="text-truncate" t-field="tasks[0].sudo().partner_id.name"/></th>
@@ -119,7 +119,7 @@
#<span t-esc="task.id"/>
</td>
<td>
<a t-attf-href="/my/#{task_url}/#{task.id}?{{ keep_query() }}"><span t-field="task.name"/></a>
<a t-attf-href="/my/task/#{task.id}?{{ keep_query() }}"><span t-field="task.name"/></a>
</td>
<td t-if="groupby != 'project'" class="text-center">
<span class="badge badge-pill badge-info" title="Current project of the task" t-esc="task.project_id.name" />
@@ -177,18 +177,20 @@
</div>
</t>
<t t-set="card_body">
<div class="row" t-if="task.user_id or task.partner_id">
<div class="col-12 col-md-6 pb-2" t-if="task.user_id">
<div class="row" t-if="task.user_ids or task.partner_id">
<div class="col-12 col-md-6 pb-2" t-if="task.user_ids">
<strong>Assigned to</strong>
<div class="row">
<div class="col d-flex align-items-center flex-grow-0 pr-3">
<img class="rounded-circle mt-1 o_portal_contact_img" t-att-src="image_data_uri(task.user_id.avatar_1024)" alt="Contact"/>
</div>
<div class="col pl-md-0">
<div t-field="task.user_id" t-options='{"widget": "contact", "fields": ["name"]}'/>
<a t-attf-href="mailto:{{task.user_id.email}}" t-if="task.user_id.email"><div t-field="task.user_id" t-options='{"widget": "contact", "fields": ["email"]}'/></a>
<a t-attf-href="tel:{{task.user_id.phone}}" t-if="task.user_id.phone"><div t-field="task.user_id" t-options='{"widget": "contact", "fields": ["phone"]}'/></a>
</div>
<t t-foreach="task.user_ids" t-as="user">
<div class="col d-flex align-items-center flex-grow-0 pr-3">
<img class="rounded-circle mt-1 o_portal_contact_img" t-att-src="image_data_uri(user.avatar_1024)" alt="Contact"/>
</div>
<div class="col pl-md-0">
<div t-esc="user" t-options='{"widget": "contact", "fields": ["name"]}'/>
<a t-attf-href="mailto:{{user.email}}" t-if="user.email"><div t-esc="user" t-options='{"widget": "contact", "fields": ["email"]}'/></a>
<a t-attf-href="tel:{{user.phone}}" t-if="user.phone"><div t-esc="user" t-options='{"widget": "contact", "fields": ["phone"]}'/></a>
</div>
</t>
</div>
</div>
<div class="coll-12 col-md-6 pb-2" t-if="task.partner_id">
+10 -10
View File
@@ -31,7 +31,7 @@
<field name="color"/>
<field name="priority"/>
<field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/>
<field name="user_id"/>
<field name="user_ids"/>
<field name="partner_id"/>
<field name="sequence"/>
<field name="is_closed"/>
@@ -90,7 +90,7 @@
</div>
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
<field name="kanban_state" widget="state_selection"/>
<field name="user_id" widget="many2one_avatar_user" options="{'no_open_chat': True}"/>
<field name="user_ids" widget="many2many_avatar_user" options="{'no_open_chat': True}"/>
</div>
</div>
</div>
@@ -116,7 +116,7 @@
<field name="child_text" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="partner_id" optional="hide"/>
<field name="user_id" optional="show" widget="many2one_avatar_user" options="{'no_open_chat': True}"/>
<field name="user_ids" optional="show" widget="many2many_avatar_user" options="{'no_open_chat': True}"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="kanban_state" widget="state_selection" optional="hide"/>
@@ -151,10 +151,10 @@
<group>
<field name="project_id" invisible="1"/>
<field name="display_project_id" string="Project" invisible="1"/>
<field name="user_id"
<field name="user_ids"
class="o_task_user_field"
options="{'no_open': True, 'no_open_chat': True}"
widget="many2one_avatar_user"
widget="many2many_avatar_user"
domain="[('share', '=', False)]"/>
</group>
<group>
@@ -173,7 +173,7 @@
<field name="description" type="html"/>
</page>
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_id': uid, 'default_parent_id': id, 'default_partner_id': partner_id}">
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': [(6, 0, [uid])], 'default_parent_id': id, 'default_partner_id': partner_id}">
<tree editable="bottom">
<field name="project_id" invisible="1"/>
<field name="is_closed" invisible="1"/>
@@ -182,7 +182,7 @@
<field name="display_project_id" string="Project" optional="hide" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="partner_id" options="{'no_open': True, 'no_create': True, 'no_edit': True}" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user" optional="show" options="{'no_open_chat': True}"/>
<field name="user_ids" widget="many2many_avatar_user" optional="show" options="{'no_open_chat': True}"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
<field name="kanban_state" widget="state_selection" optional="hide"/>
@@ -208,11 +208,11 @@
<search string="Tasks">
<field name="name" string="Task"/>
<field name="tag_ids"/>
<field name="user_id"/>
<field name="user_ids"/>
<field name="partner_id" operator="child_of"/>
<field name="stage_id"/>
<field string="Project" name="display_project_id"/>
<filter string="Unassigned" name="unassigned" domain="[('user_id', '=', False)]"/>
<filter string="Unassigned" name="unassigned" domain="[('user_ids', '=', False)]"/>
<separator/>
<filter string="Starred" name="starred" domain="[('priority', 'in', [1, 2])]"/>
<filter string="Late Tasks" name="late" domain="[('date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
@@ -223,7 +223,7 @@
domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Group By">
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
<filter string="Assigned to" name="user" context="{'group_by': 'user_id'}"/>
<filter string="Assigned to" name="user" context="{'group_by': 'user_ids'}"/>
<filter string="Project" name="project" context="{'group_by': 'project_id'}"/>
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
<filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/>
+45 -29
View File
@@ -18,14 +18,13 @@
<search string="Tasks">
<field name="name" string="Task"/>
<field name="tag_ids"/>
<field name="user_id"/>
<field name="user_ids"/>
<field string="Project" name="display_project_id"/>
<field name="stage_id"/>
<field name="partner_id" operator="child_of"/>
<field name="parent_id"/>
<filter string="My Tasks" name="my_tasks" domain="[('user_id', '=', uid)]"/>
<filter string="Followed Tasks" name="my_followed_tasks" domain="[('message_is_follower', '=', True)]" />
<filter string="Unassigned" name="unassigned" domain="[('user_id', '=', False)]"/>
<filter string="My Tasks" name="my_tasks" domain="[('user_ids', 'in', uid)]"/>
<filter string="Unassigned" name="unassigned" domain="[('user_ids', '=', False)]"/>
<separator/>
<filter string="Starred" name="starred" domain="[('priority', 'in', [0, 1])]"/>
<separator/>
@@ -46,7 +45,8 @@
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Group By">
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
<filter string="Assigned to" name="user" context="{'group_by': 'user_id'}"/>
<filter string="Personal Stage" name="personal_stage" context="{'group_by': 'personal_stage_type_ids'}"/>
<filter string="Assigned to" name="user" context="{'group_by': 'user_ids'}"/>
<filter string="Project" name="project" context="{'group_by': 'project_id'}"/>
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
<filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/>
@@ -78,7 +78,7 @@
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
<field name="domain">[('display_project_id', '=', active_id)]</field>
<field name="context">{
'pivot_row_groupby': ['user_id'],
'pivot_row_groupby': ['user_ids'],
'default_project_id': active_id,
'show_project_update': True,
}</field>
@@ -184,6 +184,7 @@
<field name="rating_template_id" optional="hide" groups="project.group_project_rating"/>
<field name="project_ids" optional="show" widget="many2many_tags" options="{'color_field': 'color'}" />
<field name="fold" optional="show"/>
<field name="user_id" optional="show"/>
</tree>
</field>
</record>
@@ -783,7 +784,7 @@
<field name="allow_task_dependencies" invisible="1" />
<header>
<button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight"
attrs="{'invisible' : [('user_id', '!=', False)]}" data-hotkey="q"/>
attrs="{'invisible' : [('user_ids', '!=', False)]}" data-hotkey="q"/>
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}"/>
</header>
<div class="alert alert-info oe_edit_only mb-0" role="status" attrs="{'invisible': ['|', ('recurring_task', '=', False), ('recurrence_id', '=', False)]}">
@@ -816,12 +817,18 @@
</div>
<group>
<group>
<field name="project_id" attrs="{'required': [('parent_id', '=', False)], 'invisible': [('parent_id', '!=', False)]}" domain="[('active', '=', True), ('company_id', '=', company_id)]"/>
<label for="project_id" class="oe_read_only"/>
<div colspan="1" class="oe_read_only">
<i class="o_project_task_project_field text-danger" attrs="{'invisible': [('project_id', '!=', False)]}">Private</i>
<field name="project_id" attrs="{'invisible': [('project_id', '=', False)]}" nolabel="1"/>
</div>
<field name="project_id" attrs="{'invisible': [('parent_id', '!=', False)]}" domain="[('active', '=', True), ('company_id', '=', company_id)]"
placeholder="Private" class="o_project_task_project_field oe_edit_only"/>
<field name="display_project_id" string="Project" attrs="{'invisible': [('parent_id', '=', False)]}" domain="[('active', '=', True), ('company_id', '=', company_id)]"/>
<field name="user_id"
<field name="user_ids"
class="o_task_user_field"
options="{'no_open': True}"
widget="many2one_avatar_user"
widget="many2many_avatar_user"
domain="[('share', '=', False)]"/>
<field name="parent_id"
domain="[('parent_id', '=', False)]"
@@ -846,7 +853,7 @@
<field name="description" type="html"/>
</page>
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_id': user_id, 'default_parent_id': id, 'default_partner_id': partner_id}">
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id, 'default_partner_id': partner_id}">
<tree editable="bottom">
<field name="project_id" invisible="1"/>
<field name="is_closed" invisible="1"/>
@@ -854,7 +861,7 @@
<field name="name"/>
<field name="display_project_id" string="Project" optional="hide"/>
<field name="partner_id" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user" optional="show"/>
<field name="user_ids" widget="many2many_avatar_user" optional="show"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="activity_ids" widget="list_activity" optional="hide"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
@@ -871,7 +878,7 @@
<field name="is_closed" invisible="1" />
<field name="name" />
<field name="project_id" optional="hide" />
<field name="user_id" widget="many2one_avatar_user" optional="show"/>
<field name="user_ids" widget="many2many_avatar_user" optional="show"/>
<field name="company_id" optional="hide" groups="base.group_multi_company" />
<field name="activity_ids" widget="list_activity" optional="hide"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show" />
@@ -980,7 +987,8 @@
<form>
<group>
<field name="name" string = "Task Title" placeholder="e.g. New Design"/>
<field name="user_id" options="{'no_open': True,'no_create': True}" domain="[('share', '=', False)]"/>
<field name="user_ids" options="{'no_open': True,'no_create': True}" domain="[('share', '=', False)]"
widget="many2many_tags"/>
<field name="company_id" invisible="1"/>
<field name="parent_id" invisible="1" groups="base.group_no_one"/>
</group>
@@ -993,11 +1001,12 @@
<field name="name">project.task.kanban</field>
<field name="model">project.task</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_kanban_project_tasks" on_create="quick_create" quick_create_view="project.quick_create_task_form" examples="project" js_class="project_task_kanban" sample="1">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_kanban_project_tasks" on_create="quick_create"
quick_create_view="project.quick_create_task_form" examples="project" js_class="project_task_kanban" sample="1">
<field name="color"/>
<field name="priority"/>
<field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/>
<field name="user_id"/>
<field name="user_ids"/>
<field name="partner_id"/>
<field name="sequence"/>
<field name="is_closed"/>
@@ -1013,6 +1022,7 @@
<field name="rating_ids"/>
<field name="allow_subtasks"/>
<field name="child_text"/>
<field name="is_private"/>
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger", "normal": "muted"}'/>
<templates>
<t t-name="kanban-box">
@@ -1024,7 +1034,13 @@
<s t-if="!record.active.raw_value"><field name="name" widget="name_with_subtask_count"/></s>
<t t-else=""><field name="name" widget="name_with_subtask_count"/></t>
</strong>
<span invisible="context.get('default_project_id', False)"><br/><field name="project_id" required="1"/></span>
<t t-if="!record.is_private.raw_value">
<span invisible="context.get('default_project_id', False)"><br/><field name="project_id" required="1"/></span>
</t>
<t t-else="">
<br/>
<span class="fa fa-lock text-muted"/><span class="text-muted"> Private</span>
</t>
<br />
<t t-if="record.partner_id.value">
<span t-if="!record.partner_is_company.raw_value">
@@ -1070,7 +1086,7 @@
</div>
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
<field name="kanban_state" widget="state_selection" groups="base.group_user" invisible="context.get('fsm_mode', False)"/>
<t t-if="record.user_id.raw_value"><field name="user_id" widget="many2one_avatar_user"/></t>
<t t-if="record.user_ids.raw_value"><field name="user_ids" widget="many2many_avatar_user"/></t>
</div>
</div>
</div>
@@ -1098,7 +1114,7 @@
<field name="project_id" optional="show" readonly="1"/>
<field name="partner_id" optional="hide"/>
<field name="parent_id" optional="hide" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
<field name="user_id" optional="show" widget="many2one_avatar_user"/>
<field name="user_ids" optional="show" widget="many2many_avatar_user"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
<field name="activity_ids" widget="list_activity" optional="show"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
@@ -1131,10 +1147,10 @@
<field name="model">project.task</field>
<field eval="2" name="priority"/>
<field name="arch" type="xml">
<calendar date_start="date_deadline" string="Tasks" mode="month" color="user_id" event_limit="5"
<calendar date_start="date_deadline" string="Tasks" mode="month" color="user_ids" event_limit="5"
hide_time="true" js_class="project_calendar" event_open_popup="true" quick_add="false">
<field name="project_id"/>
<field name="user_id" avatar_field="avatar_128" filters="1"/>
<field name="project_id" filters="1"/>
<field name="user_ids" widget="many2many_avatar_user"/>
<field name="partner_id" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="priority" widget="priority"/>
<field name="date_deadline"/>
@@ -1181,10 +1197,10 @@
<field name="model">project.task</field>
<field name="arch" type="xml">
<activity string="Project Tasks" js_class="project_activity">
<field name="user_id"/>
<field name="user_ids"/>
<templates>
<div t-name="activity-box">
<field name="user_id" widget="many2one_avatar_user"/>
<field name="user_ids" widget="many2many_avatar_user"/>
<div>
<field name="name" display="full"/>
<field name="project_id" muted="1" display="full" invisible="context.get('default_project_id', False)"/>
@@ -1213,10 +1229,10 @@
</record>
<record id="action_view_all_task" model="ir.actions.act_window">
<field name="name">Tasks</field>
<field name="name">My Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">tree,kanban,form,calendar,pivot,graph,activity</field>
<field name="context">{'search_default_my_tasks': 1, 'all_task': 0}</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
<field name="context">{'search_default_my_tasks': 1, 'search_default_personal_stage': 1, 'all_task': 0}</field>
<field name="search_view_id" ref="view_task_search_form_extended"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -1239,7 +1255,7 @@
<field name="act_window_id" ref="action_view_task"/>
</record>
<menuitem name="Tasks" id="menu_project_management" parent="menu_main_pm"
<menuitem name="My Tasks" id="menu_project_management" parent="menu_main_pm"
action="action_view_all_task" sequence="2" groups="base.group_no_one,group_project_user"/>
<record id="project_task_action_from_partner" model="ir.actions.act_window">
@@ -1283,7 +1299,7 @@
<field name="name">Assigned Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">tree,form,calendar,graph</field>
<field name="context">{'search_default_user_id': [active_id], 'default_user_id': active_id}</field>
<field name="context">{'search_default_user_ids': [active_id], 'default_user_ids': active_id}</field>
<field name="domain">[('display_project_id', '!=', False)]</field>
<field name="binding_model_id" ref="base.model_res_users"/>
<field name="binding_view_types">form</field>
+1 -1
View File
@@ -259,7 +259,7 @@ class SaleOrderLine(models.Model):
'sale_line_id': self.id,
'sale_order_id': self.order_id.id,
'company_id': project.company_id.id,
'user_id': False, # force non assigned task, as created as sudo()
'user_ids': False, # force non assigned task, as created as sudo()
}
def _timesheet_create_task(self, project):
+21 -21
View File
@@ -34,9 +34,9 @@ class TestNestedTaskUpdate(TransactionCase):
#----------------------------------
def test_creating_subtask_user_id_on_parent_dont_go_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': self.user.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_id': False})
self.assertFalse(child.user_id)
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_ids': False})
self.assertFalse(child.user_ids)
def test_creating_subtask_partner_id_on_parent_goes_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
@@ -88,13 +88,13 @@ class TestNestedTaskUpdate(TransactionCase):
#----------------------------------------
def test_write_user_id_on_parent_dont_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': False})
child = self.env['project.task'].create({'name': 'child', 'user_id': False, 'parent_id': parent.id})
self.assertFalse(child.user_id)
parent.write({'user_id': self.user.id})
self.assertFalse(child.user_id)
parent.write({'user_id': False})
self.assertFalse(child.user_id)
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False})
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'parent_id': parent.id})
self.assertFalse(child.user_ids)
parent.write({'user_ids': [(4, self.user.id)]})
self.assertFalse(child.user_ids)
parent.write({'user_ids': False})
self.assertFalse(child.user_ids)
def test_write_partner_id_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False})
@@ -148,11 +148,11 @@ class TestNestedTaskUpdate(TransactionCase):
#----------------------------------
def test_linking_user_id_on_parent_dont_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': self.user.id})
child = self.env['project.task'].create({'name': 'child', 'user_id': False})
self.assertFalse(child.user_id)
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
child = self.env['project.task'].create({'name': 'child', 'user_ids': False})
self.assertFalse(child.user_ids)
child.write({'parent_id': parent.id})
self.assertFalse(child.user_id)
self.assertFalse(child.user_ids)
def test_linking_partner_id_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
@@ -195,8 +195,8 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertFalse(child.sale_line_id)
def test_writing_on_parent_with_multiple_tasks(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': False, 'partner_id': self.partner.id})
children_values = [{'name': 'child%s' % i, 'user_id': False, 'parent_id': parent.id} for i in range(5)]
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False, 'partner_id': self.partner.id})
children_values = [{'name': 'child%s' % i, 'user_ids': False, 'parent_id': parent.id} for i in range(5)]
children = self.env['project.task'].create(children_values)
children._compute_partner_id()
# test writing sale_line_id
@@ -207,17 +207,17 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertEqual(child.sale_line_id, self.order_line)
def test_linking_on_parent_with_multiple_tasks(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'user_id': self.user.id})
children_values = [{'name': 'child%s' % i, 'user_id': False} for i in range(5)]
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'user_ids': [(4, self.user.id)]})
children_values = [{'name': 'child%s' % i, 'user_ids': False} for i in range(5)]
children = self.env['project.task'].create(children_values)
# test writing user_id and sale_line_id
# test writing user_ids and sale_line_id
for child in children:
self.assertFalse(child.user_id)
self.assertFalse(child.user_ids)
self.assertFalse(child.sale_line_id)
children.write({'parent_id': parent.id})
for child in children:
self.assertEqual(child.sale_line_id, self.order_line)
self.assertFalse(child.user_id)
self.assertFalse(child.user_ids)
@@ -29,7 +29,7 @@
<!-- Project Task -->
<record id="project_task_internal" model="project.task">
<field name="name">Internal training</field>
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="project_id" ref="project.project_project_1"/>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
</record>
@@ -167,7 +167,7 @@
<!-- Assign sale order's task to admin -->
<function model="project.task" name="write">
<value model="project.task" eval="obj().search([('sale_line_id', '=', ref('sale_line_13'))]).ids"/>
<value eval="{'user_id': ref('base.user_admin')}"/>
<value eval="{'user_ids': [(4, ref('base.user_admin'))]}"/>
</function>
<!-- Timesheets on sale_order_1 -->
@@ -482,7 +482,7 @@
<field name="sale_line_id" ref="sale_timesheet.sale_line_13" />
<field name="sale_order_id" ref="sale_timesheet.sale_order_1" />
<field name="partner_id" ref="base.res_partner_2" />
<field name="user_id" ref="base.user_admin"/>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
</record>
<!-- Timesheet for those tasks -->
@@ -246,7 +246,7 @@ class TestSaleService(TestCommonSaleTimesheet):
self.assertTrue(so_line1.project_id, "SO confirmation should create a project and link it to SOL")
self.assertEqual(self.sale_order.tasks_count, 1, "The SO should have only one task")
self.assertEqual(so_line1.task_id.sale_line_id, so_line1, "The created task is also linked to its origin sale line, for invoicing purpose.")
self.assertFalse(so_line1.task_id.user_id, "The created task should be unassigned")
self.assertFalse(so_line1.task_id.user_ids, "The created task should be unassigned")
self.assertEqual(so_line1.product_uom_qty, so_line1.task_id.planned_hours, "The planned hours should be the same as the ordered quantity of the native SO line")
so_line1.write({'product_uom_qty': 20})
@@ -130,7 +130,7 @@
<xpath expr="//field[@name='timesheet_ids']/tree" position="attributes">
<attribute name="decoration-muted">timesheet_invoice_id != False</attribute>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<xpath expr="//field[@name='user_ids']" position="after">
<field name="is_project_map_empty" invisible="1"/>
<field name="has_multi_sol" invisible="1"/>
</xpath>