[FIX] sale_project,sale_timesheet: get SOLs linked to project
Before this commit, when we need to get all SOL linked to the project. That is the SOL linked to the project and also the SOL linked to all tasks of this project. Technically, a fetch is made to get the SOL of the project, a fetch to get all tasks of the project and then a fetch to get the SOL linked to all tasks fetched, finally the union is made to avoid having duplicate sale order items in the recordset (`self.sale_line_id | self.tasks.sale_line_id`). This way could be a performance issue if the project contained many active tasks and so the project update could take more and more time to compute the sold items shown in the right side panel of project update view. This commit does a query builder to fetch the SOLs linked to the project and its active tasks to avoid doing the prefetchs. part of task-2710808 closes odoo/odoo#85660 X-original-commit: 0734f4e Related: odoo/enterprise#24918 Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
@@ -5,6 +5,8 @@ from ast import literal_eval
|
||||
|
||||
from odoo import api, fields, models, _, _lt
|
||||
from odoo.exceptions import ValidationError, AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.osv.query import Query
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
@@ -74,12 +76,13 @@ class Project(models.Model):
|
||||
|
||||
def _get_all_sales_orders(self):
|
||||
self.ensure_one()
|
||||
return self.sale_order_id | self.task_ids.sale_order_id | self.milestone_ids.sale_line_id.order_id
|
||||
return self._fetch_sale_order_items({'project.task': ['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)]}).order_id
|
||||
|
||||
@api.depends('sale_order_id', 'task_ids.sale_order_id')
|
||||
def _compute_sale_order_count(self):
|
||||
sale_order_items_per_project_id = self._fetch_sale_order_items_per_project_id({'project.task': ['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)]})
|
||||
for project in self:
|
||||
project.sale_order_count = len(project._get_all_sales_orders())
|
||||
project.sale_order_count = len(sale_order_items_per_project_id.get(project.id, self.env['sale.order.line']).order_id)
|
||||
|
||||
def action_view_sos(self):
|
||||
self.ensure_one()
|
||||
@@ -129,6 +132,84 @@ class Project(models.Model):
|
||||
# Project Updates
|
||||
# ----------------------------
|
||||
|
||||
def _fetch_sale_order_items_per_project_id(self, domain_per_model=None):
|
||||
if not self:
|
||||
return {}
|
||||
if len(self) == 1:
|
||||
return {self.id: self._fetch_sale_order_items(domain_per_model)}
|
||||
query_str, params = self._get_sale_order_items_query(domain_per_model).select('id', 'ARRAY_AGG(DISTINCT sale_line_id) AS sale_line_ids')
|
||||
query = f"""
|
||||
{query_str}
|
||||
GROUP BY id
|
||||
"""
|
||||
self._cr.execute(query, params)
|
||||
return {row['id']: self.env['sale.order.line'].browse(row['sale_line_ids']) for row in self._cr.dictfetchall()}
|
||||
|
||||
def _fetch_sale_order_items(self, domain_per_model=None, limit=None, offset=None):
|
||||
return self.env['sale.order.line'].browse(self._fetch_sale_order_item_ids(domain_per_model, limit, offset))
|
||||
|
||||
def _fetch_sale_order_item_ids(self, domain_per_model=None, limit=None, offset=None):
|
||||
if not self or not self.filtered('allow_billable'):
|
||||
return []
|
||||
query = self._get_sale_order_items_query(domain_per_model)
|
||||
query.limit = limit
|
||||
query.offset = offset
|
||||
query_str, params = query.select('DISTINCT sale_line_id')
|
||||
self._cr.execute(query_str, params)
|
||||
return [row[0] for row in self._cr.fetchall()]
|
||||
|
||||
def _get_sale_orders(self):
|
||||
return self._get_sale_order_items().order_id
|
||||
|
||||
def _get_sale_order_items(self):
|
||||
return self._fetch_sale_order_items()
|
||||
|
||||
def _get_sale_order_items_query(self, domain_per_model=None):
|
||||
if domain_per_model is None:
|
||||
domain_per_model = {}
|
||||
billable_project = [('allow_billable', '=', True)]
|
||||
project_domain = [('id', 'in', self.ids), ('sale_line_id', '!=', False)]
|
||||
if 'project.project' in domain_per_model:
|
||||
project_domain = expression.AND([
|
||||
domain_per_model['project.project'],
|
||||
project_domain,
|
||||
billable_project,
|
||||
])
|
||||
project_query = self.env['project.project']._where_calc(project_domain)
|
||||
self._apply_ir_rules(project_query, 'read')
|
||||
project_query_str, project_params = project_query.select('id', 'sale_line_id')
|
||||
|
||||
Task = self.env['project.task']
|
||||
task_domain = [('project_id', 'in', self.ids), ('sale_line_id', '!=', False)]
|
||||
if Task._name in domain_per_model:
|
||||
task_domain = expression.AND([
|
||||
domain_per_model[Task._name],
|
||||
task_domain,
|
||||
billable_project,
|
||||
])
|
||||
task_query = Task._where_calc(task_domain)
|
||||
Task._apply_ir_rules(task_query, 'read')
|
||||
task_query_str, task_params = task_query.select(f'{Task._table}.project_id AS id', f'{Task._table}.sale_line_id')
|
||||
|
||||
ProjectMilestone = self.env['project.milestone']
|
||||
milestone_domain = [('project_id', 'in', self.ids), ('allow_billable', '=', True)]
|
||||
if ProjectMilestone._name in domain_per_model:
|
||||
milestone_domain = expression.AND([
|
||||
domain_per_model[ProjectMilestone._name],
|
||||
milestone_domain,
|
||||
billable_project,
|
||||
])
|
||||
milestone_query = ProjectMilestone._where_calc(milestone_domain)
|
||||
ProjectMilestone._apply_ir_rules(milestone_query)
|
||||
milestone_query_str, milestone_params = milestone_query.select(
|
||||
f'{ProjectMilestone._table}.project_id AS id',
|
||||
f'{ProjectMilestone._table}.sale_line_id',
|
||||
)
|
||||
|
||||
query = Query(self._cr, 'project_sale_order_item', ' UNION '.join([project_query_str, task_query_str, milestone_query_str]))
|
||||
query._where_params = project_params + task_params + milestone_params
|
||||
return query
|
||||
|
||||
def _get_stat_buttons(self):
|
||||
buttons = super(Project, self)._get_stat_buttons()
|
||||
if self.user_has_groups('sales_team.group_sale_salesman_all_leads'):
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestSaleProject(TransactionCase):
|
||||
cls.project_global = cls.env['project.project'].create({
|
||||
'name': 'Global Project',
|
||||
'analytic_account_id': cls.analytic_account_sale.id,
|
||||
'allow_billable': True,
|
||||
})
|
||||
cls.project_template = cls.env['project.project'].create({
|
||||
'name': 'Project TEMPLATE for services',
|
||||
@@ -82,10 +83,11 @@ class TestSaleProject(TransactionCase):
|
||||
})
|
||||
|
||||
def test_sale_order_with_project_task(self):
|
||||
SaleOrder = self.env['sale.order'].with_context(tracking_disable=True)
|
||||
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True)
|
||||
|
||||
partner = self.env['res.partner'].create({'name': "Mur en béton"})
|
||||
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
sale_order = SaleOrder.create({
|
||||
'partner_id': partner.id,
|
||||
'partner_invoice_id': partner.id,
|
||||
'partner_shipping_id': partner.id,
|
||||
@@ -127,3 +129,26 @@ class TestSaleProject(TransactionCase):
|
||||
# service_tracking 'project_only'
|
||||
self.assertFalse(so_line_order_only_project.task_id, "Task should not be created")
|
||||
self.assertTrue(so_line_order_only_project.project_id, "Sales order line should be linked to newly created project")
|
||||
|
||||
self.assertEqual(self.project_global._get_sale_order_items(), self.project_global.sale_line_id | self.project_global.tasks.sale_line_id, 'The _get_sale_order_items should returns all the SOLs linked to the project and its active tasks.')
|
||||
|
||||
sale_order_2 = SaleOrder.create({
|
||||
'partner_id': partner.id,
|
||||
'partner_invoice_id': partner.id,
|
||||
'partner_shipping_id': partner.id,
|
||||
})
|
||||
sale_line_1_order_2 = SaleOrderLine.create({
|
||||
'product_id': self.product_order_service1.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.product_order_service1.uom_id.id,
|
||||
'price_unit': self.product_order_service1.list_price,
|
||||
'order_id': sale_order_2.id,
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'sale_line_id': sale_line_1_order_2.id,
|
||||
'project_id': self.project_global.id,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id, sale_line_1_order_2)
|
||||
self.assertIn(task.sale_line_id, self.project_global._get_sale_order_items())
|
||||
self.assertEqual(self.project_global._get_sale_orders(), sale_order | sale_order_2)
|
||||
|
||||
@@ -174,11 +174,6 @@ class Project(models.Model):
|
||||
], limit=1)
|
||||
project.sale_line_id = sol or project.sale_line_employee_ids.sale_line_id[:1] # get the first SOL containing in the employee mappings if no sol found in the search
|
||||
|
||||
def _get_all_sales_orders(self):
|
||||
if self.allow_billable:
|
||||
return super()._get_all_sales_orders() | self.sale_line_employee_ids.sale_line_id.order_id
|
||||
return self.env['sale.order']
|
||||
|
||||
@api.depends('sale_line_employee_ids.sale_line_id', 'allow_billable')
|
||||
def _compute_sale_order_count(self):
|
||||
billable_projects = self.filtered('allow_billable')
|
||||
@@ -306,7 +301,7 @@ class Project(models.Model):
|
||||
}
|
||||
|
||||
def _get_sale_order_lines(self):
|
||||
sale_orders = self.sale_order_id | self.tasks.sale_order_id
|
||||
sale_orders = self._get_sale_orders()
|
||||
return self.env['sale.order.line'].search([('order_id', 'in', sale_orders.ids), ('is_service', '=', True), ('is_downpayment', '=', False)], order='id asc')
|
||||
|
||||
def _get_sold_items(self):
|
||||
@@ -342,6 +337,30 @@ class Project(models.Model):
|
||||
}
|
||||
return sold_items
|
||||
|
||||
def _get_sale_order_items_query(self, domain_per_model=None):
|
||||
if domain_per_model is None:
|
||||
domain_per_model = {}
|
||||
query = super()._get_sale_order_items_query(domain_per_model)
|
||||
EmployeeMapping = self.env['project.sale.line.employee.map']
|
||||
employee_mapping_domain = [('project_id', 'in', self.ids), ('project_id.allow_billable', '=', True), ('sale_line_id', '!=', False)]
|
||||
if EmployeeMapping._name in domain_per_model:
|
||||
employee_mapping_domain = expression.AND([
|
||||
domain_per_model[EmployeeMapping._name],
|
||||
employee_mapping_domain,
|
||||
])
|
||||
employee_mapping_query = EmployeeMapping._where_calc(employee_mapping_domain)
|
||||
EmployeeMapping._apply_ir_rules(employee_mapping_query, 'read')
|
||||
employee_mapping_query_str, employee_mapping_params = employee_mapping_query.select(
|
||||
f'{EmployeeMapping._table}.project_id AS id',
|
||||
f'{EmployeeMapping._table}.sale_line_id',
|
||||
)
|
||||
query._tables['project_sale_order_item'] = ' UNION '.join([
|
||||
query._tables['project_sale_order_item'],
|
||||
employee_mapping_query_str,
|
||||
])
|
||||
query._where_params += employee_mapping_params
|
||||
return query
|
||||
|
||||
def _get_profitability_items(self):
|
||||
if not self.user_has_groups('project.group_project_manager'):
|
||||
return {'data': []}
|
||||
|
||||
@@ -5,6 +5,7 @@ from . import common
|
||||
from . import common_reporting
|
||||
from . import test_sale_timesheet
|
||||
from . import test_sale_service
|
||||
from . import test_project
|
||||
from . import test_project_billing
|
||||
from . import test_reinvoice
|
||||
from . import test_reporting
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
|
||||
from .common import TestCommonSaleTimesheet
|
||||
|
||||
class TestProject(TestCommonSaleTimesheet):
|
||||
def test_fetch_sale_order_items(self):
|
||||
""" Test _fetch_sale_order_items and _get_sale_order_items methods
|
||||
This test will check we have the SOLs linked to the project and its tasks.
|
||||
Test Case:
|
||||
=========
|
||||
1) No SOLs and SO should be found on a non billable project
|
||||
2) Sol linked to the project should be fetched
|
||||
3) SOL linked to the project and its task should be fetched
|
||||
4) Add a employee mapping and check the SOL of this mapping is fetched with the others.
|
||||
5) remove the SOL linked to the project and check the SOL linked to the task is fetched
|
||||
6) Add an additional domain in the tasks to check if we can fetch with an additional filter
|
||||
for instance, only the SOLs linked to the folded tasks.
|
||||
7) Set allàw_billable=False and check no SOL is found since the project is not billable.
|
||||
"""
|
||||
self.assertFalse(self.project_non_billable._fetch_sale_order_items())
|
||||
self.assertFalse(self.project_non_billable._get_sale_order_items())
|
||||
self.assertFalse(self.project_non_billable._get_sale_orders())
|
||||
|
||||
sale_item = self.so.order_line[0]
|
||||
self.project_global.sale_line_id = sale_item
|
||||
self.project_global.write({
|
||||
'sale_line_id': sale_item.id,
|
||||
})
|
||||
self.project_global.invalidate_cache()
|
||||
expected_task_sale_order_items = self.project_global.tasks.sale_line_id
|
||||
expected_sale_order_items = sale_item | expected_task_sale_order_items
|
||||
self.assertEqual(self.project_global._fetch_sale_order_items(), expected_sale_order_items)
|
||||
self.assertEqual(self.project_global._get_sale_order_items(), expected_sale_order_items)
|
||||
self.assertEqual(self.project_global._get_sale_orders(), self.so)
|
||||
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task with SOL',
|
||||
'project_id': self.project_global.id,
|
||||
'sale_line_id': self.so.order_line[1].id,
|
||||
})
|
||||
|
||||
self.assertEqual(task.project_id, self.project_global)
|
||||
self.assertEqual(task.sale_line_id, self.so.order_line[1])
|
||||
self.assertEqual(task.sale_order_id, self.so)
|
||||
sale_lines = self.project_global._get_sale_order_items()
|
||||
self.assertEqual(sale_lines, task.sale_line_id + self.project_global.sale_line_id, 'The Sales Order Items found should be the one linked to the project and the one of project task.')
|
||||
self.assertEqual(self.project_global._get_sale_orders(), self.so, 'The Sales Order fetched should be the one of the both sale_lines fetched.')
|
||||
|
||||
employee_mapping = self.env['project.sale.line.employee.map'].create({
|
||||
'project_id': self.project_global.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'sale_line_id': self.so.order_line[-1].id,
|
||||
})
|
||||
expected_sale_order_items |= employee_mapping.sale_line_id
|
||||
self.assertEqual(self.project_global._get_sale_order_items(), expected_sale_order_items)
|
||||
self.assertEqual(self.project_global._get_sale_orders(), expected_sale_order_items.order_id)
|
||||
|
||||
self.project_global.write({
|
||||
'sale_line_id': False,
|
||||
})
|
||||
self.project_global.invalidate_cache()
|
||||
expected_task_sale_order_items |= task.sale_line_id
|
||||
self.assertEqual(self.project_global._get_sale_order_items(), expected_task_sale_order_items | employee_mapping.sale_line_id)
|
||||
self.assertEqual(self.project_global._get_sale_orders(), self.so)
|
||||
|
||||
new_stage = self.env['project.task.type'].create({
|
||||
'name': 'New',
|
||||
'sequence': 1,
|
||||
'project_ids': [Command.set(self.project_global.ids)],
|
||||
})
|
||||
done_stage = self.env['project.task.type'].create({
|
||||
'name': 'Done',
|
||||
'sequence': 2,
|
||||
'project_ids': [Command.set(self.project_global.ids)],
|
||||
'fold': True,
|
||||
})
|
||||
task.write({
|
||||
'stage_id': done_stage.id,
|
||||
})
|
||||
self.env['project.task.type'].flush()
|
||||
self.assertEqual(self.project_global._fetch_sale_order_items({'project.task': [('stage_id.fold', '=', False)]}), employee_mapping.sale_line_id)
|
||||
self.assertEqual(self.project_global._fetch_sale_order_items({'project.task': [('stage_id.fold', '=', True)]}), task.sale_line_id | employee_mapping.sale_line_id)
|
||||
|
||||
task2 = self.env['project.task'].create({
|
||||
'name': 'Task 2',
|
||||
'project_id': self.project_global.id,
|
||||
'sale_line_id': sale_item.id,
|
||||
'stage_id': new_stage.id,
|
||||
})
|
||||
|
||||
self.assertEqual(self.project_global._fetch_sale_order_items({'project.task': [('stage_id.fold', '=', False)]}), task2.sale_line_id | employee_mapping.sale_line_id)
|
||||
self.assertEqual(self.project_global._fetch_sale_order_items({'project.task': [('stage_id.fold', '=', True)]}), task.sale_line_id | employee_mapping.sale_line_id)
|
||||
|
||||
self.project_global.allow_billable = False
|
||||
self.assertFalse(self.project_global._get_sale_order_items())
|
||||
self.assertFalse(self.project_global._get_sale_orders())
|
||||
Reference in New Issue
Block a user