[IMP] sale_timesheet: improve 'task_in_project' to handle more use cases
Task #1915660 Purpose ======= If one or more sale_order_line have a product configured with this option, a new 'project_id' field appears on the sale_order under the 'Sales Information' section. When a project_id is selected, the analytic account on the sale order is automatically changed to this project's analytic account. This allows handling more use cases on sale_order: - If a sale_order_line has a service product with that new option and no project_id configured on the sale_order: -> create a new project based on the configured project_template_id and then create a task in that project (same behavior as before) - If a sale_order_line has a service product with that new option and a project_id is configured on the sale_order: -> directly create a task in the selected project_id The behavior does not change for the 3 other options ('no', 'task_global_project' and 'project_only'). Specs ======= Add project field on sale order in other info tab, shows only when a product on the SO has the option activated if projec its selected on SO, create task in this project Tech Spec (jem) ======= On product: - rename 'task_new_project' into "task_in_project" (help message precise either new project (based on template) or SO's project is prior - allow to have the new option with project template On sale order: - non stored computed field to check if at least one SOL has the new option. - if so, display project field - selectable project are billate_type in 'no' and 'task_rate' (TBC for the 2nd) [awa: also add 'analytic_account_id' != False in domain] - setting project on SO should change the AA of the SO. [awa: add onchange on project_id to select this project's analytic account on the SO] - Never link SO's project to SO or AA [awa: do not touch the project_id's sale_order_id and analytic_account_id fields for this use case] - 2 SOL with new option (no template) will create 2 tasks in SO's project - 2 SOL with new options (one no template, one with) + 1 SOL "create new P no T" --> create 2 tasks in SO's project and one new project
This commit is contained in:
@@ -22,7 +22,10 @@ class ProductTemplate(models.Model):
|
||||
('task_in_project', 'Create a task in sale order\'s project'),
|
||||
('project_only', 'Create a new project but no task'),
|
||||
], string="Service Tracking", default="no",
|
||||
help="On Sales order confirmation, this product can generate a project and/or task. From those, you can track the service you are selling.")
|
||||
help="On Sales order confirmation, this product can generate a project and/or task. \
|
||||
From those, you can track the service you are selling.\n \
|
||||
'In sale order\'s project': Will use the sale order\'s configured project if defined or fallback to \
|
||||
creating a new project based on the selected template.")
|
||||
project_id = fields.Many2one(
|
||||
'project.project', 'Project', company_dependent=True, domain=[('billable_type', '=', 'no')],
|
||||
help='Select a non billable project on which tasks can be created. This setting must be set for each company.')
|
||||
|
||||
@@ -17,6 +17,11 @@ class SaleOrder(models.Model):
|
||||
tasks_ids = fields.Many2many('project.task', compute='_compute_tasks_ids', string='Tasks associated to this sale')
|
||||
tasks_count = fields.Integer(string='Tasks', compute='_compute_tasks_ids', groups="project.group_project_user")
|
||||
|
||||
visible_project = fields.Boolean('Display project', compute='_compute_visible_project', readonly=True)
|
||||
project_id = fields.Many2one(
|
||||
'project.project', 'Project', domain=[('billable_type', 'in', ('no', 'task_rate')), ('analytic_account_id', '!=', False)],
|
||||
readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
help='Select a non billable project on which tasks can be created.')
|
||||
project_ids = fields.Many2many('project.project', compute="_compute_project_ids", string='Projects', copy=False, groups="project.group_project_user", help="Projects used in this sales order.")
|
||||
|
||||
@api.multi
|
||||
@@ -39,14 +44,31 @@ class SaleOrder(models.Model):
|
||||
order.tasks_ids = self.env['project.task'].search([('sale_line_id', 'in', order.order_line.ids)])
|
||||
order.tasks_count = len(order.tasks_ids)
|
||||
|
||||
@api.multi
|
||||
@api.depends('order_line.product_id.service_tracking')
|
||||
def _compute_visible_project(self):
|
||||
""" Users should be able to select a project_id on the SO if at least one SO line has a product with its service tracking
|
||||
configured as 'task_in_project' """
|
||||
for order in self:
|
||||
order.visible_project = any(
|
||||
service_tracking == 'task_in_project' for service_tracking in order.order_line.mapped('product_id.service_tracking')
|
||||
)
|
||||
|
||||
@api.multi
|
||||
@api.depends('order_line.product_id', 'order_line.project_id')
|
||||
def _compute_project_ids(self):
|
||||
for order in self:
|
||||
projects = order.order_line.mapped('product_id.project_id')
|
||||
projects |= order.order_line.mapped('project_id')
|
||||
projects |= order.project_id
|
||||
order.project_ids = projects
|
||||
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project_id(self):
|
||||
""" Set the SO analytic account to the selected project's analytic account """
|
||||
if self.project_id.analytic_account_id:
|
||||
self.analytic_account_id = self.project_id.analytic_account_id
|
||||
|
||||
@api.multi
|
||||
def _action_confirm(self):
|
||||
""" On SO confirmation, some lines should generate a task or a project. """
|
||||
@@ -305,6 +327,21 @@ class SaleOrderLine(models.Model):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _determine_project(so_line):
|
||||
"""Determine the project for this sale order line.
|
||||
Rules are different based on the service_tracking:
|
||||
|
||||
- 'project_only': the project_id can only come from the sale order line itself
|
||||
- 'task_in_project': the project_id comes from the sale order line only if no project_id was configured
|
||||
on the parent sale order"""
|
||||
|
||||
if so_line.product_id.service_tracking == 'project_only':
|
||||
return so_line.project_id
|
||||
elif so_line.product_id.service_tracking == 'task_in_project':
|
||||
return so_line.order_id.project_id or so_line.project_id
|
||||
|
||||
return False
|
||||
|
||||
# task_global_project: create task in global project
|
||||
for so_line in so_line_task_global_project:
|
||||
if not so_line.task_id:
|
||||
@@ -312,8 +349,9 @@ class SaleOrderLine(models.Model):
|
||||
so_line._timesheet_create_task(project=map_sol_project[so_line.id])
|
||||
|
||||
# project_only, task_in_project: create a new project, based or not on a template (1 per SO). May be create a task too.
|
||||
# if 'task_in_project' and project_id configured on SO, use that one instead
|
||||
for so_line in so_line_new_project:
|
||||
project = so_line.project_id
|
||||
project = _determine_project(so_line)
|
||||
if not project and _can_create_project(so_line):
|
||||
project = so_line._timesheet_create_project()
|
||||
if so_line.product_id.project_template_id:
|
||||
|
||||
@@ -28,11 +28,16 @@ class TestCommonSaleTimesheetNoChart(TestCommonSaleNoChart):
|
||||
'reconcile': True,
|
||||
'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
|
||||
})
|
||||
cls.analytic_account_sale = cls.env['account.analytic.account'].create({
|
||||
'name': 'Project for selling timesheet - AA',
|
||||
'code': 'AA-2030'
|
||||
})
|
||||
|
||||
# Create projects
|
||||
cls.project_global = cls.env['project.project'].create({
|
||||
'name': 'Project for selling timesheets',
|
||||
'allow_timesheets': True,
|
||||
'analytic_account_id': cls.analytic_account_sale.id,
|
||||
})
|
||||
cls.project_template = cls.env['project.project'].create({
|
||||
'name': 'Project TEMPLATE for services',
|
||||
|
||||
@@ -390,6 +390,106 @@ class TestSaleService(TestCommonSaleTimesheetNoChart):
|
||||
self.assertEqual(so_line2.project_id.sale_line_id, so_line2, "SO line of project should be the one that create it.")
|
||||
self.assertEqual(so_line5.project_id.sale_line_id, so_line5, "SO line of project with template B should be the one that create it.")
|
||||
|
||||
def test_sale_task_in_project_with_project(self):
|
||||
""" This will test the new 'task_in_project' service tracking correctly creates tasks and projects
|
||||
when a project_id is configured on the parent sale_order (ref task #1915660).
|
||||
|
||||
Setup:
|
||||
- Configure a project_id on the SO
|
||||
- SO line 1: a product with its delivery tracking set to 'task_in_project'
|
||||
- SO line 2: the same product as SO line 1
|
||||
- SO line 3: a product with its delivery tracking set to 'project_only'
|
||||
- Confirm sale_order
|
||||
|
||||
Expected result:
|
||||
- 2 tasks created on the project_id configured on the SO
|
||||
- 1 project created with the correct template for the 'project_only' product
|
||||
"""
|
||||
|
||||
self.sale_order.write({'project_id': self.project_global.id})
|
||||
self.sale_order._onchange_project_id()
|
||||
self.assertEqual(self.sale_order.analytic_account_id, self.analytic_account_sale, "Changing the project on the SO should set the analytic account accordingly.")
|
||||
|
||||
so_line1 = self.env['sale.order.line'].create({
|
||||
'name': self.product_order_timesheet3.name,
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom_qty': 11,
|
||||
'product_uom': self.product_order_timesheet3.uom_id.id,
|
||||
'price_unit': self.product_order_timesheet3.list_price,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
so_line2 = self.env['sale.order.line'].create({
|
||||
'name': self.product_order_timesheet3.name,
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.product_order_timesheet3.uom_id.id,
|
||||
'price_unit': self.product_order_timesheet3.list_price,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
so_line3 = self.env['sale.order.line'].create({
|
||||
'name': self.product_order_timesheet4.name,
|
||||
'product_id': self.product_order_timesheet4.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': self.product_order_timesheet4.uom_id.id,
|
||||
'price_unit': self.product_order_timesheet4.list_price,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
||||
# temporary project_template_id for our checks
|
||||
self.product_order_timesheet4.write({
|
||||
'project_template_id': self.project_template.id
|
||||
})
|
||||
self.sale_order.action_confirm()
|
||||
# remove it after the confirm because other tests don't like it
|
||||
self.product_order_timesheet4.write({
|
||||
'project_template_id': False
|
||||
})
|
||||
|
||||
self.assertTrue(so_line1.task_id, "so_line1 should create a task as its product's service_tracking is set as 'task_in_project'")
|
||||
self.assertEqual(so_line1.task_id.project_id, self.project_global, "The project on so_line1's task should be project_global as configured on its parent sale_order")
|
||||
self.assertTrue(so_line2.task_id, "so_line2 should create a task as its product's service_tracking is set as 'task_in_project'")
|
||||
self.assertEqual(so_line2.task_id.project_id, self.project_global, "The project on so_line2's task should be project_global as configured on its parent sale_order")
|
||||
self.assertFalse(so_line3.task_id.name, "so_line3 should not create a task as its product's service_tracking is set as 'project_only'")
|
||||
self.assertNotEqual(so_line3.project_id, self.project_template, "so_line3 should create a new project and not directly use the configured template")
|
||||
self.assertIn(self.project_template.name, so_line3.project_id.name, "The created project for so_line3 should use the configured template")
|
||||
|
||||
def test_sale_task_in_project_without_project(self):
|
||||
""" This will test the new 'task_in_project' service tracking correctly creates tasks and projects
|
||||
when the parent sale_order does NOT have a configured project_id (ref task #1915660).
|
||||
|
||||
Setup:
|
||||
- SO line 1: a product with its delivery tracking set to 'task_in_project'
|
||||
- Confirm sale_order
|
||||
|
||||
Expected result:
|
||||
- 1 project created with the correct template for the 'task_in_project' because the SO
|
||||
does not have a configured project_id
|
||||
- 1 task created from this new project
|
||||
"""
|
||||
|
||||
so_line1 = self.env['sale.order.line'].create({
|
||||
'name': self.product_order_timesheet3.name,
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.product_order_timesheet3.uom_id.id,
|
||||
'price_unit': self.product_order_timesheet3.list_price,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
||||
# temporary project_template_id for our checks
|
||||
self.product_order_timesheet3.write({
|
||||
'project_template_id': self.project_template.id
|
||||
})
|
||||
self.sale_order.action_confirm()
|
||||
# remove it after the confirm because other tests don't like it
|
||||
self.product_order_timesheet3.write({
|
||||
'project_template_id': False
|
||||
})
|
||||
|
||||
self.assertTrue(so_line1.task_id, "so_line1 should create a task as its product's service_tracking is set as 'task_in_project'")
|
||||
self.assertNotEqual(so_line1.project_id, self.project_template, "so_line1 should create a new project and not directly use the configured template")
|
||||
self.assertIn(self.project_template.name, so_line1.project_id.name, "The created project for so_line1 should use the configured template")
|
||||
|
||||
def test_billable_task_and_subtask(self):
|
||||
""" Test if subtasks and tasks are billed on the correct SO line """
|
||||
# create SO line and confirm it
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
<field name="tasks_count" widget="statinfo" string="Tasks"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='analytic_account_id']" position="after">
|
||||
<field name="visible_project" invisible="1"/>
|
||||
<field name="project_id" options="{'no_create': True}" attrs="{'invisible': [('visible_project', '=', False)]}"/>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -73,6 +73,7 @@ class ProjectCreateSalesOrder(models.TransientModel):
|
||||
|
||||
# create SO
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'project_id': self.project_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'analytic_account_id': self.project_id.analytic_account_id.id,
|
||||
'client_order_ref': self.project_id.name,
|
||||
|
||||
Reference in New Issue
Block a user