[IMP] hr_timesheet: allow timesheeting on sub-tasks with no project
Allow timesheeting on sub-tasks with no project_id set. Instead, refer to the project_id set on its parent_id, and so on, recursively. task-3336215 X-original-commit: d794e41619a36a8c9ad1e77a816e57051207cdcf Part-of: odoo/odoo#124563
This commit is contained in:
committed by
Xavier Bol (xbo)
parent
ca8dc2d9b4
commit
6b3c5370a8
@@ -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_id.allow_timesheets', '=', True), ('project_id', '=?', project_id)]")
|
||||
domain="[('company_id', '=', company_id), ('project_root_id.allow_timesheets', '=', True), ('project_root_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,
|
||||
@@ -103,12 +103,12 @@ class AccountAnalyticLine(models.Model):
|
||||
if timesheet.project_id:
|
||||
timesheet.partner_id = timesheet.task_id.partner_id or timesheet.project_id.partner_id
|
||||
|
||||
@api.depends('task_id', 'task_id.project_id')
|
||||
@api.depends('task_id')
|
||||
def _compute_project_id(self):
|
||||
for line in self:
|
||||
if not line.task_id.project_id or line.project_id == line.task_id.project_id:
|
||||
if not line.task_id.project_root_id or line.project_id == line.task_id.project_root_id:
|
||||
continue
|
||||
line.project_id = line.task_id.project_id
|
||||
line.project_id = line.task_id.project_root_id
|
||||
|
||||
@api.depends('project_id')
|
||||
def _compute_task_id(self):
|
||||
@@ -120,7 +120,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_id:
|
||||
if self.project_id != self.task_id.project_root_id:
|
||||
self.task_id = False
|
||||
|
||||
@api.depends('employee_id')
|
||||
@@ -313,9 +313,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_id:
|
||||
if not task.project_root_id:
|
||||
raise ValidationError(_('Timesheets cannot be created on a private task.'))
|
||||
account_ids = account_ids.union(tasks.analytic_account_id.ids, tasks.project_id.analytic_account_id.ids)
|
||||
account_ids = account_ids.union(tasks.analytic_account_id.ids, tasks.project_root_id.analytic_account_id.ids)
|
||||
|
||||
project_per_id = {}
|
||||
if project_ids:
|
||||
@@ -335,7 +335,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_id.id
|
||||
vals['project_id'] = data.project_root_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,8 +26,9 @@ class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
project_id = fields.Many2one(domain=[('is_internal_project', '=', False)])
|
||||
analytic_account_active = fields.Boolean("Active Analytic Account", compute='_compute_analytic_account_active', compute_sudo=True)
|
||||
allow_timesheets = fields.Boolean("Allow timesheets", related='project_id.allow_timesheets', help="Timesheets can be logged on this task.", readonly=True)
|
||||
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)
|
||||
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)
|
||||
@@ -54,7 +55,7 @@ class Task(models.Model):
|
||||
def _compute_encode_uom_in_days(self):
|
||||
self.encode_uom_in_days = self._uom_in_days()
|
||||
|
||||
@api.depends('analytic_account_id.active', 'project_id.analytic_account_id.active')
|
||||
@api.depends('analytic_account_id.active', 'project_root_id.analytic_account_id.active')
|
||||
def _compute_analytic_account_active(self):
|
||||
""" Overridden in sale_timesheet """
|
||||
for task in self:
|
||||
@@ -162,7 +163,13 @@ class Task(models.Model):
|
||||
|
||||
def write(self, values):
|
||||
# a timesheet must have an analytic account (and a project)
|
||||
if 'project_id' in values and not values.get('project_id') and self._get_timesheet():
|
||||
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)
|
||||
|
||||
|
||||
@@ -484,16 +484,51 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
3) Enter the 8 hour timesheet in the child task
|
||||
4) Check subtask Effective hours in parent task
|
||||
"""
|
||||
self.task1.child_ids = [Command.set([self.task2.id])]
|
||||
self.env['account.analytic.line'].create({
|
||||
subtask_with_project, subtask_without_project = self.env['project.task'].create([
|
||||
{
|
||||
'name': 'Subtask with project set',
|
||||
'project_id': self.project_customer.id,
|
||||
},
|
||||
{
|
||||
'name': 'Suubtask without project set',
|
||||
'child_ids': [Command.create({'name': 'subsubtask without project set'})],
|
||||
},
|
||||
])
|
||||
subsubtask = subtask_without_project.child_ids
|
||||
self.task1.child_ids = subtask_with_project + subtask_without_project
|
||||
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.')
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
Timesheet.create({
|
||||
'name': 'FirstTimeSheet',
|
||||
'project_id': self.project_customer.id,
|
||||
'task_id': self.task2.id,
|
||||
'task_id': subtask_with_project.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.task1.child_ids = [Command.clear()]
|
||||
|
||||
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,
|
||||
'unit_amount': 1.0,
|
||||
'employee_id': self.empl_employee2.id,
|
||||
},
|
||||
{
|
||||
'name': '/',
|
||||
'task_id': subsubtask.id,
|
||||
'unit_amount': 1.0,
|
||||
'employee_id': self.empl_employee2.id,
|
||||
},
|
||||
])
|
||||
self.assertEqual(self.task1.subtask_effective_hours, 10)
|
||||
|
||||
def test_ensure_product_uom_set_in_timesheet(self):
|
||||
self.assertFalse(self.project_customer.timesheet_ids, 'No timesheet should be recorded in this project')
|
||||
@@ -608,3 +643,25 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
self.task2.unlink()
|
||||
with self.assertRaises(RedirectWarning):
|
||||
self.task1.unlink()
|
||||
|
||||
def test_cannot_convert_task_with_timesheets_in_private_task(self):
|
||||
self.env['account.analytic.line'].create({
|
||||
'name': '/',
|
||||
'unit_amount': 1,
|
||||
'project_id': self.project_customer.id,
|
||||
'task_id': self.task1.id,
|
||||
'employee_id': self.empl_employee.id,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.task1.project_id = False
|
||||
|
||||
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):
|
||||
self.task1.write({'project_id': False, 'parent_id': False})
|
||||
|
||||
@@ -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_id, 'default_name':''}">
|
||||
<field name="timesheet_ids" mode="tree,kanban" attrs="{'invisible': [('analytic_account_active', '=', False)]}" context="{'default_project_id': project_root_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)]}"/>
|
||||
|
||||
@@ -151,6 +151,7 @@ class Task(models.Model):
|
||||
|
||||
project_id = fields.Many2one('project.project', string='Project',
|
||||
index=True, tracking=True, check_company=True, change_default=True)
|
||||
project_root_id = fields.Many2one('project.project', compute='_compute_project_root_id', search='_search_project_root_id', recursive=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',
|
||||
@@ -290,6 +291,79 @@ class Task(models.Model):
|
||||
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)
|
||||
@@ -1530,7 +1604,7 @@ class Task(models.Model):
|
||||
# ---------------------------------------------------
|
||||
def _get_task_analytic_account_id(self):
|
||||
self.ensure_one()
|
||||
return self.analytic_account_id or self.project_id.analytic_account_id
|
||||
return self.analytic_account_id or self.project_root_id.analytic_account_id
|
||||
|
||||
@api.model
|
||||
def get_unusual_days(self, date_from, date_to=None):
|
||||
|
||||
@@ -197,3 +197,32 @@ class TestProjectBase(TestProjectCommon):
|
||||
with self.assertRaises(UserError):
|
||||
# Cannot change the company of a project if the company of the partner is different
|
||||
self.project_pigs.company_id = company_1
|
||||
|
||||
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
|
||||
|
||||
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([('project_root_id', '=', project.id)]), tasks)
|
||||
self.assertEqual(ProjectTask.search([('project_root_id', 'in', project.ids)]), tasks)
|
||||
self.assertEqual(ProjectTask.search([('project_root_id.allow_milestones', '=', False)]), tasks)
|
||||
self.assertEqual(ProjectTask.search(['!', ('project_root_id.allow_milestones', '=', True)]), tasks)
|
||||
self.assertEqual(ProjectTask.search([('project_root_id', '=?', project.id)]), tasks)
|
||||
self.assertEqual(ProjectTask.search([('project_root_id', 'not in', other_projects.ids), ('id', 'in', tasks.ids)]), tasks)
|
||||
self.assertEqual(ProjectTask.search([('project_root_id', '!=', self.project_pigs.id), ('id', 'in', tasks.ids)]), tasks)
|
||||
|
||||
@@ -254,3 +254,37 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
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)
|
||||
|
||||
@@ -250,6 +250,7 @@
|
||||
<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"/>
|
||||
<header>
|
||||
<field name="stage_id" widget="statusbar" 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"/>
|
||||
|
||||
@@ -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_id.allow_timesheets', '=', True),"
|
||||
"('project_id', '=?', project_id), ('is_timeoff_task', '=', False)]")
|
||||
task_id = fields.Many2one(domain="[('company_id', '=', company_id), ('project_root_id.allow_timesheets', '=', True),"
|
||||
"('project_root_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
|
||||
|
||||
@@ -696,9 +696,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_id.sale_order_id')
|
||||
project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_root_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_id.allow_billable")
|
||||
allow_billable = fields.Boolean(related="project_root_id.allow_billable")
|
||||
|
||||
# Project sharing fields
|
||||
display_sale_order_button = fields.Boolean(string='Display Sales Order', compute='_compute_display_sale_order_button')
|
||||
|
||||
@@ -46,8 +46,8 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
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.assertFalse(child.allow_billable, "The subtask should not be billable as it is parent task since no project is set")
|
||||
self.assertFalse(child.partner_id, "Subtask with no project should not have partner")
|
||||
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)]})
|
||||
@@ -216,3 +216,33 @@ class TestNestedTaskUpdate(TransactionCase):
|
||||
for child in children:
|
||||
self.assertEqual(child.sale_line_id, self.order_line)
|
||||
self.assertFalse(child.user_ids)
|
||||
|
||||
def test_allow_billable_on_subtasks(self):
|
||||
parent = self.env['project.task'].create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project.id,
|
||||
'child_ids': [
|
||||
Command.create({
|
||||
'name': 'Subtask with project set',
|
||||
'project_id': self.project.id,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Subtask without any project set',
|
||||
'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)
|
||||
|
||||
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)
|
||||
self.assertFalse(subsubtask.allow_billable)
|
||||
|
||||
@@ -325,7 +325,7 @@ class TestSaleProject(TestSaleProjectCommon):
|
||||
]
|
||||
})
|
||||
sub_B_without = task_B.child_ids.filtered(lambda sub: sub.name == 'Sub B without project')
|
||||
self.assertEqual(len(sub_B_without.sale_line_id), 0)
|
||||
self.assertEqual(sub_B_without.sale_line_id, task_B.sale_line_id)
|
||||
|
||||
# [CASE 4] Without parent --> use sale order line of the project
|
||||
task_D = self.env['project.task'].create({
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="sale_order_id" attrs="{'invisible': True}" groups="sales_team.group_sale_salesman"/>
|
||||
<field name="sale_line_id" groups="!sales_team.group_sale_salesman" string="Sales Order Item" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('project_id', '=', False)]}"/>
|
||||
<field name="sale_line_id" groups="sales_team.group_sale_salesman" string="Sales Order Item" options='{"no_create": True}' readonly="0" context="{'create': False, 'edit': False, 'delete': False, 'with_price_unit': True}" placeholder="Non-billable" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('project_id', '=', False)]}"/>
|
||||
<field name="sale_line_id" groups="!sales_team.group_sale_salesman" string="Sales Order Item" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('project_root_id', '=', False)]}"/>
|
||||
<field name="sale_line_id" groups="sales_team.group_sale_salesman" string="Sales Order Item" options='{"no_create": True}' readonly="0" context="{'create': False, 'edit': False, 'delete': False, 'with_price_unit': True}" placeholder="Non-billable" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('project_root_id', '=', False)]}"/>
|
||||
<field name="allow_billable" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='child_ids']/tree/field[@name='partner_id']" position="after">
|
||||
|
||||
@@ -524,10 +524,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_id.pricing_type")
|
||||
pricing_type = fields.Selection(related="project_root_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_id.timesheet_product_id")
|
||||
timesheet_product_id = fields.Many2one(related="project_root_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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user