From a772936bad4908d485ef205f48bb0d45e9b4e506 Mon Sep 17 00:00:00 2001 From: "Victor Piryns (pivi)" Date: Fri, 22 Sep 2023 08:26:10 +0000 Subject: [PATCH] [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) --- addons/sale_project/i18n/sale_project.pot | 7 ++++ addons/sale_project/models/sale_order.py | 46 ++++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/addons/sale_project/i18n/sale_project.pot b/addons/sale_project/i18n/sale_project.pot index bd542dded18..a8629329deb 100644 --- a/addons/sale_project/i18n/sale_project.pot +++ b/addons/sale_project/i18n/sale_project.pot @@ -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 diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index 0d84bc07c99..997f8aa80c2 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -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):