From b3f5bf3bfccc31d3605fc5bc5e4770ed18592b3a Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 6 Jan 2025 16:38:10 +0100 Subject: [PATCH] [IMP] compute_field_after_install: Group computes by compute function --- .../models/recompute_field.py | 46 +++++++++------ .../tests/test_recompute.py | 59 ++++++++++++++++++- 2 files changed, 86 insertions(+), 19 deletions(-) diff --git a/compute_field_after_install/models/recompute_field.py b/compute_field_after_install/models/recompute_field.py index 145c9ba3f5c..00b36c712b6 100644 --- a/compute_field_after_install/models/recompute_field.py +++ b/compute_field_after_install/models/recompute_field.py @@ -4,12 +4,13 @@ import logging +from functools import reduce +from itertools import groupby from odoo import api, fields, models from odoo.exceptions import Warning as UserError -from odoo.tools.translate import _ - from odoo.tools import config +from odoo.tools.translate import _ _logger = logging.getLogger(__name__) @@ -62,32 +63,41 @@ def _run_all(self): return self.search([("state", "=", "todo")]).run() def run(self): - for task in self: - cursor = self.env.cr - model = self.env[task.model] + # Group tasks by compute method to avoid computing multifields computes multiple times + model_tasks = groupby( + self, + lambda task: (task.model, self.env[task.model]._fields[task.field].compute), + ) + for (model, _compute_fun), tasks in model_tasks: + tasks = reduce(lambda x, y: x | y, tasks) + fields = tasks.mapped("field") + last_id = max(tasks.mapped("last_id"), default=None) + step = min(tasks.mapped("step")) while True: _logger.info( - "Recompute field %s for model %s in background. Last id %d", - task.field, - task.model, - task.last_id, + "Recompute fields %s for model %s in background. Last id %d", + fields, + model, + last_id, ) - records = model.search( - [("id", "<", task.last_id)] if task.last_id else [], - limit=task.step, + records = self.env[model].search( + [("id", "<", last_id)] if last_id else [], + limit=step, order="id desc", ) if not records: - task.state = "done" - cursor.commit() + tasks.state = "done" + self.env.cr.commit() # pylint: disable=E8102 break + for field in fields: + field_ = records._fields[field] + self.env.add_to_compute(field_, records) - field = records._fields[task.field] - self.env.add_to_compute(field, records) records.recompute() - task.last_id = records[-1].id - cursor.commit() + last_id = records[-1].id + tasks.last_id = last_id + self.env.cr.commit() # pylint: disable=E8102 return True diff --git a/compute_field_after_install/tests/test_recompute.py b/compute_field_after_install/tests/test_recompute.py index 3eb63d3860d..fdadcc13925 100644 --- a/compute_field_after_install/tests/test_recompute.py +++ b/compute_field_after_install/tests/test_recompute.py @@ -2,8 +2,8 @@ # @author Sébastien BEAU # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from unittest.mock import Mock, patch from contextlib import contextmanager +from unittest.mock import Mock, patch from odoo.tests import TransactionCase from odoo.tools import config @@ -56,6 +56,7 @@ def test_add_fields(self): # Purge field commercial_company_name to simulate # the installation of a new field self.env.cr.execute("UPDATE res_partner SET commercial_company_name=null") + self.env.invalidate_all() partner = self.env.ref("base.res_partner_address_7") self.assertFalse(partner.commercial_company_name) @@ -98,6 +99,7 @@ def test_add_fields_batch(self): # Purge field commercial_company_name to simulate # the installation of a new field self.env.cr.execute("UPDATE res_partner SET commercial_company_name=null") + self.env.invalidate_all() partner = self.env.ref("base.res_partner_address_7") self.assertFalse(partner.commercial_company_name) @@ -187,3 +189,58 @@ def test_default_step(self): ) self.assertEqual(recompute_field.step, 4000) + def test_multifields(self): + if "sale.order" not in self.env: + self.skipTest("This test requires the sale module to be installed") + + records = self.env["sale.order"].search([]) + original_id_amounts = { + record.id: (record.amount_untaxed, record.amount_tax, record.amount_total) + for record in records + } + + with patch.dict( + config.options, {"computed_fields_defer_threshold": 1}, clear=True + ): + for field in ("amount_untaxed", "amount_tax", "amount_total"): + self.env(context={"module": "fake_module"}).add_to_compute( + records._fields[field], records + ) + + # Check that jobs have been created + recompute_fields = self.env["recompute.field"].search( + [ + ("model", "=", "sale.order"), + ] + ) + self.assertEqual(len(recompute_fields), 3) + self.assertEqual(recompute_fields.mapped("state"), ["todo", "todo", "todo"]) + + # Purge field commercial_company_name to simulate + # the installation of a new field + self.env.cr.execute( + "UPDATE sale_order SET amount_untaxed=null, amount_tax=null, amount_total=null" + ) + self.env.invalidate_all() + + for record in records: + self.assertFalse(record.amount_untaxed) + self.assertFalse(record.amount_tax) + self.assertFalse(record.amount_total) + + # Run the cron to process computed field + + with self._count_computes( + self.env["sale.order"], + "_compute_amounts", + ) as computed: + self.env["recompute.field"]._run_all() + self.assertEqual(computed["records"], len(records)) + self.assertEqual(computed["calls"], 1) + + self.assertEqual(recompute_fields.mapped("state"), ["done", "done", "done"]) + + for record in records: + self.assertEqual(record.amount_untaxed, original_id_amounts[record.id][0]) + self.assertEqual(record.amount_tax, original_id_amounts[record.id][1]) + self.assertEqual(record.amount_total, original_id_amounts[record.id][2])