[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)]"/> <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>
<xpath expr="//filter[@name='my_tasks']" position="after"> <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> </xpath>
</field> </field>
</record> </record>
@@ -147,7 +147,7 @@
<field name="model">project.task</field> <field name="model">project.task</field>
<field name="inherit_id" ref="project.project_sharing_project_task_view_tree" /> <field name="inherit_id" ref="project.project_sharing_project_task_view_tree" />
<field name="arch" type="xml"> <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_subtasks" invisible="1"/>
<field name="allow_timesheets" 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)]}"/> <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="inherit_id" ref="project.view_task_search_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//filter[@name='my_tasks']" position='after'> <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>
<xpath expr="//filter[@name='late']" position='after'> <xpath expr="//filter[@name='late']" position='after'>
<filter string="Tasks in Overtime" name="overtime" domain="[('overtime', '&gt;', 0)]"/> <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"/> <field name="stage_id" ref="project.project_project_stage_2"/>
</record> </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 --> <!-- Tasks -->
<record id="project_task_1" model="project.task"> <record id="project_task_1" model="project.task">
<field name="planned_hours" eval="40.0"/> <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="priority">0</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Meeting Room Furnitures</field> <field name="name">Meeting Room Furnitures</field>
@@ -101,7 +179,7 @@
</record> </record>
<record id="project_task_2" model="project.task"> <record id="project_task_2" model="project.task">
<field name="planned_hours" eval="32.0"/> <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="priority">0</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Lunch Room: kitchen</field> <field name="name">Lunch Room: kitchen</field>
@@ -147,7 +225,7 @@
<record id="project_task_3" model="project.task"> <record id="project_task_3" model="project.task">
<field name="planned_hours" eval="10.0"/> <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="priority">0</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Noise Reduction</field> <field name="name">Noise Reduction</field>
@@ -195,7 +273,7 @@
<record id="project_task_4" model="project.task"> <record id="project_task_4" model="project.task">
<field name="planned_hours" eval="60.0"/> <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="priority">0</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Black Chairs for managers</field> <field name="name">Black Chairs for managers</field>
@@ -228,7 +306,7 @@
<record id="project_task_5" model="project.task"> <record id="project_task_5" model="project.task">
<field name="planned_hours" eval="76.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Room 1: Decoration</field> <field name="name">Room 1: Decoration</field>
@@ -261,7 +339,7 @@
<record id="project_task_6" model="project.task"> <record id="project_task_6" model="project.task">
<field name="planned_hours" eval="24.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Room 2: Decoration</field> <field name="name">Room 2: Decoration</field>
@@ -289,7 +367,7 @@
<record id="project_task_7" model="project.task"> <record id="project_task_7" model="project.task">
<field name="planned_hours" eval="15.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Energy Certificate</field> <field name="name">Energy Certificate</field>
@@ -317,7 +395,7 @@
<record id="project_task_8" model="project.task"> <record id="project_task_8" model="project.task">
<field name="planned_hours" eval="22.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">New portal system</field> <field name="name">New portal system</field>
@@ -366,7 +444,7 @@
<record id="project_task_9" model="project.task"> <record id="project_task_9" model="project.task">
<field name="planned_hours" eval="18.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">Document management</field> <field name="name">Document management</field>
@@ -375,7 +453,7 @@
</record> </record>
<record id="project_task_10" model="project.task"> <record id="project_task_10" model="project.task">
<field name="planned_hours" eval="38.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">Social network integration</field> <field name="name">Social network integration</field>
@@ -404,7 +482,7 @@
<record id="project_task_11" model="project.task"> <record id="project_task_11" model="project.task">
<field name="planned_hours" eval="16.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">User interface improvements</field> <field name="name">User interface improvements</field>
@@ -435,7 +513,7 @@
<record id="project_task_12" model="project.task"> <record id="project_task_12" model="project.task">
<field name="planned_hours" eval="40.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">Planning and budget</field> <field name="name">Planning and budget</field>
@@ -447,7 +525,7 @@
<record id="project_task_19" model="project.task"> <record id="project_task_19" model="project.task">
<field name="planned_hours">24.0</field> <field name="planned_hours">24.0</field>
<field name="stage_id" ref="project_stage_3"/> <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="project_id" ref="project.project_project_2"/>
<field name="name">Basic outline</field> <field name="name">Basic outline</field>
<field name="tag_ids" eval="[(6, 0, [ <field name="tag_ids" eval="[(6, 0, [
@@ -493,7 +571,7 @@
<record id="project_task_20" model="project.task"> <record id="project_task_20" model="project.task">
<field name="planned_hours">42.0</field> <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="stage_id" ref="project_stage_0"/>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">Create new components</field> <field name="name">Create new components</field>
@@ -502,7 +580,7 @@
<record id="project_task_21" model="project.task"> <record id="project_task_21" model="project.task">
<field name="planned_hours">14.0</field> <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="stage_id" ref="project_stage_1"/>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">Useablity review</field> <field name="name">Useablity review</field>
@@ -532,7 +610,7 @@
<record id="project_task_22" model="project.task"> <record id="project_task_22" model="project.task">
<field name="planned_hours">12.0</field> <field name="planned_hours">12.0</field>
<field name="stage_id" ref="project_stage_1"/> <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="priority">0</field>
<field name="project_id" ref="project.project_project_2"/> <field name="project_id" ref="project.project_project_2"/>
<field name="name">Customer analysis + Architecture</field> <field name="name">Customer analysis + Architecture</field>
@@ -562,7 +640,7 @@
<field name="sequence">17</field> <field name="sequence">17</field>
<field name="planned_hours">8.0</field> <field name="planned_hours">8.0</field>
<field name="stage_id" ref="project_stage_2"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_1"/> <field name="project_id" ref="project.project_project_1"/>
<field name="name">Modifications asked by the customer</field> <field name="name">Modifications asked by the customer</field>
@@ -610,7 +688,7 @@
<record id="project_task_25" model="project.task"> <record id="project_task_25" model="project.task">
<field name="sequence">20</field> <field name="sequence">20</field>
<field name="planned_hours">20.0</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="project_id" ref="project.project_project_1"/>
<field name="name">Office planning</field> <field name="name">Office planning</field>
<field name="stage_id" ref="project_stage_0"/> <field name="stage_id" ref="project_stage_0"/>
@@ -620,7 +698,7 @@
<record id="project_task_26" model="project.task"> <record id="project_task_26" model="project.task">
<field name="sequence">20</field> <field name="sequence">20</field>
<field name="planned_hours">35.0</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="project_id" ref="project.project_project_2"/>
<field name="name">Unit Testing</field> <field name="name">Unit Testing</field>
<field name="stage_id" ref="project_stage_2"/> <field name="stage_id" ref="project_stage_2"/>
@@ -665,7 +743,7 @@
<record id="project_task_30" model="project.task"> <record id="project_task_30" model="project.task">
<field name="planned_hours" eval="40.0"/> <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="priority">0</field>
<field name="project_id" ref="project.project_project_3"/> <field name="project_id" ref="project.project_project_3"/>
<field name="name">Entry Hall</field> <field name="name">Entry Hall</field>
@@ -674,7 +752,7 @@
</record> </record>
<record id="project_task_31" model="project.task"> <record id="project_task_31" model="project.task">
<field name="planned_hours" eval="10.0"/> <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="priority">0</field>
<field name="project_id" ref="project.project_project_3"/> <field name="project_id" ref="project.project_project_3"/>
<field name="name">Check Lift</field> <field name="name">Check Lift</field>
@@ -684,7 +762,7 @@
</record> </record>
<record id="project_task_32" model="project.task"> <record id="project_task_32" model="project.task">
<field name="planned_hours" eval="24.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_3"/> <field name="project_id" ref="project.project_project_3"/>
<field name="name">Room 1: Paint</field> <field name="name">Room 1: Paint</field>
@@ -696,7 +774,7 @@
</record> </record>
<record id="project_task_33" model="project.task"> <record id="project_task_33" model="project.task">
<field name="planned_hours" eval="76.0"/> <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="priority">1</field>
<field name="project_id" ref="project.project_project_3"/> <field name="project_id" ref="project.project_project_3"/>
<field name="name">Bathroom</field> <field name="name">Bathroom</field>
+2
View File
@@ -5,6 +5,8 @@ from . import analytic_account
from . import project_milestone from . import project_milestone
from . import project_project_stage from . import project_project_stage
from . import project_task_recurrence 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
from . import project_collaborator from . import project_collaborator
from . import project_update from . import project_update
+131 -19
View File
@@ -43,7 +43,7 @@ PROJECT_TASK_WRITABLE_FIELDS = {
'name', 'name',
'partner_id', 'partner_id',
'partner_email', 'partner_email',
'user_id', 'user_ids',
'date_deadline', 'date_deadline',
'tag_ids', 'tag_ids',
'sequence', 'sequence',
@@ -53,7 +53,6 @@ PROJECT_TASK_WRITABLE_FIELDS = {
'parent_id', 'parent_id',
} }
class ProjectTaskType(models.Model): class ProjectTaskType(models.Model):
_name = 'project.task.type' _name = 'project.task.type'
_description = 'Task Stage' _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.") 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') 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): def unlink_wizard(self, stage_view=False):
self = self.with_context(active_test=False) self = self.with_context(active_test=False)
# retrieves all the projects with a least 1 task in that stage # 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) stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
return stages.browse(stage_ids) 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) active = fields.Boolean(default=True)
name = fields.Char(string='Title', tracking=True, required=True, index=True) name = fields.Char(string='Title', tracking=True, required=True, index=True)
description = fields.Html(string='Description') 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) 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', 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.") 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', # Tracking of this field is done in the write function
string='Assigned to', user_ids = fields.Many2many('res.users', relation='project_task_user_rel', column1='task_id', column2='user_id',
default=lambda self: self.env.uid, string='Assignees', default=lambda self: self.env.user)
index=True, tracking=True) # 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', partner_id = fields.Many2one('res.partner',
string='Customer', string='Customer',
compute='_compute_partner_id', recursive=True, store=True, readonly=False, tracking=True, 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, 'res.company', string='Company', compute='_compute_company_id', store=True, readonly=False,
required=True, copy=True, default=_default_company_id) required=True, copy=True, default=_default_company_id)
color = fields.Integer(string='Color Index') 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", attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments",
help="Attachments that don't come from a message.") 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 # 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") 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 # 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'])]) 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 # Task Dependencies fields
allow_task_dependencies = fields.Boolean(related='project_id.allow_task_dependencies') 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)]") 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 # recurrence fields
@@ -1028,6 +1048,64 @@ class Task(models.Model):
def SELF_WRITABLE_FIELDS(self): def SELF_WRITABLE_FIELDS(self):
return PROJECT_TASK_WRITABLE_FIELDS 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') @api.constrains('depend_on_ids')
def _check_no_cyclic_dependencies(self): def _check_no_cyclic_dependencies(self):
if not self._check_m2m_recursion('depend_on_ids'): if not self._check_m2m_recursion('depend_on_ids'):
@@ -1449,8 +1527,8 @@ class Task(models.Model):
default_project_id=project_id default_project_id=project_id
).default_get(['stage_id']).get('stage_id') ).default_get(['stage_id']).get('stage_id')
vals["stage_id"] = default_stage[project_id] vals["stage_id"] = default_stage[project_id]
# user_id change: update date_assign # user_ids change: update date_assign
if vals.get('user_id'): if vals.get('user_ids'):
vals['date_assign'] = fields.Datetime.now() vals['date_assign'] = fields.Datetime.now()
# Stage change: Update date_end if folded stage and date_last_stage_update # Stage change: Update date_end if folded stage and date_last_stage_update
if vals.get('stage_id'): if vals.get('stage_id'):
@@ -1475,6 +1553,7 @@ class Task(models.Model):
} }
self = self.with_context(ctx).sudo() self = self.with_context(ctx).sudo()
tasks = super(Task, self).create(vals_list) tasks = super(Task, self).create(vals_list)
tasks._populate_missing_personal_stages()
if is_portal_user: if is_portal_user:
# since we use sudo to create tasks, we need to check # 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. # 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 # reset kanban state when changing stage
if 'kanban_state' not in vals: if 'kanban_state' not in vals:
vals['kanban_state'] = 'normal' vals['kanban_state'] = 'normal'
# user_id change: update date_assign # user_ids change: update date_assign
if vals.get('user_id') and 'date_assign' not in vals: if vals.get('user_ids') and 'date_assign' not in vals:
vals['date_assign'] = now vals['date_assign'] = now
# recurrence fields # recurrence fields
@@ -1542,7 +1621,45 @@ class Task(models.Model):
if portal_can_write: if portal_can_write:
tasks = tasks.sudo() 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) 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 # rating on stage
if 'stage_id' in vals and vals.get('stage_id'): 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) 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 # 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') @api.depends('parent_id', 'project_id', 'display_project_id')
def _compute_partner_id(self): def _compute_partner_id(self):
""" Compute the partner_id when the tasks have no partner_id. """ 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 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'], {}) 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) take_action = self._notify_get_action_link('assign', **local_msg_vals)
project_actions = [{'url': take_action, 'title': _('I take it')}] project_actions = [{'url': take_action, 'title': _('I take it')}]
new_group[2]['actions'] = project_actions 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 # want the gateway user to be responsible if no other responsible is
# found. # found.
create_context = dict(self.env.context or {}) create_context = dict(self.env.context or {})
create_context['default_user_id'] = False create_context['default_user_ids'] = False
if custom_values is None: if custom_values is None:
custom_values = {} custom_values = {}
defaults = { defaults = {
@@ -1783,7 +1895,7 @@ class Task(models.Model):
return super(Task, self)._message_post_after_hook(message, msg_vals) return super(Task, self)._message_post_after_hook(message, msg_vals)
def action_assign_to_me(self): 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 == 1, return only direct children
# If depth == 3, return children to third generation # 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() 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['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 return create_values
def _create_next_task(self): 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_end as date_end,
t.date_last_stage_update as date_last_stage_update, t.date_last_stage_update as date_last_stage_update,
t.date_deadline as date_deadline, t.date_deadline as date_deadline,
t.user_id, tu.user_id,
t.project_id, t.project_id,
t.priority, t.priority,
t.name as name, t.name as name,
@@ -82,7 +82,7 @@ class ReportProjectTaskUser(models.Model):
t.date_end, t.date_end,
t.date_deadline, t.date_deadline,
t.date_last_stage_update, t.date_last_stage_update,
t.user_id, tu.user_id,
t.project_id, t.project_id,
t.priority, t.priority,
t.name, t.name,
@@ -98,6 +98,7 @@ class ReportProjectTaskUser(models.Model):
CREATE view %s as CREATE view %s as
%s %s
FROM project_task t FROM project_task t
LEFT JOIN project_task_user_rel tu on t.id=tu.task_id
WHERE t.active = 'true' WHERE t.active = 'true'
%s %s
""" % (self._table, self._select(), self._group_by())) """ % (self._table, self._select(), self._group_by()))
@@ -54,7 +54,7 @@
<field name="date_deadline"/> <field name="date_deadline"/>
<field name="date_last_stage_update"/> <field name="date_last_stage_update"/>
<filter string="My Projects" name="own_projects" domain="[('project_id.user_id', '=', uid)]"/> <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/> <separator/>
<filter string="Starred" name="starred" domain="[('task_id.priority', 'in', [0, 1])]"/> <filter string="Starred" name="starred" domain="[('task_id.priority', 'in', [0, 1])]"/>
<separator/> <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, 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, mm.date as date_end,
mtv.old_value_integer as stage_id, mtv.old_value_integer as stage_id,
pt.user_id, usr_rel.user_id,
pt.date_assign, pt.date_assign,
pt.date_deadline, pt.date_deadline,
pt.partner_id pt.partner_id
@@ -71,6 +71,7 @@ WITH all_moves_stage_task AS (
JOIN ir_model_fields imf ON mtv.field = imf.id JOIN ir_model_fields imf ON mtv.field = imf.id
AND imf.model = 'project.task' AND imf.model = 'project.task'
AND imf.name = 'stage_id' AND imf.name = 'stage_id'
JOIN project_task_user_rel usr_rel ON pt.id=usr_rel.task_id
WHERE pt.active WHERE pt.active
--We compute the last reached stage --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, COALESCE(md.date, pt.create_date) as date_begin,
(CURRENT_DATE + interval '1 month')::date as date_end, (CURRENT_DATE + interval '1 month')::date as date_end,
pt.stage_id, pt.stage_id,
pt.user_id, usr_rel.user_id,
pt.date_assign, pt.date_assign,
pt.date_deadline, pt.date_deadline,
pt.partner_id pt.partner_id
@@ -98,6 +99,7 @@ WITH all_moves_stage_task AS (
AND mm.model = 'project.task' AND mm.model = 'project.task'
ORDER BY mm.id DESC ORDER BY mm.id DESC
FETCH FIRST ROW ONLY) md ON TRUE FETCH FIRST ROW ONLY) md ON TRUE
JOIN project_task_user_rel usr_rel ON pt.id=usr_rel.task_id
WHERE pt.active WHERE pt.active
) )
SELECT (task_id*10^7 + 10^6 + to_char(d, 'YYMMDD')::integer)::bigint as id, 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,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_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_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_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_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 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_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_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_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]), ('message_partner_ids', 'in', [user.partner_id.id]),
# to subscribe check access to the record, follower is not enough at creation # to subscribe check access to the record, follower is not enough at creation
('user_id', '=', user.id) ('user_ids', 'in', user.id)
]</field> ]</field>
<field name="groups" eval="[(4,ref('base.group_user'))]"/> <field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record> </record>
@@ -101,12 +101,54 @@
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/> <field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
</record> </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"> <record model="ir.rule" id="report_project_task_user_report_comp_rule">
<field name="name">Task Analysis multi-company</field> <field name="name">Task Analysis multi-company</field>
<field name="model_id" ref="model_report_project_task_user"/> <field name="model_id" ref="model_report_project_task_user"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field> <field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record> </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 --> <!-- Portal -->
<record id="project_project_rule_portal" model="ir.rule"> <record id="project_project_rule_portal" model="ir.rule">
<field name="name">Project: portal users: portal and following</field> <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 KanbanRenderer from 'web.KanbanRenderer';
import KanbanView from 'web.KanbanView'; import KanbanView from 'web.KanbanView';
import KanbanColumn from 'web.KanbanColumn'; import KanbanColumn from 'web.KanbanColumn';
import viewRegistry from 'web.view_registry';
import KanbanRecord from 'web.KanbanRecord'; 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 { ProjectControlPanel } from '@project/js/project_control_panel';
import viewUtils from 'web.viewUtils';
// PROJECTS // PROJECTS
@@ -70,6 +72,30 @@ const ProjectTaskKanbanRenderer = KanbanRenderer.extend({
config: Object.assign({}, KanbanRenderer.prototype.config, { config: Object.assign({}, KanbanRenderer.prototype.config, {
KanbanColumn: ProjectTaskKanbanColumn, 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({ 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({ const ProjectKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, { config: _.extend({}, KanbanView.prototype.config, {
Model: ProjectTaskKanbanModel,
Controller: ProjectKanbanController, Controller: ProjectKanbanController,
Renderer: ProjectTaskKanbanRenderer, Renderer: ProjectTaskKanbanRenderer,
ControlPanel: ProjectControlPanel, ControlPanel: ProjectControlPanel,
@@ -34,3 +34,8 @@
color: $o-main-text-color; 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_project_update_ui
from . import test_portal from . import test_portal
from . import test_multicompany from . import test_multicompany
from . import test_personal_stages
from . import test_task_dependencies from . import test_task_dependencies
+4 -4
View File
@@ -235,13 +235,13 @@ class TestProjectPortalCommon(TestProjectCommon):
'groups_id': [(6, 0, [])]}) 'groups_id': [(6, 0, [])]})
self.task_3 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({ 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({ 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({ 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({ 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): 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}) Task = cls.env['project.task'].with_context({'mail_create_nolog': True, 'tracking_disable': True})
cls.task_1 = Task.create({ cls.task_1 = Task.create({
'name': 'Task 1 in Project A', '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 'project_id': cls.project_company_a.id
}) })
cls.task_2 = Task.create({ cls.task_2 = Task.create({
'name': 'Task 2 in Project B', '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 '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 # Already-existing tasks in Pigs
cls.task_1 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({ cls.task_1 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Pigs UserTask', 'name': 'Pigs UserTask',
'user_id': cls.user_projectuser.id, 'user_ids': cls.user_projectuser,
'project_id': cls.project_pigs.id}) 'project_id': cls.project_pigs.id})
cls.task_2 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({ cls.task_2 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Pigs ManagerTask', 'name': 'Pigs ManagerTask',
'user_id': cls.user_projectmanager.id, 'user_ids': cls.user_projectmanager,
'project_id': cls.project_pigs.id}) 'project_id': cls.project_pigs.id})
# Test 'Goats' project, same as 'Pigs', but with 2 stages # 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({ parent_task = Task.create({
'name': 'Mother Task', 'name': 'Mother Task',
'user_id': self.user_projectuser.id, 'user_ids': self.user_projectuser,
'project_id': self.project_pigs.id, 'project_id': self.project_pigs.id,
'partner_id': self.partner_2.id, 'partner_id': self.partner_2.id,
'planned_hours': 12, 'planned_hours': 12,
@@ -120,7 +120,7 @@ class TestProjectFlow(TestProjectCommon):
another_parent_task = Task.create({ another_parent_task = Task.create({
'name': 'Another Mother Task', 'name': 'Another Mother Task',
'user_id': self.user_projectuser.id, 'user_ids': self.user_projectuser,
'project_id': self.project_pigs.id, 'project_id': self.project_pigs.id,
'partner_id': self.partner_3.id, 'partner_id': self.partner_3.id,
'planned_hours': 0, 'planned_hours': 0,
@@ -210,7 +210,7 @@ class TestProjectFlow(TestProjectCommon):
Task = self.env['project.task'].with_context({'tracking_disable': True}) Task = self.env['project.task'].with_context({'tracking_disable': True})
first_task = Task.create({ first_task = Task.create({
'name': 'first task', 'name': 'first task',
'user_id': self.user_projectuser.id, 'user_ids': self.user_projectuser,
'project_id': self.project_pigs.id, 'project_id': self.project_pigs.id,
'partner_id': self.partner_2.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({ cls.task_cow = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Cow UserTask', 'name': 'Cow UserTask',
'user_id': cls.user_projectuser.id, 'user_ids': cls.user_projectuser,
'project_id': cls.project_cows.id, 'project_id': cls.project_cows.id,
}) })
cls.task_portal = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({ cls.task_portal = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Portal UserTask', 'name': 'Portal UserTask',
'user_id': cls.user_projectuser.id, 'user_ids': cls.user_projectuser,
'project_id': cls.project_portal.id, 'project_id': cls.project_portal.id,
}) })
@@ -123,7 +123,7 @@ class TestProjectSharing(TestProjectSharingCommon):
task = form.save() task = form.save()
self.assertEqual(task.name, 'Test') self.assertEqual(task.name, 'Test')
self.assertEqual(task.project_id, self.project_portal) 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. # 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."): with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
form.project_id = self.project_cows form.project_id = self.project_cows
@@ -172,4 +172,4 @@ class TestProjectSharing(TestProjectSharingCommon):
form.save() form.save()
self.assertEqual(task.child_ids.name, 'Test Subtask') self.assertEqual(task.child_ids.name, 'Test Subtask')
self.assertEqual(task.child_ids.project_id, self.project_cows) 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 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: with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form:
task_form.name = 'Test Task 1' task_form.name = 'Test Task 1'
task_form.project_id = self.project_pigs 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 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: 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.name = 'Test Task 1'
task_form.project_id = self.project_pigs 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({ cls.task_3 = cls.env['project.task'].with_context({'mail_create_nolog': True}).create({
'name': 'Pigs UserTask 2', 'name': 'Pigs UserTask 2',
'user_id': cls.user_projectuser.id, 'user_ids': cls.user_projectuser,
'project_id': cls.project_pigs.id, 'project_id': cls.project_pigs.id,
}) })
@@ -97,7 +97,8 @@ class TestTaskDependencies(TestProjectCommon):
self.env['res.config.settings'].create({ self.env['res.config.settings'].create({
'group_project_task_dependencies': True 'group_project_task_dependencies': True
}).execute() }).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)] 'depend_on_ids': [Command.link(self.task_2.id)]
}) })
self.cr.precommit.clear() 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> <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> <span class="text-truncate" t-field="tasks[0].sudo().stage_id.name"/></th>
<th t-if="groupby == 'user'"> <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> <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-field="tasks[0].sudo().user_id.name"/></th> <span class="text-truncate" t-esc="', '.join(tasks[0].sudo().mapped('user_ids.name'))"/></th>
<th t-if="groupby == 'customer'"> <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> <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> <span class="text-truncate" t-field="tasks[0].sudo().partner_id.name"/></th>
@@ -119,7 +119,7 @@
#<span t-esc="task.id"/> #<span t-esc="task.id"/>
</td> </td>
<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>
<td t-if="groupby != 'project'" class="text-center"> <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" /> <span class="badge badge-pill badge-info" title="Current project of the task" t-esc="task.project_id.name" />
@@ -177,18 +177,20 @@
</div> </div>
</t> </t>
<t t-set="card_body"> <t t-set="card_body">
<div class="row" t-if="task.user_id or task.partner_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_id"> <div class="col-12 col-md-6 pb-2" t-if="task.user_ids">
<strong>Assigned to</strong> <strong>Assigned to</strong>
<div class="row"> <div class="row">
<div class="col d-flex align-items-center flex-grow-0 pr-3"> <t t-foreach="task.user_ids" t-as="user">
<img class="rounded-circle mt-1 o_portal_contact_img" t-att-src="image_data_uri(task.user_id.avatar_1024)" alt="Contact"/> <div class="col d-flex align-items-center flex-grow-0 pr-3">
</div> <img class="rounded-circle mt-1 o_portal_contact_img" t-att-src="image_data_uri(user.avatar_1024)" alt="Contact"/>
<div class="col pl-md-0"> </div>
<div t-field="task.user_id" t-options='{"widget": "contact", "fields": ["name"]}'/> <div class="col pl-md-0">
<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> <div t-esc="user" t-options='{"widget": "contact", "fields": ["name"]}'/>
<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> <a t-attf-href="mailto:{{user.email}}" t-if="user.email"><div t-esc="user" t-options='{"widget": "contact", "fields": ["email"]}'/></a>
</div> <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> </div>
<div class="coll-12 col-md-6 pb-2" t-if="task.partner_id"> <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="color"/>
<field name="priority"/> <field name="priority"/>
<field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/> <field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/>
<field name="user_id"/> <field name="user_ids"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="sequence"/> <field name="sequence"/>
<field name="is_closed"/> <field name="is_closed"/>
@@ -90,7 +90,7 @@
</div> </div>
<div class="oe_kanban_bottom_right" t-if="!selection_mode"> <div class="oe_kanban_bottom_right" t-if="!selection_mode">
<field name="kanban_state" widget="state_selection"/> <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> </div>
</div> </div>
@@ -116,7 +116,7 @@
<field name="child_text" invisible="1"/> <field name="child_text" invisible="1"/>
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
<field name="partner_id" optional="hide"/> <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="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="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="kanban_state" widget="state_selection" optional="hide"/> <field name="kanban_state" widget="state_selection" optional="hide"/>
@@ -151,10 +151,10 @@
<group> <group>
<field name="project_id" invisible="1"/> <field name="project_id" invisible="1"/>
<field name="display_project_id" string="Project" invisible="1"/> <field name="display_project_id" string="Project" invisible="1"/>
<field name="user_id" <field name="user_ids"
class="o_task_user_field" class="o_task_user_field"
options="{'no_open': True, 'no_open_chat': True}" options="{'no_open': True, 'no_open_chat': True}"
widget="many2one_avatar_user" widget="many2many_avatar_user"
domain="[('share', '=', False)]"/> domain="[('share', '=', False)]"/>
</group> </group>
<group> <group>
@@ -173,7 +173,7 @@
<field name="description" type="html"/> <field name="description" type="html"/>
</page> </page>
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}"> <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"> <tree editable="bottom">
<field name="project_id" invisible="1"/> <field name="project_id" invisible="1"/>
<field name="is_closed" 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="display_project_id" string="Project" optional="hide" invisible="1"/>
<field name="company_id" 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="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="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/> <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
<field name="kanban_state" widget="state_selection" optional="hide"/> <field name="kanban_state" widget="state_selection" optional="hide"/>
@@ -208,11 +208,11 @@
<search string="Tasks"> <search string="Tasks">
<field name="name" string="Task"/> <field name="name" string="Task"/>
<field name="tag_ids"/> <field name="tag_ids"/>
<field name="user_id"/> <field name="user_ids"/>
<field name="partner_id" operator="child_of"/> <field name="partner_id" operator="child_of"/>
<field name="stage_id"/> <field name="stage_id"/>
<field string="Project" name="display_project_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/> <separator/>
<filter string="Starred" name="starred" domain="[('priority', 'in', [1, 2])]"/> <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'))]"/> <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'))]"/> domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Group By"> <group expand="0" string="Group By">
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/> <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="Project" name="project" context="{'group_by': 'project_id'}"/>
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/> <filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
<filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/> <filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/>
+45 -29
View File
@@ -18,14 +18,13 @@
<search string="Tasks"> <search string="Tasks">
<field name="name" string="Task"/> <field name="name" string="Task"/>
<field name="tag_ids"/> <field name="tag_ids"/>
<field name="user_id"/> <field name="user_ids"/>
<field string="Project" name="display_project_id"/> <field string="Project" name="display_project_id"/>
<field name="stage_id"/> <field name="stage_id"/>
<field name="partner_id" operator="child_of"/> <field name="partner_id" operator="child_of"/>
<field name="parent_id"/> <field name="parent_id"/>
<filter string="My Tasks" name="my_tasks" domain="[('user_id', '=', uid)]"/> <filter string="My Tasks" name="my_tasks" domain="[('user_ids', 'in', uid)]"/>
<filter string="Followed Tasks" name="my_followed_tasks" domain="[('message_is_follower', '=', True)]" /> <filter string="Unassigned" name="unassigned" domain="[('user_ids', '=', False)]"/>
<filter string="Unassigned" name="unassigned" domain="[('user_id', '=', False)]"/>
<separator/> <separator/>
<filter string="Starred" name="starred" domain="[('priority', 'in', [0, 1])]"/> <filter string="Starred" name="starred" domain="[('priority', 'in', [0, 1])]"/>
<separator/> <separator/>
@@ -46,7 +45,8 @@
domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/> domain="[('my_activity_date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Group By"> <group expand="0" string="Group By">
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/> <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="Project" name="project" context="{'group_by': 'project_id'}"/>
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/> <filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
<filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/> <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="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
<field name="domain">[('display_project_id', '=', active_id)]</field> <field name="domain">[('display_project_id', '=', active_id)]</field>
<field name="context">{ <field name="context">{
'pivot_row_groupby': ['user_id'], 'pivot_row_groupby': ['user_ids'],
'default_project_id': active_id, 'default_project_id': active_id,
'show_project_update': True, 'show_project_update': True,
}</field> }</field>
@@ -184,6 +184,7 @@
<field name="rating_template_id" optional="hide" groups="project.group_project_rating"/> <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="project_ids" optional="show" widget="many2many_tags" options="{'color_field': 'color'}" />
<field name="fold" optional="show"/> <field name="fold" optional="show"/>
<field name="user_id" optional="show"/>
</tree> </tree>
</field> </field>
</record> </record>
@@ -783,7 +784,7 @@
<field name="allow_task_dependencies" invisible="1" /> <field name="allow_task_dependencies" invisible="1" />
<header> <header>
<button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight" <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)]}"/> <field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}"/>
</header> </header>
<div class="alert alert-info oe_edit_only mb-0" role="status" attrs="{'invisible': ['|', ('recurring_task', '=', False), ('recurrence_id', '=', False)]}"> <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> </div>
<group> <group>
<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="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" class="o_task_user_field"
options="{'no_open': True}" options="{'no_open': True}"
widget="many2one_avatar_user" widget="many2many_avatar_user"
domain="[('share', '=', False)]"/> domain="[('share', '=', False)]"/>
<field name="parent_id" <field name="parent_id"
domain="[('parent_id', '=', False)]" domain="[('parent_id', '=', False)]"
@@ -846,7 +853,7 @@
<field name="description" type="html"/> <field name="description" type="html"/>
</page> </page>
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}"> <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"> <tree editable="bottom">
<field name="project_id" invisible="1"/> <field name="project_id" invisible="1"/>
<field name="is_closed" invisible="1"/> <field name="is_closed" invisible="1"/>
@@ -854,7 +861,7 @@
<field name="name"/> <field name="name"/>
<field name="display_project_id" string="Project" optional="hide"/> <field name="display_project_id" string="Project" optional="hide"/>
<field name="partner_id" 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="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="activity_ids" widget="list_activity" optional="hide"/> <field name="activity_ids" widget="list_activity" optional="hide"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/> <field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
@@ -871,7 +878,7 @@
<field name="is_closed" invisible="1" /> <field name="is_closed" invisible="1" />
<field name="name" /> <field name="name" />
<field name="project_id" optional="hide" /> <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="company_id" optional="hide" groups="base.group_multi_company" />
<field name="activity_ids" widget="list_activity" optional="hide"/> <field name="activity_ids" widget="list_activity" optional="hide"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show" /> <field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show" />
@@ -980,7 +987,8 @@
<form> <form>
<group> <group>
<field name="name" string = "Task Title" placeholder="e.g. New Design"/> <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="company_id" invisible="1"/>
<field name="parent_id" invisible="1" groups="base.group_no_one"/> <field name="parent_id" invisible="1" groups="base.group_no_one"/>
</group> </group>
@@ -993,11 +1001,12 @@
<field name="name">project.task.kanban</field> <field name="name">project.task.kanban</field>
<field name="model">project.task</field> <field name="model">project.task</field>
<field name="arch" type="xml"> <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="color"/>
<field name="priority"/> <field name="priority"/>
<field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/> <field name="stage_id" options='{"group_by_tooltip": {"description": "Description"}}'/>
<field name="user_id"/> <field name="user_ids"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="sequence"/> <field name="sequence"/>
<field name="is_closed"/> <field name="is_closed"/>
@@ -1013,6 +1022,7 @@
<field name="rating_ids"/> <field name="rating_ids"/>
<field name="allow_subtasks"/> <field name="allow_subtasks"/>
<field name="child_text"/> <field name="child_text"/>
<field name="is_private"/>
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger", "normal": "muted"}'/> <progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger", "normal": "muted"}'/>
<templates> <templates>
<t t-name="kanban-box"> <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> <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> <t t-else=""><field name="name" widget="name_with_subtask_count"/></t>
</strong> </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 /> <br />
<t t-if="record.partner_id.value"> <t t-if="record.partner_id.value">
<span t-if="!record.partner_is_company.raw_value"> <span t-if="!record.partner_is_company.raw_value">
@@ -1070,7 +1086,7 @@
</div> </div>
<div class="oe_kanban_bottom_right" t-if="!selection_mode"> <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)"/> <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> </div>
</div> </div>
@@ -1098,7 +1114,7 @@
<field name="project_id" optional="show" readonly="1"/> <field name="project_id" optional="show" readonly="1"/>
<field name="partner_id" optional="hide"/> <field name="partner_id" optional="hide"/>
<field name="parent_id" optional="hide" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/> <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="company_id" groups="base.group_multi_company" optional="show"/>
<field name="activity_ids" widget="list_activity" 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)]}"/> <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 name="model">project.task</field>
<field eval="2" name="priority"/> <field eval="2" name="priority"/>
<field name="arch" type="xml"> <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"> hide_time="true" js_class="project_calendar" event_open_popup="true" quick_add="false">
<field name="project_id"/> <field name="project_id" filters="1"/>
<field name="user_id" avatar_field="avatar_128" filters="1"/> <field name="user_ids" widget="many2many_avatar_user"/>
<field name="partner_id" attrs="{'invisible': [('partner_id', '=', False)]}"/> <field name="partner_id" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="priority" widget="priority"/> <field name="priority" widget="priority"/>
<field name="date_deadline"/> <field name="date_deadline"/>
@@ -1181,10 +1197,10 @@
<field name="model">project.task</field> <field name="model">project.task</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<activity string="Project Tasks" js_class="project_activity"> <activity string="Project Tasks" js_class="project_activity">
<field name="user_id"/> <field name="user_ids"/>
<templates> <templates>
<div t-name="activity-box"> <div t-name="activity-box">
<field name="user_id" widget="many2one_avatar_user"/> <field name="user_ids" widget="many2many_avatar_user"/>
<div> <div>
<field name="name" display="full"/> <field name="name" display="full"/>
<field name="project_id" muted="1" display="full" invisible="context.get('default_project_id', False)"/> <field name="project_id" muted="1" display="full" invisible="context.get('default_project_id', False)"/>
@@ -1213,10 +1229,10 @@
</record> </record>
<record id="action_view_all_task" model="ir.actions.act_window"> <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="res_model">project.task</field>
<field name="view_mode">tree,kanban,form,calendar,pivot,graph,activity</field> <field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
<field name="context">{'search_default_my_tasks': 1, 'all_task': 0}</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="search_view_id" ref="view_task_search_form_extended"/>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
@@ -1239,7 +1255,7 @@
<field name="act_window_id" ref="action_view_task"/> <field name="act_window_id" ref="action_view_task"/>
</record> </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"/> 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"> <record id="project_task_action_from_partner" model="ir.actions.act_window">
@@ -1283,7 +1299,7 @@
<field name="name">Assigned Tasks</field> <field name="name">Assigned Tasks</field>
<field name="res_model">project.task</field> <field name="res_model">project.task</field>
<field name="view_mode">tree,form,calendar,graph</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="domain">[('display_project_id', '!=', False)]</field>
<field name="binding_model_id" ref="base.model_res_users"/> <field name="binding_model_id" ref="base.model_res_users"/>
<field name="binding_view_types">form</field> <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_line_id': self.id,
'sale_order_id': self.order_id.id, 'sale_order_id': self.order_id.id,
'company_id': project.company_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): 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): 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}) 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_id': False}) child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_ids': False})
self.assertFalse(child.user_id) self.assertFalse(child.user_ids)
def test_creating_subtask_partner_id_on_parent_goes_on_child(self): 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}) 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): def test_write_user_id_on_parent_dont_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': False}) parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False})
child = self.env['project.task'].create({'name': 'child', 'user_id': False, 'parent_id': parent.id}) child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'parent_id': parent.id})
self.assertFalse(child.user_id) self.assertFalse(child.user_ids)
parent.write({'user_id': self.user.id}) parent.write({'user_ids': [(4, self.user.id)]})
self.assertFalse(child.user_id) self.assertFalse(child.user_ids)
parent.write({'user_id': False}) parent.write({'user_ids': False})
self.assertFalse(child.user_id) self.assertFalse(child.user_ids)
def test_write_partner_id_on_parent_write_on_child(self): def test_write_partner_id_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False}) 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): 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}) parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
child = self.env['project.task'].create({'name': 'child', 'user_id': False}) child = self.env['project.task'].create({'name': 'child', 'user_ids': False})
self.assertFalse(child.user_id) self.assertFalse(child.user_ids)
child.write({'parent_id': parent.id}) 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): 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}) 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) self.assertFalse(child.sale_line_id)
def test_writing_on_parent_with_multiple_tasks(self): 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}) parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False, 'partner_id': self.partner.id})
children_values = [{'name': 'child%s' % i, 'user_id': False, 'parent_id': parent.id} for i in range(5)] 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 = self.env['project.task'].create(children_values)
children._compute_partner_id() children._compute_partner_id()
# test writing sale_line_id # test writing sale_line_id
@@ -207,17 +207,17 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertEqual(child.sale_line_id, self.order_line) self.assertEqual(child.sale_line_id, self.order_line)
def test_linking_on_parent_with_multiple_tasks(self): 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}) 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_id': False} for i in range(5)] children_values = [{'name': 'child%s' % i, 'user_ids': False} for i in range(5)]
children = self.env['project.task'].create(children_values) 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: for child in children:
self.assertFalse(child.user_id) self.assertFalse(child.user_ids)
self.assertFalse(child.sale_line_id) self.assertFalse(child.sale_line_id)
children.write({'parent_id': parent.id}) children.write({'parent_id': parent.id})
for child in children: for child in children:
self.assertEqual(child.sale_line_id, self.order_line) 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 --> <!-- Project Task -->
<record id="project_task_internal" model="project.task"> <record id="project_task_internal" model="project.task">
<field name="name">Internal training</field> <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="project_id" ref="project.project_project_1"/>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/> <field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
</record> </record>
@@ -167,7 +167,7 @@
<!-- Assign sale order's task to admin --> <!-- Assign sale order's task to admin -->
<function model="project.task" name="write"> <function model="project.task" name="write">
<value model="project.task" eval="obj().search([('sale_line_id', '=', ref('sale_line_13'))]).ids"/> <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> </function>
<!-- Timesheets on sale_order_1 --> <!-- Timesheets on sale_order_1 -->
@@ -482,7 +482,7 @@
<field name="sale_line_id" ref="sale_timesheet.sale_line_13" /> <field name="sale_line_id" ref="sale_timesheet.sale_line_13" />
<field name="sale_order_id" ref="sale_timesheet.sale_order_1" /> <field name="sale_order_id" ref="sale_timesheet.sale_order_1" />
<field name="partner_id" ref="base.res_partner_2" /> <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> </record>
<!-- Timesheet for those tasks --> <!-- 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.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(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.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") 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}) so_line1.write({'product_uom_qty': 20})
@@ -130,7 +130,7 @@
<xpath expr="//field[@name='timesheet_ids']/tree" position="attributes"> <xpath expr="//field[@name='timesheet_ids']/tree" position="attributes">
<attribute name="decoration-muted">timesheet_invoice_id != False</attribute> <attribute name="decoration-muted">timesheet_invoice_id != False</attribute>
</xpath> </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="is_project_map_empty" invisible="1"/>
<field name="has_multi_sol" invisible="1"/> <field name="has_multi_sol" invisible="1"/>
</xpath> </xpath>