[IMP] project,_*: allow all project features on tasks w/o project
_*: hr_timesheet,sale_project,sale_timesheet Generally, you don't want subtasks to be displayed at the same level as their parent tasks. You want to consider the subtasks as part of their parent's project, but don't want to see them directly in this project. Rather, you want them to be accessible only via their parent. (There are exceptions to that and we want to stay flexible.) First solution that comes to mind is to have `project_id` set to False for the latter tasks, but then how to get the value of the fields that are related to the project? So, second solution would be to have a field `project_root_id`, which would be the project of the parent, or the grand-parent, etc. The issue now is that we have to fields "project", and it isn't obvious when to use one or the other. The most simple way to answer this need is to keep one field "project", that will always be set for (non-private) tasks, and create a boolean field : `display_in_project`. But we want it to be technical (no checkbox in the view). So, when the user unsets the project on a subtask, the view will act as if the project was unset, but in the back-end, we'll set `display_in_project` to False and set `project_id` back. The fact that all tasks have a project allows us to know if action x can be perform on this task t, dependind on t.project_id.allow_x. task-3367246 closes odoo/odoo#128281 Related: odoo/enterprise#43996 Related: odoo/upgrade#4930 Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
committed by
Xavier Bol (xbo)
parent
fbd38450b1
commit
fb88a7448c
@@ -50,7 +50,7 @@ class AccountAnalyticLine(models.Model):
|
||||
task_id = fields.Many2one(
|
||||
'project.task', 'Task', index='btree_not_null',
|
||||
compute='_compute_task_id', store=True, readonly=False,
|
||||
domain="[('company_id', '=', company_id), ('project_root_id.allow_timesheets', '=', True), ('project_root_id', '=?', project_id)]")
|
||||
domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), ('project_id', '=?', project_id)]")
|
||||
parent_task_id = fields.Many2one('project.task', related='task_id.parent_id', store=True)
|
||||
project_id = fields.Many2one(
|
||||
'project.project', 'Project', domain=_domain_project_id, index=True,
|
||||
@@ -98,9 +98,9 @@ class AccountAnalyticLine(models.Model):
|
||||
@api.depends('task_id')
|
||||
def _compute_project_id(self):
|
||||
for line in self:
|
||||
if not line.task_id.project_root_id or line.project_id == line.task_id.project_root_id:
|
||||
if not line.task_id.project_id or line.project_id == line.task_id.project_id:
|
||||
continue
|
||||
line.project_id = line.task_id.project_root_id
|
||||
line.project_id = line.task_id.project_id
|
||||
|
||||
@api.depends('project_id')
|
||||
def _compute_task_id(self):
|
||||
@@ -112,7 +112,7 @@ class AccountAnalyticLine(models.Model):
|
||||
# TODO KBA in master - check to do it "properly", currently:
|
||||
# This onchange is used to reset the task_id when the project changes.
|
||||
# Doing it in the compute will remove the task_id when the project of a task changes.
|
||||
if self.project_id != self.task_id.project_root_id:
|
||||
if self.project_id != self.task_id.project_id:
|
||||
self.task_id = False
|
||||
|
||||
@api.depends('employee_id')
|
||||
@@ -319,9 +319,9 @@ class AccountAnalyticLine(models.Model):
|
||||
tasks = self.env['project.task'].sudo().browse(task_ids)
|
||||
for task in tasks:
|
||||
task_per_id[task.id] = task
|
||||
if not task.project_root_id:
|
||||
if not task.project_id:
|
||||
raise ValidationError(_('Timesheets cannot be created on a private task.'))
|
||||
account_ids = account_ids.union(tasks.analytic_account_id.ids, tasks.project_root_id.analytic_account_id.ids)
|
||||
account_ids = account_ids.union(tasks.analytic_account_id.ids, tasks.project_id.analytic_account_id.ids)
|
||||
|
||||
project_per_id = {}
|
||||
if project_ids:
|
||||
@@ -341,7 +341,7 @@ class AccountAnalyticLine(models.Model):
|
||||
vals = vals_list[index]
|
||||
data = task_per_id[vals['task_id']] if vals.get('task_id') else project_per_id[vals['project_id']]
|
||||
if not vals.get('project_id'):
|
||||
vals['project_id'] = data.project_root_id.id
|
||||
vals['project_id'] = data.project_id.id
|
||||
if not vals.get('account_id'):
|
||||
account = data._get_task_analytic_account_id() if vals.get('task_id') else data.analytic_account_id
|
||||
if not account or not account.active:
|
||||
|
||||
@@ -26,9 +26,8 @@ class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
project_id = fields.Many2one(domain="['|', ('company_id', '=', False), ('company_id', '=?', company_id), ('is_internal_project', '=', False)]")
|
||||
project_root_id = fields.Many2one(domain=[('is_internal_project', '=', False)])
|
||||
analytic_account_active = fields.Boolean("Active Analytic Account", compute='_compute_analytic_account_active', compute_sudo=True, recursive=True)
|
||||
allow_timesheets = fields.Boolean("Allow timesheets", related="project_root_id.allow_timesheets", help="Timesheets can be logged on this task.", readonly=True, recursive=True)
|
||||
allow_timesheets = fields.Boolean("Allow timesheets", related="project_id.allow_timesheets", help="Timesheets can be logged on this task.", readonly=True, recursive=True)
|
||||
remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', store=True, readonly=True, help="Number of allocated hours minus the number of hours spent.")
|
||||
remaining_hours_percentage = fields.Float(compute='_compute_remaining_hours_percentage', search='_search_remaining_hours_percentage')
|
||||
effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True)
|
||||
@@ -49,13 +48,19 @@ class Task(models.Model):
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS | PROJECT_TASK_READABLE_FIELDS
|
||||
|
||||
@api.constrains('project_id')
|
||||
def _check_project_root(self):
|
||||
private_tasks = self.filtered(lambda t: not t.project_id)
|
||||
if private_tasks and self.env['account.analytic.line'].sudo().search_count([('task_id', 'in', private_tasks.ids)], limit=1):
|
||||
raise UserError(_("This task cannot be private because there are some timesheets linked to it."))
|
||||
|
||||
def _uom_in_days(self):
|
||||
return self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day')
|
||||
|
||||
def _compute_encode_uom_in_days(self):
|
||||
self.encode_uom_in_days = self._uom_in_days()
|
||||
|
||||
@api.depends('analytic_account_id.active', 'project_root_id.analytic_account_id.active')
|
||||
@api.depends('analytic_account_id.active', 'project_id.analytic_account_id.active')
|
||||
def _compute_analytic_account_active(self):
|
||||
""" Overridden in sale_timesheet """
|
||||
for task in self:
|
||||
@@ -161,26 +166,6 @@ class Task(models.Model):
|
||||
# Is override in sale_timesheet
|
||||
return self.timesheet_ids
|
||||
|
||||
def write(self, values):
|
||||
# a timesheet must have an analytic account (and a project)
|
||||
is_removed_project = 'project_id' in values and not values['project_id']
|
||||
is_removed_parent = 'parent_id' in values and not values['parent_id']
|
||||
if (
|
||||
((is_removed_project and (not self.parent_id or is_removed_parent))
|
||||
or (is_removed_parent and not (self.project_id or is_removed_project)))
|
||||
and self._get_timesheet()
|
||||
):
|
||||
raise UserError(_('This task must be part of a project because there are some timesheets linked to it.'))
|
||||
res = super(Task, self).write(values)
|
||||
|
||||
if 'project_id' in values:
|
||||
project = self.env['project.project'].browse(values.get('project_id'))
|
||||
if project.allow_timesheets:
|
||||
# We write on all non yet invoiced timesheet the new project_id (if project allow timesheet)
|
||||
self._get_timesheet().write({'project_id': values.get('project_id')})
|
||||
|
||||
return res
|
||||
|
||||
@api.depends('allow_timesheets', 'planned_hours', 'encode_uom_in_days', 'remaining_hours')
|
||||
@api.depends_context('hr_timesheet_display_remaining_hours')
|
||||
def _compute_display_name(self):
|
||||
|
||||
@@ -291,17 +291,21 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
|
||||
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_customer.id)])
|
||||
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_customer2.id)])
|
||||
self.assertEqual(timesheet_count1, 2, "There are still timesheets linked to Project1")
|
||||
self.assertEqual(timesheet_count2, 1, "1 timesheets should be linked to Project2")
|
||||
self.assertEqual(timesheet_count1, 3, "3 timesheets should be linked to Project1")
|
||||
self.assertEqual(timesheet_count2, 0, "No timesheets should be linked to Project2")
|
||||
self.assertEqual(len(self.task1.timesheet_ids), 1, "The timesheet still should be linked to task1")
|
||||
self.assertEqual(len(task_child.timesheet_ids), 1, "The timesheet still should be linked to task_child")
|
||||
self.assertEqual(len(task_grandchild.timesheet_ids), 1, "The timesheet still should be linked to task_grandchild")
|
||||
|
||||
# it is forbidden to set a task with timesheet without project
|
||||
# It is forbidden to unset the project of a task with timesheet...
|
||||
with self.assertRaises(UserError):
|
||||
self.task1.write({
|
||||
'project_id': False
|
||||
})
|
||||
# ...except if one of its ascendant has one.
|
||||
task_child.write({
|
||||
'project_id': False
|
||||
})
|
||||
|
||||
def test_recompute_amount_for_multiple_timesheets(self):
|
||||
""" Check that amount is recomputed correctly when setting unit_amount for multiple timesheets at once. """
|
||||
@@ -363,7 +367,6 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
|
||||
def test_task_with_timesheet_project_change(self):
|
||||
'''This test checks that no error is raised when moving a task that contains timesheet to another project.
|
||||
This move implying writing on the account.analytic.line.
|
||||
'''
|
||||
|
||||
project_manager = self.env['res.users'].create({
|
||||
@@ -403,7 +406,7 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
'project_id': second_project.id
|
||||
})
|
||||
|
||||
self.assertEqual(timesheet.project_id, second_project, 'The project_id of timesheet should be second_project')
|
||||
self.assertEqual(timesheet.project_id, project, 'The project_id of timesheet shouldn\'t have changed')
|
||||
|
||||
def test_create_timesheet_employee_not_in_company(self):
|
||||
''' ts.employee_id only if the user has an employee in the company or one employee for all companies.
|
||||
@@ -484,40 +487,35 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
3) Enter the 8 hour timesheet in the child task
|
||||
4) Check subtask Effective hours in parent task
|
||||
"""
|
||||
subtask_with_project, subtask_without_project = self.env['project.task'].create([
|
||||
subtask_1, subtask_2 = self.env['project.task'].create([
|
||||
{
|
||||
'name': 'Subtask with project set',
|
||||
'name': 'Subtask 1',
|
||||
'project_id': self.project_customer.id,
|
||||
},
|
||||
{
|
||||
'name': 'Suubtask without project set',
|
||||
'child_ids': [Command.create({'name': 'subsubtask without project set'})],
|
||||
'name': 'Subtask 2',
|
||||
'project_id': self.project_customer.id,
|
||||
'child_ids': [Command.create({'name': 'Subsubtask'})],
|
||||
},
|
||||
])
|
||||
subsubtask = subtask_without_project.child_ids
|
||||
self.task1.child_ids = subtask_with_project + subtask_without_project
|
||||
subsubtask = subtask_2.child_ids
|
||||
self.task1.child_ids = subtask_1 + subtask_2
|
||||
self.assertTrue(self.project_customer.allow_timesheets, 'The project should be timesheetable')
|
||||
self.assertEqual(subtask_with_project.allow_timesheets, self.project_customer.allow_timesheets, 'The subtask should follow the settings of its project linked.')
|
||||
self.assertEqual(subtask_1.allow_timesheets, self.project_customer.allow_timesheets, 'The subtask should follow the settings of its project linked.')
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
Timesheet.create({
|
||||
'name': 'FirstTimeSheet',
|
||||
'project_id': self.project_customer.id,
|
||||
'task_id': subtask_with_project.id,
|
||||
'task_id': subtask_1.id,
|
||||
'unit_amount': 8.0,
|
||||
'employee_id': self.empl_employee2.id,
|
||||
})
|
||||
self.assertEqual(self.task1.subtask_effective_hours, 8, 'Hours Spent on Sub-tasks should be 8 hours in Parent Task')
|
||||
|
||||
self.assertEqual(subtask_without_project.project_root_id, self.task1.project_id, 'The subtask without any project set should follow the settings of the project linked to its parent task.')
|
||||
self.assertTrue(subtask_without_project.allow_timesheets, 'The subtask without any project set should be timesheetable because its parent task is timesheetable.')
|
||||
|
||||
self.assertEqual(subsubtask.project_root_id, self.task1.project_id, 'The subtask without any project set should follow the first project linked to its ancestors.')
|
||||
self.assertTrue(subsubtask.allow_timesheets, 'The subtask without any project set should be timesheetable because its ancestor is timesheetable.')
|
||||
|
||||
Timesheet.create([
|
||||
{
|
||||
'name': '/',
|
||||
'task_id': subtask_without_project.id,
|
||||
'task_id': subtask_2.id,
|
||||
'unit_amount': 1.0,
|
||||
'employee_id': self.empl_employee2.id,
|
||||
},
|
||||
@@ -658,9 +656,6 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
self.task1.parent_id = self.task2
|
||||
self.task1.project_id = False
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.task1.parent_id = False
|
||||
|
||||
self.task1.project_id = self.project_customer
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
You cannot log timesheets on this project since it is linked to an inactive analytic account. Please change this account, or reactivate the current one to timesheet on the project.
|
||||
</div>
|
||||
</group>
|
||||
<field name="timesheet_ids" mode="tree,kanban" attrs="{'invisible': [('analytic_account_active', '=', False)]}" context="{'default_project_id': project_root_id, 'default_name':''}">
|
||||
<field name="timesheet_ids" mode="tree,kanban" attrs="{'invisible': [('analytic_account_active', '=', False)]}" context="{'default_project_id': project_id, 'default_name':''}">
|
||||
<tree editable="bottom" string="Timesheet Activities" default_order="date" decoration-muted="readonly_timesheet == True">
|
||||
<field name="readonly_timesheet" invisible="1"/>
|
||||
<field name="date" attrs="{'readonly': [('readonly_timesheet', '=', True)]}"/>
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
|
||||
'project/static/src/components/project_task_name_with_subtask_count_char_field/*',
|
||||
'project/static/src/components/project_task_state_selection/*',
|
||||
'project/static/src/components/project_private_task_many2one_field/*',
|
||||
'project/static/src/components/project_many2one_field/*',
|
||||
'partner_autocomplete/static/src/js/partner_autocomplete_core.js',
|
||||
'partner_autocomplete/static/src/js/partner_autocomplete_many2one.js',
|
||||
'partner_autocomplete/static/src/xml/partner_autocomplete.xml',
|
||||
|
||||
@@ -19,9 +19,9 @@ PROJECT_TASK_READABLE_FIELDS = {
|
||||
'active',
|
||||
'priority',
|
||||
'project_id',
|
||||
'display_in_project',
|
||||
'color',
|
||||
'subtask_count',
|
||||
'is_private',
|
||||
'email_from',
|
||||
'create_date',
|
||||
'write_date',
|
||||
@@ -34,7 +34,6 @@ PROJECT_TASK_READABLE_FIELDS = {
|
||||
'allow_milestones',
|
||||
'milestone_id',
|
||||
'has_late_and_unreached_milestone',
|
||||
'company_id',
|
||||
'date_assign',
|
||||
'dependent_ids',
|
||||
'message_is_follower',
|
||||
@@ -155,7 +154,7 @@ class Task(models.Model):
|
||||
"Based on this information you can identify tasks that are stalling and get statistics on the time it usually takes to move tasks from one stage/state to another.")
|
||||
|
||||
project_id = fields.Many2one('project.project', string='Project', domain="['|', ('company_id', '=', False), ('company_id', '=?', company_id)]", index=True, tracking=True, change_default=True)
|
||||
project_root_id = fields.Many2one('project.project', compute='_compute_project_root_id', search='_search_project_root_id', recursive=True)
|
||||
display_in_project = fields.Boolean(default=True, readonly=True)
|
||||
task_properties = fields.Properties('Properties', definition='project_id.task_properties_definition', copy=True)
|
||||
planned_hours = fields.Float("Allocated Time", tracking=True)
|
||||
subtask_planned_hours = fields.Float("Sub-tasks Planned Hours", compute='_compute_subtask_planned_hours',
|
||||
@@ -203,7 +202,6 @@ class Task(models.Model):
|
||||
working_days_close = fields.Float(compute='_compute_elapsed', string='Working Days to Close', store=True, group_operator="avg")
|
||||
# customer portal: include comment and incoming emails in communication history
|
||||
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])
|
||||
is_private = fields.Boolean(compute='_compute_is_private', search='_search_is_private')
|
||||
allow_milestones = fields.Boolean(related='project_id.allow_milestones')
|
||||
milestone_id = fields.Many2one(
|
||||
'project.milestone',
|
||||
@@ -268,7 +266,8 @@ class Task(models.Model):
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('recurring_task_has_no_parent', 'CHECK (NOT (recurring_task IS TRUE AND parent_id IS NOT NULL))', "A subtask cannot be recurrent.")
|
||||
('recurring_task_has_no_parent', 'CHECK (NOT (recurring_task IS TRUE AND parent_id IS NOT NULL))', "A subtask cannot be recurrent."),
|
||||
('private_task_has_no_parent', 'CHECK (NOT (project_id IS NULL AND parent_id IS NOT NULL))', "A private task cannot have a parent."),
|
||||
]
|
||||
|
||||
@api.constrains('company_id', 'partner_id')
|
||||
@@ -291,96 +290,6 @@ class Task(models.Model):
|
||||
for task in self:
|
||||
task.analytic_account_id = task.project_id.analytic_account_id
|
||||
|
||||
@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('project_id', 'parent_id.project_id')
|
||||
def _compute_project_root_id(self):
|
||||
# project of the first ascendant that has one. Let's call it the root task.
|
||||
for task in self:
|
||||
task.project_root_id = task.project_id or task.parent_id.project_root_id
|
||||
|
||||
def _search_project_root_id(self, operator, value):
|
||||
if operator not in ("in", "not in", "any", "not any", "=", "!=", "=?"):
|
||||
raise UserError(_("Unsupported operator for search on project_root_id"))
|
||||
|
||||
where_query_1 = where_query_2 = "TRUE"
|
||||
params = tuple()
|
||||
if operator in ("any", "not any"):
|
||||
query = self.env['project.project']._where_calc(value)
|
||||
subquery_str, params = query.subselect()
|
||||
where_operator = 'in' if operator == 'any' else 'not in'
|
||||
where_query_1 = f"""
|
||||
project_id {where_operator} ({subquery_str})
|
||||
"""
|
||||
where_query_2 = f"""
|
||||
project_root_id {'in' if operator == 'any' else 'not in'} ({subquery_str})
|
||||
"""
|
||||
elif operator != "=?" or (value is not False and value is not None):
|
||||
where_operator = operator if operator != '=?' else '='
|
||||
where_query_1 = f"project_id {where_operator} %s"
|
||||
where_query_2 = f"project_root_id {where_operator} %s"
|
||||
if isinstance(value, (list, tuple)):
|
||||
params = (tuple(value),)
|
||||
else:
|
||||
params = (value,)
|
||||
|
||||
self.env.cr.execute(
|
||||
f"""
|
||||
SELECT ARRAY_AGG(id)
|
||||
FROM project_task
|
||||
WHERE {where_query_1}
|
||||
AND project_id IS NOT NULL
|
||||
""", params
|
||||
)
|
||||
task_ids = self._cr.fetchone()[0] or []
|
||||
|
||||
self.env.cr.execute(
|
||||
f"""
|
||||
WITH RECURSIVE project_hierarchy AS (
|
||||
SELECT pt.id,
|
||||
pt.parent_id,
|
||||
pt.project_id,
|
||||
pt.project_id AS project_root_id
|
||||
FROM project_task pt
|
||||
WHERE pt.project_id IS NULL
|
||||
AND pt.parent_id IS NOT NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT ph.id,
|
||||
pt.parent_id,
|
||||
ph.project_id,
|
||||
COALESCE(ph.project_root_id, pt.project_id) AS project_root_id
|
||||
FROM project_hierarchy ph
|
||||
JOIN project_task pt ON ph.parent_id = pt.id
|
||||
)
|
||||
SELECT ARRAY_AGG(id)
|
||||
FROM project_hierarchy
|
||||
WHERE {where_query_2}
|
||||
""", params
|
||||
)
|
||||
if not self._cr.rowcount and not task_ids:
|
||||
return expression.FALSE_DOMAIN
|
||||
task_ids += self.env.cr.fetchone()[0] or []
|
||||
if not task_ids:
|
||||
return expression.FALSE_DOMAIN
|
||||
return [('id', 'in', task_ids)]
|
||||
|
||||
def _search_is_private(self, operator, value):
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError(_('Value should be True or False (not %s)'), value)
|
||||
if operator not in ['=', '!=']:
|
||||
raise NotImplementedError(_('Operation should be = or != (not %s)'), value)
|
||||
domain = expression.normalize_domain([('project_id', '=', False), ('parent_id', '=', False)])
|
||||
if (operator == '=') != value:
|
||||
domain.insert(0, expression.NOT_OPERATOR)
|
||||
domain = expression.distribute_not(domain)
|
||||
return domain
|
||||
|
||||
@api.depends('depend_on_ids.state', 'project_id.allow_task_dependencies')
|
||||
def _compute_state(self):
|
||||
for task in self:
|
||||
@@ -623,9 +532,10 @@ class Task(models.Model):
|
||||
@api.depends('project_id')
|
||||
def _compute_stage_id(self):
|
||||
for task in self:
|
||||
if task.project_id:
|
||||
if task.project_id not in task.stage_id.project_ids:
|
||||
task.stage_id = task.stage_find(task.project_id.id, [('fold', '=', False)])
|
||||
project = task.project_id or task.parent_id.project_id
|
||||
if project:
|
||||
if project not in task.stage_id.project_ids:
|
||||
task.stage_id = task.stage_find(project.id, [('fold', '=', False)])
|
||||
else:
|
||||
task.stage_id = False
|
||||
|
||||
@@ -948,6 +858,28 @@ class Task(models.Model):
|
||||
self.check_access_rights('create')
|
||||
default_stage = dict()
|
||||
for vals in vals_list:
|
||||
project_id = vals.get('project_id')
|
||||
if vals.get('user_ids'):
|
||||
vals['date_assign'] = fields.Datetime.now()
|
||||
if not (vals.get('parent_id') or project_id or self._context.get('default_project_id')):
|
||||
user_ids = self._fields['user_ids'].convert_to_cache(vals.get('user_ids', []), self)
|
||||
if self.env.user.id not in list(user_ids) + [SUPERUSER_ID]:
|
||||
vals['user_ids'] = [Command.set(list(user_ids) + [self.env.user.id])]
|
||||
if project_id:
|
||||
# set the project => "I want to display the task in the project"
|
||||
# => => set `display_in_project` to True
|
||||
vals['display_in_project'] = vals.get('display_in_project', True)
|
||||
elif vals.get('parent_id'):
|
||||
# unset the project => 2 cases:
|
||||
# 1) the task has no parent => "I want it to be private" => nothing to do
|
||||
# 2) the task has a parent => "I don't want to display the task in the project"
|
||||
# => set `project_id` to the one of its parent and `display_in_project` to False
|
||||
project_id = self.browse(vals['parent_id']).project_id.id
|
||||
vals.update({
|
||||
'project_id': project_id,
|
||||
'display_in_project': False,
|
||||
})
|
||||
|
||||
if default_personal_stage and 'personal_stage_type_id' not in vals:
|
||||
vals['personal_stage_type_id'] = default_personal_stage[0]
|
||||
if not vals.get('name') and vals.get('display_name'):
|
||||
@@ -955,7 +887,6 @@ class Task(models.Model):
|
||||
if is_portal_user:
|
||||
self._ensure_fields_are_accessible(vals.keys(), operation='write', check_group_user=False)
|
||||
|
||||
project_id = vals.get('project_id') or self.env.context.get('default_project_id')
|
||||
if project_id and not "company_id" in vals:
|
||||
vals["company_id"] = self.env["project.project"].browse(
|
||||
project_id
|
||||
@@ -973,12 +904,6 @@ class Task(models.Model):
|
||||
).default_get(['stage_id']).get('stage_id')
|
||||
vals["stage_id"] = default_stage[project_id]
|
||||
# user_ids change: update date_assign
|
||||
if vals.get('user_ids'):
|
||||
vals['date_assign'] = fields.Datetime.now()
|
||||
if not project_id:
|
||||
user_ids = self._fields['user_ids'].convert_to_cache(vals.get('user_ids', []), self)
|
||||
if self.env.user.id not in list(user_ids) + [SUPERUSER_ID]:
|
||||
vals['user_ids'] = [Command.set(list(user_ids) + [self.env.user.id])]
|
||||
# Stage change: Update date_end if folded stage and date_last_stage_update
|
||||
if vals.get('stage_id'):
|
||||
vals.update(self.update_date_end(vals['stage_id']))
|
||||
@@ -1032,10 +957,49 @@ class Task(models.Model):
|
||||
self.check_access_rule('write')
|
||||
portal_can_write = True
|
||||
|
||||
now = fields.Datetime.now()
|
||||
if 'parent_id' in vals and vals['parent_id'] in self.ids:
|
||||
raise UserError(_("Sorry. You can't set a task as its parent task."))
|
||||
if 'project_id' in vals:
|
||||
project_id = vals['project_id']
|
||||
if project_id:
|
||||
# set the project => "I want to display the task in the project"
|
||||
# => set `display_in_project` to True
|
||||
if 'display_in_project' not in vals:
|
||||
vals['display_in_project'] = True
|
||||
no_display_subtasks = self.child_ids.filtered(lambda t: not t.display_in_project)
|
||||
if no_display_subtasks:
|
||||
no_display_subtasks.write({'project_id': project_id})
|
||||
else:
|
||||
# unset the project => 2 cases:
|
||||
# 1) the task has no parent => "I want it to be private" => nothing to do
|
||||
# 2) the task has a parent => "I don't want to display the task in the project"
|
||||
# => set `project_id` back and `display_in_project` to False
|
||||
if 'parent_id' in vals:
|
||||
if vals['parent_id']:
|
||||
vals.update({
|
||||
'project_id': self.browse(vals['parent_id']).project_id.id,
|
||||
'display_in_project': False,
|
||||
})
|
||||
else:
|
||||
task_ids_per_parent_project_id = defaultdict(list)
|
||||
for task in self:
|
||||
task_ids_per_parent_project_id[task.parent_id.project_id.id].append(task.id)
|
||||
self = self.browse(task_ids_per_parent_project_id.pop(False, False))
|
||||
for parent_project_id, task_ids in task_ids_per_parent_project_id.items():
|
||||
self.browse(task_ids).write({
|
||||
**vals,
|
||||
'project_id': parent_project_id,
|
||||
'display_in_project': False,
|
||||
})
|
||||
|
||||
if 'parent_id' in vals:
|
||||
parent_id = vals['parent_id']
|
||||
if parent_id in self.ids:
|
||||
raise UserError(_("Sorry. You can't set a task as its parent task."))
|
||||
elif not parent_id:
|
||||
# unset the parent => "I want to display the task back in the project"
|
||||
# => set `display_in_project` to True
|
||||
vals['display_in_project'] = True
|
||||
# stage change: update date_last_stage_update
|
||||
now = fields.Datetime.now()
|
||||
if 'stage_id' in vals:
|
||||
if not 'project_id' in vals and self.filtered(lambda t: not t.project_id):
|
||||
raise UserError(_('You can only set a personal stage on a private task.'))
|
||||
@@ -1127,14 +1091,14 @@ class Task(models.Model):
|
||||
# Subtasks
|
||||
# ---------------------------------------------------
|
||||
|
||||
@api.depends('parent_id.partner_id', 'project_id', 'is_private')
|
||||
@api.depends('parent_id.partner_id', 'project_id')
|
||||
def _compute_partner_id(self):
|
||||
""" Compute the partner_id when the tasks have no partner_id.
|
||||
|
||||
Use the project partner_id if any, or else the parent task partner_id.
|
||||
"""
|
||||
for task in self:
|
||||
if task.partner_id and task.is_private:
|
||||
if task.partner_id and not (task.project_id or task.parent_id):
|
||||
task.partner_id = False
|
||||
continue
|
||||
if not task.partner_id:
|
||||
@@ -1143,7 +1107,7 @@ class Task(models.Model):
|
||||
@api.depends('project_id')
|
||||
def _compute_milestone_id(self):
|
||||
for task in self:
|
||||
if task.project_id != task.milestone_id.project_id:
|
||||
if (task.project_id or task.parent_id.project_id) != task.milestone_id.project_id:
|
||||
task.milestone_id = False
|
||||
|
||||
def _compute_has_late_and_unreached_milestone(self):
|
||||
@@ -1614,7 +1578,7 @@ class Task(models.Model):
|
||||
# ---------------------------------------------------
|
||||
def _get_task_analytic_account_id(self):
|
||||
self.ensure_one()
|
||||
return self.analytic_account_id or self.project_root_id.analytic_account_id
|
||||
return self.analytic_account_id or self.project_id.analytic_account_id
|
||||
|
||||
@api.model
|
||||
def get_unusual_days(self, date_from, date_to=None):
|
||||
|
||||
@@ -117,7 +117,8 @@ class ProjectTask(models.Model):
|
||||
rand = populate.Random('project.task+children_generator')
|
||||
task_ids_per_company = collections.defaultdict(set)
|
||||
for task in tasks:
|
||||
task_ids_per_company[task.company_id].add(task.id)
|
||||
if task.project_id:
|
||||
task_ids_per_company[task.company_id].add(task.id)
|
||||
|
||||
for task_ids in task_ids_per_company.values():
|
||||
parent_ids = set()
|
||||
|
||||
@@ -91,7 +91,6 @@ class ReportProjectTaskUser(models.Model):
|
||||
t.working_hours_open,
|
||||
t.working_hours_close,
|
||||
(extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days,
|
||||
CASE WHEN t.project_id IS NOT NULL OR t.parent_id IS NOT NULL THEN false ELSE true END as is_private,
|
||||
COUNT(td.task_id) as dependent_ids_count
|
||||
"""
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
<field name="name">Project/Task: project manager: see all tasks linked to a project or its own tasks</field>
|
||||
<field name="model_id" ref="model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'|', ('is_private', '=', False),
|
||||
'|', ('project_id', '!=', False),
|
||||
('user_ids', 'in', user.id),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { Many2OneField, many2OneField } from '@web/views/fields/many2one/many2one_field';
|
||||
|
||||
export class ProjectMany2OneField extends Many2OneField {
|
||||
get Many2XAutocompleteProps() {
|
||||
const props = super.Many2XAutocompleteProps;
|
||||
const { project_id, parent_id } = this.props.record.data;
|
||||
if (!project_id && !parent_id) {
|
||||
props.placeholder = this.env._t("Private");
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
const { project_id, display_in_project } = this.props.record.data;
|
||||
return project_id && !display_in_project ? "" : super.displayName;
|
||||
}
|
||||
|
||||
updateRecord(value) {
|
||||
const { project_id, display_in_project } = this.props.record.data;
|
||||
if (!display_in_project && value && value[0] === project_id[0]) {
|
||||
this.props.record.update({ "display_in_project": true });
|
||||
}
|
||||
super.updateRecord(value);
|
||||
}
|
||||
}
|
||||
ProjectMany2OneField.template = 'project.ProjectMany2OneField';
|
||||
|
||||
export const projectMany2OneField = {
|
||||
...many2OneField,
|
||||
component: ProjectMany2OneField,
|
||||
fieldDependencies: [
|
||||
...(many2OneField.fieldDependencies || []),
|
||||
{ name: "display_in_project", type: "boolean" },
|
||||
],
|
||||
};
|
||||
|
||||
registry.category("fields").add("project", projectMany2OneField);
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
.private_placeholder input::placeholder {
|
||||
color: $red;
|
||||
font-style: italic;
|
||||
}
|
||||
+4
-4
@@ -1,21 +1,21 @@
|
||||
<templates>
|
||||
|
||||
<t t-name="project.ProjectPrivateTaskMany2OneField" t-inherit="web.Many2OneField" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="project.ProjectMany2OneField" t-inherit="web.Many2OneField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-if='!props.canOpen']/span" position="attributes">
|
||||
<attribute name="t-if">props.record.data[props.name]</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='!props.canOpen']/span" position="after">
|
||||
<span t-elif="props.record.data.is_private" class="text-danger fst-italic text-muted"><i class="fa fa-lock"></i> Private</span>
|
||||
<span t-elif="!props.record.data.parent_id && !props.record.data.project_id" class="text-danger fst-italic text-muted"><i class="fa fa-lock"></i> Private</span>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-else='']/a" position="attributes">
|
||||
<attribute name="t-if">displayName</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-else='']/a" position="after">
|
||||
<span t-elif="props.record.data.is_private" class="text-danger fst-italic text-muted"><i class="fa fa-lock"></i> Private</span>
|
||||
<span t-elif="!props.record.data.parent_id && !props.record.data.project_id" class="text-danger fst-italic text-muted"><i class="fa fa-lock"></i> Private</span>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_field_many2one_selection')]" position="attributes">
|
||||
<attribute name="t-att-class">{
|
||||
project_private_task_many2one_field: props.record.data.is_private,
|
||||
private_placeholder: !props.record.data.parent_id && !props.record.data.project_id,
|
||||
}</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { Many2OneField, many2OneField } from '@web/views/fields/many2one/many2one_field';
|
||||
|
||||
export class ProjectPrivateTaskMany2OneField extends Many2OneField {
|
||||
get Many2XAutocompleteProps() {
|
||||
const props = super.Many2XAutocompleteProps;
|
||||
if (this.props.record.data.is_private) {
|
||||
props.placeholder = this.env._t("Private");
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
ProjectPrivateTaskMany2OneField.template = 'project.ProjectPrivateTaskMany2OneField';
|
||||
|
||||
export const projectPrivateTaskMany2OneField = {
|
||||
...many2OneField,
|
||||
component: ProjectPrivateTaskMany2OneField,
|
||||
fieldDependencies: [
|
||||
...(many2OneField.fieldDependencies || []),
|
||||
{ name: "is_private", type: "boolean" },
|
||||
],
|
||||
};
|
||||
|
||||
registry.category("fields").add("project_private_task", projectPrivateTaskMany2OneField);
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
.project_private_task_many2one_field input::placeholder {
|
||||
color: $red;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export class SubtaskKanbanList extends Component {
|
||||
field: getFieldFromRegistry(this.fields.user_ids.type, "many2many_avatar_user", "kanban"),
|
||||
},
|
||||
project_id: {
|
||||
field: getFieldFromRegistry(this.fields.project_id.type, "project_private_task", "kanban")
|
||||
field: getFieldFromRegistry(this.fields.project_id.type, "project", "kanban")
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -432,8 +432,6 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
||||
task = task_form.save()
|
||||
self.assertEqual(task.company_id, self.project_company_b.company_id, "The company of the subtask should be the one from its project, and not from its parent.")
|
||||
|
||||
# set parent on existing orphan task; the onchange will set the correct company and subtask project
|
||||
self.task_2.write({'project_id': False})
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# <field name="parent_id" groups="base.group_no_one"/>
|
||||
@@ -446,10 +444,11 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
||||
with Form(self.task_2) as task_form:
|
||||
task_form.name = 'Test Task 2 becomes child of Task 1 (other company)'
|
||||
task_form.parent_id = self.task_1
|
||||
task_form.project_id = self.env['project.project']
|
||||
task = task_form.save()
|
||||
|
||||
self.assertFalse(task.project_id, "The subtask should not have any project set")
|
||||
self.assertEqual(task.company_id, self.task_1.company_id, "The company of the subtask should be the one from its parent when no project is set.")
|
||||
self.assertEqual(task.project_id, task.parent_id.project_id, "The subtask should have the same project as its parents")
|
||||
self.assertEqual(task.company_id, task.parent_id.company_id, "The company of the subtask should be the one from its parent when no project is set.")
|
||||
|
||||
def test_cross_subtask_project(self):
|
||||
|
||||
@@ -471,7 +470,7 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
||||
|
||||
task = task_form.save()
|
||||
|
||||
self.assertFalse(task.project_id, "No project should be set on the subtask by default")
|
||||
self.assertEqual(task.project_id, task.parent_id.project_id, "The subtask should have the same project as its parents")
|
||||
self.assertEqual(task.company_id, task.parent_id.company_id, "The company of the subtask should be the one from its parent.")
|
||||
self.assertEqual(self.task_1.child_ids.ids, [task.id])
|
||||
|
||||
|
||||
@@ -233,38 +233,6 @@ class TestProjectBase(TestProjectCommon):
|
||||
partner.company_id = company_1
|
||||
self.assertEqual(partner.company_id, company_1, "The company of the partner should have been updated.")
|
||||
|
||||
def test_search_project_root_id(self):
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Test project',
|
||||
'allow_milestones': False,
|
||||
})
|
||||
ProjectTask = self.env['project.task']
|
||||
parent = ProjectTask.create({
|
||||
'name': 'Test task',
|
||||
'project_id': project.id,
|
||||
})
|
||||
child = ProjectTask.create({
|
||||
'name': 'Test subtask',
|
||||
'parent_id': parent.id,
|
||||
})
|
||||
tasks = parent | child
|
||||
|
||||
other_projects = self.project_goats + self.project_pigs
|
||||
other_projects.allow_milestones = True
|
||||
# Restrict all searches to the three test projects to avoid interacting with other data
|
||||
base_domain = [('project_root_id', 'in', [project.id] + other_projects.ids)]
|
||||
|
||||
self.assertFalse(child.project_id)
|
||||
self.assertEqual(child.project_root_id, parent.project_id)
|
||||
self.assertEqual(parent.project_root_id, parent.project_id)
|
||||
self.assertEqual(ProjectTask.search(expression.AND([base_domain, [('project_root_id', '=', project.id)]])), tasks)
|
||||
self.assertEqual(ProjectTask.search(expression.AND([base_domain, [('project_root_id', 'in', project.ids)]])), tasks)
|
||||
self.assertEqual(ProjectTask.search(expression.AND([base_domain, [('project_root_id.allow_milestones', '=', False)]])), tasks)
|
||||
self.assertEqual(ProjectTask.search(expression.AND([base_domain, ['!', ('project_root_id.allow_milestones', '=', True)]])), tasks)
|
||||
self.assertEqual(ProjectTask.search(expression.AND([base_domain, [('project_root_id', '=?', project.id)]])), tasks)
|
||||
self.assertEqual(ProjectTask.search(expression.AND([base_domain, [('project_root_id', 'not in', other_projects.ids), ('id', 'in', tasks.ids)]])), tasks)
|
||||
self.assertEqual(ProjectTask.search(expression.AND([base_domain, [('project_root_id', '!=', self.project_pigs.id), ('id', 'in', tasks.ids)]])), tasks)
|
||||
|
||||
def test_add_customer_rating_project(self):
|
||||
""" Tests that the rating_ids field contains a rating once created
|
||||
"""
|
||||
|
||||
@@ -75,9 +75,9 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
- Should not change parent project
|
||||
- Project should be correct
|
||||
3) Reset the project to False
|
||||
- Project should be correct
|
||||
- Project should be inheritted from parent
|
||||
4) Change parent task project
|
||||
- Project should stay false
|
||||
- Project should be inheritted from parent
|
||||
5) Set project on subtask and change parent task project
|
||||
- Project should be the one set by the user
|
||||
6) Remove parent task:
|
||||
@@ -91,7 +91,7 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
|
||||
self.assertFalse(self.task_1.child_ids.project_id, "The project should be False by default.")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.task_1.project_id, "The project should be inheritted from parent.")
|
||||
|
||||
# 2)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
@@ -106,14 +106,14 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
with task_form.child_ids.edit(0) as child_task_form:
|
||||
child_task_form.project_id = self.env['project.project']
|
||||
|
||||
self.assertFalse(self.task_1.child_ids.project_id, "The project of the subtask should be False")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.task_1.project_id, "The project of the subtask should be inheritted from parent")
|
||||
|
||||
# 4)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.project_id = self.project_goats
|
||||
|
||||
self.assertEqual(self.task_1.project_id, self.project_goats, "Parent project should change.")
|
||||
self.assertFalse(self.task_1.child_ids.project_id, "The project of the subtask should stay False")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.task_1.project_id, "The project of the subtask should stay False")
|
||||
|
||||
# 5)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
@@ -160,7 +160,7 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
|
||||
self.assertFalse(self.task_1.child_ids.stage_id, "No stage should be set on the subtask since no project is set by default.")
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "Stage should be set on the subtask since it inheritted the project of its parent.")
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
with task_form.child_ids.edit(0) as subtask_form:
|
||||
subtask_form.project_id = task_form.project_id
|
||||
@@ -249,42 +249,9 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
with Form(child_subtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
with subtask_form.child_ids.new() as child_subtask_form:
|
||||
child_subtask_form.name = 'Test Subtask 2'
|
||||
self.assertFalse(child_subtask_form.project_id)
|
||||
self.assertEqual(child_subtask_form.project_id, subtask_form.project_id)
|
||||
self.assertFalse(child_subtask_form.display_in_project)
|
||||
|
||||
self.assertEqual(task.subtask_count, 1, "Parent task should have 1 children")
|
||||
task_2 = task.copy()
|
||||
self.assertEqual(task_2.subtask_count, 1, "If the parent task is duplicated then the sub task should be copied")
|
||||
|
||||
def test_project_root_id(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task 1',
|
||||
'project_id': self.project_goats.id,
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'Subtask 1',
|
||||
'child_ids': [Command.create({'name': 'Subsubtask 1'})],
|
||||
}),
|
||||
],
|
||||
})
|
||||
subtask = task.child_ids
|
||||
subsubtask = subtask.child_ids
|
||||
self.assertTrue(bool(subtask))
|
||||
self.assertTrue(bool(subsubtask))
|
||||
self.assertEqual(task.project_root_id, task.project_id)
|
||||
self.assertFalse(subtask.project_id)
|
||||
self.assertEqual(subtask.project_root_id, task.project_id)
|
||||
self.assertFalse(subsubtask.project_id)
|
||||
self.assertEqual(subsubtask.project_root_id, task.project_id)
|
||||
|
||||
subtask.project_id = self.project_pigs
|
||||
self.assertNotEqual(subtask.project_root_id, task.project_id)
|
||||
self.assertEqual(subtask.project_root_id, self.project_pigs)
|
||||
self.assertFalse(subsubtask.project_id)
|
||||
self.assertNotEqual(subsubtask.project_root_id, task.project_id)
|
||||
self.assertEqual(subsubtask.project_root_id, subtask.project_id)
|
||||
|
||||
subtask.project_id = False
|
||||
subsubtask.project_id = self.project_pigs
|
||||
self.assertEqual(subtask.project_root_id, task.project_id)
|
||||
self.assertNotEqual(subsubtask.project_root_id, task.project_id)
|
||||
self.assertEqual(subsubtask.project_root_id, self.project_pigs)
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
<field name="description" type="html" options="{'collaborative': true}"/>
|
||||
</page>
|
||||
<page name="sub_tasks_page" string="Sub-tasks">
|
||||
<field name="child_ids" context="{'default_project_id': project_id, 'default_parent_id': id, 'default_partner_id': partner_id, 'form_view_ref' : 'project.project_sharing_project_task_view_form'}">
|
||||
<field name="child_ids" context="{'default_parent_id': id, 'default_partner_id': partner_id, 'form_view_ref' : 'project.project_sharing_project_task_view_form'}">
|
||||
<tree editable="bottom">
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
@@ -205,7 +205,7 @@
|
||||
<field name="stage_id" optional="show"/>
|
||||
<button name="action_open_task" type="object" title="View Task" string="View Task" class="btn btn-link float-end"
|
||||
context="{'form_view_ref': 'project.project_sharing_project_task_view_form', 'search_view_ref': 'project.project_sharing_project_task_view_search'}"
|
||||
attrs="{'invisible': "['|', ('project_id', '=', False), ('project_id', '!=', active_id)]"}"/>
|
||||
attrs="{'invisible': "['|', ('project_id', '!=', False), ('project_id', '!=', active_id)]"}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
@@ -232,7 +232,7 @@
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="search_view_id" ref="project.project_sharing_project_task_view_search"/>
|
||||
<field name="domain">[('project_id', '=', active_id)]</field>
|
||||
<field name="domain">[('project_id', '=', active_id), ('display_in_project', '=', True)]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No tasks found. Let's create one!
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
<field name="name">Tasks</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
|
||||
<field name="domain">[('project_id', '=', active_id)]</field>
|
||||
<field name="domain">[('project_id', '=', active_id), ('display_in_project', '=', True)]</field>
|
||||
<field name="context">{
|
||||
'default_project_id': active_id,
|
||||
'show_project_update': True,
|
||||
@@ -250,12 +250,11 @@
|
||||
<field name="allow_milestones" invisible="1" />
|
||||
<field name="parent_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="is_private" invisible="1"/>
|
||||
<field name="project_root_id" invisible="1"/>
|
||||
<field name="project_id" invisible="1"/>
|
||||
<header>
|
||||
<field name="stage_id" widget="statusbar_duration" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}"/>
|
||||
<field name="state" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" invisible="1"/>
|
||||
<field name="personal_stage_type_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('is_private', '=', False)]}" domain="[('user_id', '=', uid)]" string="Personal Stage"/>
|
||||
<field name="personal_stage_type_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '!=', False)]}" domain="[('user_id', '=', uid)]" string="Personal Stage"/>
|
||||
</header>
|
||||
<sheet string="Task">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
@@ -314,7 +313,7 @@
|
||||
<group>
|
||||
<field name="project_id"
|
||||
domain="[('active', '=', True), '|', ('company_id', '=', False), ('company_id', '=?', company_id)]"
|
||||
widget="project_private_task"
|
||||
widget="project"
|
||||
/>
|
||||
<field name="milestone_id"
|
||||
placeholder="e.g. Product Launch"
|
||||
@@ -361,21 +360,23 @@
|
||||
<page name="description_page" string="Description">
|
||||
<field name="description" type="html" options="{'collaborative': true, 'resizable': false}" placeholder="Add details about this task..."/>
|
||||
</page>
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('is_private', '=', True)]}">
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('project_id', '=', False)]}">
|
||||
<field name="child_ids"
|
||||
context="{'default_user_ids': user_ids, 'default_parent_id': id,
|
||||
context="{'default_project_id': project_id, 'default_display_in_project': False, 'default_user_ids': user_ids, 'default_parent_id': id,
|
||||
'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id}"
|
||||
widget="subtasks_one2many">
|
||||
<tree editable="bottom" decoration-muted="state in ['1_done','1_canceled']">
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="display_in_project" invisible="1" force_save="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="id" optional="hide"/>
|
||||
<field name="parent_id" invisible="1"/>
|
||||
<field name="priority" widget="priority" nolabel="1" options="{'autosave': False}" width="40px"/>
|
||||
<field name="state" widget="project_task_state_selection" nolabel="1" options="{'hide_label': 1, 'autosave': False}" width="40px"/>
|
||||
<field name="name" widget="name_with_subtask_count"/>
|
||||
<field name="subtask_count" invisible="1"/>
|
||||
<field name="closed_subtask_count" invisible="1"/>
|
||||
<field name="project_id" string="Project" optional="hide" options="{'no_open': 1}"/>
|
||||
<field name="project_id" string="Project" optional="hide" options="{'no_open': 1}" widget="project"/>
|
||||
<field name="milestone_id"
|
||||
optional="hide"
|
||||
context="{'default_project_id': project_id}"
|
||||
@@ -489,7 +490,7 @@
|
||||
<group>
|
||||
<field name="display_name" string= "Task Title" placeholder="e.g. Send Invitations" required="1"/>
|
||||
<field name="project_id"
|
||||
widget="project_private_task"
|
||||
widget="project"
|
||||
invisible="context.get('default_project_id', False)"
|
||||
placeholder="Private"
|
||||
class="o_project_task_project_field"
|
||||
@@ -531,7 +532,6 @@
|
||||
<field name="activity_state"/>
|
||||
<field name="rating_count"/>
|
||||
<field name="rating_avg"/>
|
||||
<field name="is_private"/>
|
||||
<field name="rating_active"/>
|
||||
<field name="has_late_and_unreached_milestone" />
|
||||
<field name="allow_milestones" />
|
||||
@@ -597,8 +597,8 @@
|
||||
<a t-if="!record.project_id.raw_value" class="text-muted" style="font-size: 17px; padding-top: 1.5px; margin-left: 1.5px">
|
||||
<i title="Private Task" class="fa fa-lock"/>
|
||||
</a>
|
||||
<t t-if="!record.is_private.raw_value and record.subtask_count.raw_value">
|
||||
<a t-if="!record.is_private.raw_value and record.subtask_count.raw_value and record.subtask_count.value > record.closed_subtask_count.value"
|
||||
<t t-if="record.project_id.raw_value and record.subtask_count.raw_value">
|
||||
<a t-if="record.project_id.raw_value and record.subtask_count.raw_value and record.subtask_count.value > record.closed_subtask_count.value"
|
||||
t-attf-title="{{ record.closed_subtask_count.value }} sub-tasks closed out of {{ record.subtask_count.value }}"
|
||||
class="subtask_list_button text-muted">
|
||||
<span class="fa fa-check-square-o me-1"/>
|
||||
@@ -617,7 +617,7 @@
|
||||
<field name="state" widget="project_task_state_selection" options="{'hide_label': 1, 'is_toggle_mode': false}"/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="!record.is_private.raw_value and record.subtask_count.raw_value and record.subtask_count.raw_value > record.closed_subtask_count.raw_value">
|
||||
<t t-if="record.project_id.raw_value and record.subtask_count.raw_value and record.subtask_count.raw_value > record.closed_subtask_count.raw_value">
|
||||
<div class="kanban_bottom_subtasks_section"/>
|
||||
</t>
|
||||
</div>
|
||||
@@ -636,7 +636,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Tasks" sample="1" default_order="priority desc, sequence, state, date_deadline asc, id desc">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="is_private" invisible="1" />
|
||||
<field name="sequence" invisible="1" readonly="1"/>
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="subtask_count" invisible="1"/>
|
||||
@@ -645,7 +644,7 @@
|
||||
<field name="priority" widget="priority" nolabel="1"/>
|
||||
<field name="state" widget="project_task_state_selection" nolabel="1" options="{'hide_label': 1, 'is_toggle_mode': false}"/>
|
||||
<field name="name" string="Title" widget="name_with_subtask_count"/>
|
||||
<field name="project_id" widget="project_private_task" optional="show" readonly="1" options="{'no_open': 1}" invisible="context.get('default_project_id')"/>
|
||||
<field name="project_id" widget="project" optional="show" readonly="1" options="{'no_open': 1}" invisible="context.get('default_project_id')"/>
|
||||
<field name="milestone_id" attrs="{'invisible': [('allow_milestones', '=', False)]}" context="{'default_project_id': project_id}" groups="project.group_project_milestone" optional="hide"/>
|
||||
<field name="partner_id" optional="hide" widget="res_partner_many2one" attrs="{'invisible': [('project_id', '=', False)]}"/>
|
||||
<field name="user_ids" optional="show" widget="many2many_avatar_user" domain="[('share', '=', False), ('active', '=', True)]"/>
|
||||
@@ -713,16 +712,15 @@
|
||||
js_class="project_calendar"
|
||||
scales="month,year">
|
||||
<field name="allow_milestones" invisible="1" />
|
||||
<field name="is_private" invisible="1"/>
|
||||
<field name="project_id" widget="project_private_task" invisible="context.get('default_project_id', False)"/>
|
||||
<field name="project_id" widget="project" invisible="context.get('default_project_id', False)"/>
|
||||
<field name="milestone_id" attrs="{'invisible': ['|', ('allow_milestones', '=', False), ('milestone_id', '=', False)]}"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" attrs="{'invisible': [('user_ids', '=', [])]}"/>
|
||||
<field name="partner_id" attrs="{'invisible': [('partner_id', '=', False)]}"/>
|
||||
<field name="priority" widget="priority"/>
|
||||
<field name="tag_ids" widget="many2many_tags" attrs="{'invisible': [('tag_ids', '=', [])]}"/>
|
||||
<field name="stage_id" attrs="{'invisible': ['|', ('is_private', '=', True), ('stage_id', '=', False)]}"/>
|
||||
<field name="stage_id" attrs="{'invisible': ['|', ('project_id', '=', False), ('stage_id', '=', False)]}"/>
|
||||
<field name="state" widget="project_task_state_selection" readonly="1"/>
|
||||
<field name="personal_stage_id" string="Personal Stage" attrs="{'invisible': ['|', ('is_private', '=', False), ('personal_stage_id', '=', False)]}"/>
|
||||
<field name="personal_stage_id" string="Personal Stage" attrs="{'invisible': ['|', ('project_id', '!=', False), ('personal_stage_id', '=', False)]}"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
@@ -764,7 +762,6 @@
|
||||
<activity string="Project Tasks" js_class="project_activity">
|
||||
<field name="user_ids"/>
|
||||
<field name="project_id"/>
|
||||
<field name="is_private" />
|
||||
<templates>
|
||||
<div class="w-100" t-name="activity-box" style="display: inline-grid; grid-template-columns: auto max-content;">
|
||||
<div>
|
||||
@@ -773,7 +770,7 @@
|
||||
</span>
|
||||
<span t-att-title="record.project_id.value" invisible="context.get('default_project_id', False)">
|
||||
<field t-if="record.project_id.value" name="project_id" muted="1" class="w-100 text-truncate"/>
|
||||
<span t-elif="record.is_private.value" class="fst-italic text-muted"><i class="fa fa-lock"></i> Private</span>
|
||||
<span t-else="" class="fst-italic text-muted"><i class="fa fa-lock"></i> Private</span>
|
||||
</span>
|
||||
</div>
|
||||
<field name="user_ids" widget="many2many_avatar_user"/>
|
||||
@@ -788,7 +785,7 @@
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
|
||||
<field name="context">{'search_default_my_tasks': 1}</field>
|
||||
<field name="domain">[('project_id', '!=', False)]</field>
|
||||
<field name="domain">[('project_id', '!=', False), ('display_in_project', '=', True)]</field>
|
||||
<field name="search_view_id" ref="view_task_search_form"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
@@ -936,7 +933,7 @@
|
||||
<field name="name">All Tasks</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">tree,kanban,form,calendar,pivot,graph,activity</field>
|
||||
<field name="domain">['|', ('is_private', '=', True), ('project_id', '!=', False)]</field>
|
||||
<field name="domain">[('display_in_project', '=', True)]</field>
|
||||
<field name="context">{'search_default_open_tasks': 1, 'default_user_ids': [(4, uid)]}</field>
|
||||
<field name="search_view_id" ref="view_task_search_form"/>
|
||||
<field name="help" type="html">
|
||||
@@ -991,7 +988,7 @@
|
||||
<field name="name">Overpassed Tasks</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">tree,form,calendar,graph,kanban</field>
|
||||
<field name="domain">[('state', 'not in', ['1_done', '1_canceled']), ('date_deadline','<',time.strftime('%Y-%m-%d')), ('project_id', '!=', False)]</field>
|
||||
<field name="domain">[('state', 'not in', ['1_done', '1_canceled']), ('date_deadline','<',time.strftime('%Y-%m-%d')), ('project_id', '!=', False), ('display_in_project', '=', True)]</field>
|
||||
<field name="filter" eval="True"/>
|
||||
<field name="search_view_id" ref="view_task_search_form"/>
|
||||
</record>
|
||||
@@ -1001,7 +998,7 @@
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="name">Project's tasks</field>
|
||||
<field name="view_mode">tree,form,calendar,graph,kanban</field>
|
||||
<field name="domain">[('project_id', '=', active_id)]</field>
|
||||
<field name="domain">[('project_id', '=', active_id), ('display_in_project', '=', True)]</field>
|
||||
<field name="context">{'project_id':active_id}</field>
|
||||
</record>
|
||||
|
||||
@@ -1029,7 +1026,7 @@
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">tree,form,calendar,graph</field>
|
||||
<field name="context">{'search_default_user_ids': [active_id], 'default_user_ids': [(6, 0, [active_id])]}</field>
|
||||
<field name="domain">[('project_id', '!=', False)]</field>
|
||||
<field name="domain">[('project_id', '!=', False), ('display_in_project', '=', True)]</field>
|
||||
<field name="binding_model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
</record>
|
||||
|
||||
@@ -10,8 +10,8 @@ class AccountAnalyticLine(models.Model):
|
||||
|
||||
holiday_id = fields.Many2one("hr.leave", string='Time Off Request', copy=False)
|
||||
global_leave_id = fields.Many2one("resource.calendar.leaves", string="Global Time Off", ondelete='cascade')
|
||||
task_id = fields.Many2one(domain="[('company_id', '=', company_id), ('project_root_id.allow_timesheets', '=', True),"
|
||||
"('project_root_id', '=?', project_id), ('is_timeoff_task', '=', False)]")
|
||||
task_id = fields.Many2one(domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True),"
|
||||
"('project_id', '=?', project_id), ('is_timeoff_task', '=', False)]")
|
||||
|
||||
def _get_redirect_action(self):
|
||||
leave_form_view_id = self.env.ref('hr_holidays.hr_leave_view_form').id
|
||||
|
||||
@@ -757,9 +757,9 @@ class ProjectTask(models.Model):
|
||||
help="Sales Order Item to which the time spent on this task will be added in order to be invoiced to your customer.\n"
|
||||
"By default the sales order item set on the project will be selected. In the absence of one, the last prepaid sales order item that has time remaining will be used.\n"
|
||||
"Remove the sales order item in order to make this task non billable. You can also change or remove the sales order item of each timesheet entry individually.")
|
||||
project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_root_id.sale_order_id')
|
||||
project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_id.sale_order_id')
|
||||
task_to_invoice = fields.Boolean("To invoice", compute='_compute_task_to_invoice', search='_search_task_to_invoice', groups='sales_team.group_sale_salesman_all_leads')
|
||||
allow_billable = fields.Boolean(related="project_root_id.allow_billable")
|
||||
allow_billable = fields.Boolean(related="project_id.allow_billable")
|
||||
|
||||
# Project sharing fields
|
||||
display_sale_order_button = fields.Boolean(string='Display Sales Order', compute='_compute_display_sale_order_button')
|
||||
@@ -787,14 +787,14 @@ class ProjectTask(models.Model):
|
||||
|
||||
@api.depends('allow_billable')
|
||||
def _compute_partner_id(self):
|
||||
billable_task = self.filtered('allow_billable')
|
||||
billable_task = self.filtered(lambda t: t.allow_billable or (not self._origin and t.parent_id.allow_billable))
|
||||
(self - billable_task).partner_id = False
|
||||
super(ProjectTask, billable_task)._compute_partner_id()
|
||||
|
||||
@api.depends('partner_id.commercial_partner_id', 'sale_line_id.order_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'milestone_id.sale_line_id', 'allow_billable')
|
||||
def _compute_sale_line(self):
|
||||
for task in self:
|
||||
if not task.allow_billable:
|
||||
if not (task.allow_billable or task.parent_id.allow_billable):
|
||||
task.sale_line_id = False
|
||||
continue
|
||||
if not task.sale_line_id:
|
||||
|
||||
@@ -42,16 +42,16 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
|
||||
def test_default_values_creating_subtask(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [Command.link(self.user.id)], 'project_id': self.project.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'project_id': self.project.id})
|
||||
self.assertTrue(parent.allow_billable, "The parent task should be billable as the project linked is billable")
|
||||
self.assertEqual(parent.partner_id, self.project.partner_id, "The partner set on the parent task should the one set on the project linked")
|
||||
self.assertFalse(child.project_id, "The project set on the subtask should be False by default")
|
||||
self.assertEqual(child.project_id, parent.project_id, "The project set on the subtask be inheritted from parent")
|
||||
self.assertTrue(child.allow_billable, "The subtask should be billable since its parent task's project is billable")
|
||||
self.assertEqual(child.partner_id, self.project.partner_id, "The partner set on the subtask should the one set on the project linked to the parent")
|
||||
|
||||
def test_creating_subtask_user_id_on_parent_dont_go_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_ids': False})
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)], 'project_id': self.project.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_ids': False, 'project_id': self.project.id})
|
||||
self.assertFalse(child.user_ids)
|
||||
|
||||
def test_creating_subtask_partner_id_on_parent_goes_on_child(self):
|
||||
@@ -99,8 +99,8 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
#----------------------------------------
|
||||
|
||||
def test_write_user_id_on_parent_dont_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False})
|
||||
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'parent_id': parent.id})
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False, 'project_id': self.project.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'parent_id': parent.id, 'project_id': self.project.id})
|
||||
self.assertFalse(child.user_ids)
|
||||
parent.write({'user_ids': [(4, self.user.id)]})
|
||||
self.assertFalse(child.user_ids)
|
||||
@@ -108,8 +108,13 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
self.assertFalse(child.user_ids)
|
||||
|
||||
def test_write_partner_id_on_parent_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': False, 'parent_id': parent.id})
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False, 'project_id': self.project.id})
|
||||
child = self.env['project.task'].create({
|
||||
'name': 'child',
|
||||
'partner_id': False,
|
||||
'parent_id': parent.id,
|
||||
'project_id': self.env['project.project'].create({'name': 'proute'}).id,
|
||||
})
|
||||
self.assertFalse(child.partner_id)
|
||||
parent.write({'partner_id': self.user.partner_id.id})
|
||||
self.assertNotEqual(child.partner_id, parent.partner_id)
|
||||
@@ -138,7 +143,7 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
|
||||
def test_write_sale_line_id_on_parent_dont_write_on_child_if_other_partner(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'project_id': self.project.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.user.partner_id.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.user.partner_id.id, 'project_id': self.project.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
parent.write({'sale_line_id': self.order_line.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
@@ -150,15 +155,15 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
#----------------------------------
|
||||
|
||||
def test_linking_user_id_on_parent_dont_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
|
||||
child = self.env['project.task'].create({'name': 'child', 'user_ids': False})
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)], 'project_id': self.project.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'project_id': self.project.id})
|
||||
self.assertFalse(child.user_ids)
|
||||
child.write({'parent_id': parent.id})
|
||||
self.assertFalse(child.user_ids)
|
||||
|
||||
def test_linking_partner_id_on_parent_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id, 'project_id': self.project.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': False})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': False, 'project_id': self.project.id})
|
||||
self.assertFalse(child.partner_id)
|
||||
child.write({'parent_id': parent.id, 'project_id': self.project.id})
|
||||
self.assertEqual(child.partner_id, self.user.partner_id)
|
||||
@@ -223,26 +228,21 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
'project_id': self.project.id,
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'Subtask with project set',
|
||||
'name': 'Subtask 1',
|
||||
'project_id': self.project.id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Subtask without any project set',
|
||||
'name': 'Subtask 2',
|
||||
'child_ids': [Command.create({'name': 'Subsubtask'})],
|
||||
}),
|
||||
],
|
||||
})
|
||||
subtask_with_project = parent.child_ids.filtered('project_id')
|
||||
subtask_without_project = parent.child_ids - subtask_with_project
|
||||
subsubtask = subtask_without_project.child_ids
|
||||
|
||||
self.assertTrue(self.project.allow_billable)
|
||||
self.assertTrue(parent.allow_billable)
|
||||
self.assertTrue(subtask_with_project.allow_billable)
|
||||
self.assertTrue(subtask_without_project.allow_billable)
|
||||
self.assertTrue(subsubtask.allow_billable)
|
||||
self.assertTrue(all((parent + parent._get_all_subtasks()).mapped('allow_billable')))
|
||||
|
||||
subtask2 = parent.child_ids.filtered(lambda t: t.name == 'Subtask 2')
|
||||
subsubtask = subtask2.child_ids
|
||||
project_non_billable = self.env['project.project'].create({'name': 'Non-billable project', 'allow_billable': False})
|
||||
subtask_without_project.project_id = project_non_billable
|
||||
self.assertFalse(subtask_without_project.allow_billable)
|
||||
|
||||
subtask2.project_id = project_non_billable
|
||||
self.assertFalse(subtask2.allow_billable)
|
||||
self.assertFalse(subsubtask.allow_billable)
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
type="object"
|
||||
name="action_view_sos"
|
||||
@@ -204,7 +204,7 @@
|
||||
'|',
|
||||
'|',
|
||||
('allow_billable', '=', False),
|
||||
('project_root_id', '=', False),
|
||||
('project_id', '=', False),
|
||||
('sale_line_id', '=', False),
|
||||
('partner_id', '=', False),
|
||||
],
|
||||
@@ -229,7 +229,7 @@
|
||||
'|',
|
||||
'|',
|
||||
('allow_billable', '=', False),
|
||||
('project_root_id', '=', False),
|
||||
('project_id', '=', False),
|
||||
('partner_id', '=', False),
|
||||
],
|
||||
}"
|
||||
|
||||
@@ -537,10 +537,10 @@ class ProjectTask(models.Model):
|
||||
|
||||
sale_order_id = fields.Many2one(domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]")
|
||||
so_analytic_account_id = fields.Many2one(related='sale_order_id.analytic_account_id', string='Sale Order Analytic Account')
|
||||
pricing_type = fields.Selection(related="project_root_id.pricing_type")
|
||||
pricing_type = fields.Selection(related="project_id.pricing_type")
|
||||
is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty')
|
||||
has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True)
|
||||
timesheet_product_id = fields.Many2one(related="project_root_id.timesheet_product_id")
|
||||
timesheet_product_id = fields.Many2one(related="project_id.timesheet_product_id")
|
||||
remaining_hours_so = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours_so', search='_search_remaining_hours_so', compute_sudo=True)
|
||||
remaining_hours_available = fields.Boolean(related="sale_line_id.remaining_hours_available")
|
||||
|
||||
|
||||
@@ -262,9 +262,10 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
self.assertEqual(self.project_employee_rate_manager.project_id, timesheet1.project_id, "The timesheet should be linked to the project of the map entry")
|
||||
|
||||
# create a subtask
|
||||
subtask = Task.with_context(default_project_id=self.project_subtask.id).create({
|
||||
subtask = Task.create({
|
||||
'name': 'first subtask task',
|
||||
'parent_id': task.id,
|
||||
'project_id': self.project_subtask.id,
|
||||
})
|
||||
|
||||
self.assertFalse(subtask.allow_billable, "Subtask in non billable project should be non billable too")
|
||||
|
||||
@@ -518,15 +518,15 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
self.assertEqual(timesheet_count2, 1, "One timesheet in project_template")
|
||||
self.assertEqual(len(task.timesheet_ids), 1, "The timesheet should be linked to task")
|
||||
|
||||
# change project of task, as the timesheet is not yet invoiced, the timesheet will change his project
|
||||
# change project of task, it has no impact on timesheet. never.
|
||||
task.write({
|
||||
'project_id': self.project_global.id
|
||||
})
|
||||
|
||||
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
|
||||
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
|
||||
self.assertEqual(timesheet_count1, 1, "One timesheet in project_global")
|
||||
self.assertEqual(timesheet_count2, 0, "No timesheet in project_template")
|
||||
self.assertEqual(timesheet_count1, 0, "No timesheet in project_global")
|
||||
self.assertEqual(timesheet_count2, 1, "One timesheet in project_template")
|
||||
self.assertEqual(len(task.timesheet_ids), 1, "The timesheet still should be linked to task")
|
||||
|
||||
# Create an invoice
|
||||
@@ -550,9 +550,9 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
'unit_amount': 6,
|
||||
})
|
||||
|
||||
self.assertEqual(Timesheet.search_count([('project_id', '=', self.project_global.id)]), 2, "2 timesheets in project_global")
|
||||
self.assertEqual(Timesheet.search_count([('project_id', '=', self.project_global.id)]), 1, "1 timesheets in project_global")
|
||||
|
||||
# change project of task, the timesheet not yet invoiced will change its project. The timesheet already invoiced will not change his project.
|
||||
# change project of task, it has no impact on timesheet. never.
|
||||
task.write({
|
||||
'project_id': self.project_template.id
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user