[PERF] sale_project: speed up reads on tasks_ids

Issue:
Reading on the field `tasks_ids` of the `sale.order` model in some
context can be slow, leading to really slow creation and duplication
of tasks on a medium sized database.

Analysis:
After creation of a task, when the ORM flushes the model, it
recomputes all the inverse dependencies that needs to be recomputed.
Currently `task_ids` doesn't implement a `search` routine on the
field, so `task_ids` needs to be recomputed for *all* sales orders,
since we don't know which sales order needs computation. So for
*all* sales orders, we trigger the compute `_compute_tasks_ids`,
which is badly implemented using the general performance anti-pattern
```py
for record in records:
    field = self.env['model'].search(domain)
```
which makes 1 query per record that we are computing.

Solution:
Correct the implementation of `_compute_tasks_ids`, which now uses a
`_read_group` instead so we do 1 query for the whole recordset.
And implement the `search` on the non-stored `tasks_ids` field,
which speeds up the context of re-computations. This allows the ORM
to know for which sales order the field `task_ids` needs re-computation.

Results:
In the context a mid-size database (less than 10k records for the
concerned models)

Task creation/duplication:

|             | Before | After |
|-------------|--------|-------|
| Time        | 1 min  | 1 sec |
| Query count | 40k    | 300   |

Reference:
opw-3445565

closes odoo/odoo#138007

X-original-commit: 81ae499ce975a96c12b6e379581ba40167d025db
Related: odoo/enterprise#48610
Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
Victor Piryns (pivi)
2023-10-10 01:59:41 +00:00
parent 5f454f0ccb
commit a772936bad
2 changed files with 48 additions and 5 deletions
@@ -379,6 +379,13 @@ msgid ""
" '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."
msgstr ""
#. module: sale_project
#. odoo-python
#: code:addons/sale_project/models/sale_order.py:0
#, python-format
msgid "Operation not supported"
msgstr ""
#. module: sale_project
#. odoo-python
#: code:addons/sale_project/models/project.py:0
+41 -5
View File
@@ -4,7 +4,7 @@
import ast
from collections import defaultdict
from odoo import api, fields, models, _
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
from odoo.osv.expression import AND
@@ -13,7 +13,7 @@ from odoo.osv.expression import AND
class SaleOrder(models.Model):
_inherit = 'sale.order'
tasks_ids = fields.Many2many('project.task', compute='_compute_tasks_ids', string='Tasks associated to this sale')
tasks_ids = fields.Many2many('project.task', compute='_compute_tasks_ids', search='_search_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)
@@ -53,11 +53,47 @@ class SaleOrder(models.Model):
order.show_task_button = order.show_project_button or order.tasks_count
order.show_create_project_button = is_project_manager and order.id in show_button_ids and not order.project_count and order.order_line.product_template_id.filtered(lambda x: x.service_policy in ['delivered_timesheet', 'delivered_milestones'])
def _search_tasks_ids(self, operator, value):
is_name_search = operator in ['=', '!=', 'like', '=like', 'ilike', '=ilike'] and isinstance(value, str)
is_id_eq_search = operator in ['=', '!='] and isinstance(value, int)
is_id_in_search = operator in ['in', 'not in'] and isinstance(value, list) and all(isinstance(item, int) for item in value)
if not (is_name_search or is_id_eq_search or is_id_in_search):
raise NotImplementedError(_('Operation not supported'))
if is_name_search:
tasks_ids = self.env['project.task']._name_search(value, operator=operator, limit=None)
elif is_id_eq_search:
tasks_ids = value if operator == '=' else self.env['project.task']._search([('id', '!=', value)], order='id')
else: # is_id_in_search
tasks_ids = self.env['project.task']._search([('id', operator, value)], order='id')
tasks = self.env['project.task'].browse(tasks_ids)
return [('id', 'in', tasks.sale_order_id.ids)]
@api.depends('order_line.product_id.project_id')
def _compute_tasks_ids(self):
for order in self:
order.tasks_ids = self.env['project.task'].search(['&', ('project_id', '!=', False), '|', ('sale_line_id', 'in', order.order_line.ids), ('sale_order_id', '=', order.id)])
order.tasks_count = len(order.tasks_ids)
tasks_per_so = self.env['project.task']._read_group(
domain=['&', ('project_id', '!=', False), '|', ('sale_line_id', 'in', self.order_line.ids), ('sale_order_id', 'in', self.ids)],
groupby=['sale_order_id'],
aggregates=['id:recordset', '__count']
)
so_with_tasks = self.env['sale.order']
for order, tasks_ids, tasks_count in tasks_per_so:
if order:
order.tasks_ids = tasks_ids
order.tasks_count = tasks_count
so_with_tasks += order
else:
# tasks that have no sale_order_id need to be associated with the SO from their sale_line_id
for task in tasks_ids:
task_so = task.sale_line_id.order_id
task_so.tasks_ids = [Command.link(task.id)]
task_so.tasks_count += 1
so_with_tasks += task_so
remaining_orders = self - so_with_tasks
if remaining_orders:
remaining_orders.tasks_ids = [Command.clear()]
remaining_orders.tasks_count = 0
@api.depends('order_line.product_id.service_tracking')
def _compute_visible_project(self):