From 68f074cb598a6dd5ea01c8ee9fdc6482bb227f9f Mon Sep 17 00:00:00 2001 From: mileo Date: Wed, 29 Jul 2026 00:48:32 -0300 Subject: [PATCH] [ADD] base_field_trigger_warmup: build compute dependency trees at boot The ORM resolves the transitive closure of compute triggers lazily, the first time a field is written. On models with many interdependent stored computed fields the first request served by each worker pays for the whole closure, and every restart brings the penalty back. This module builds those trees while the registry loads, where nobody is waiting. Behaviour is unchanged: it only decides when work the ORM would do anyway is done. The scope defaults to every model and can be narrowed with the base_field_trigger_warmup.models system parameter; ODOO_FIELD_TRIGGER_WARMUP=0 disables it without uninstalling. Registry.get_field_trigger_tree is private ORM API, so it is called defensively: a version that renames or drops it degrades to a no-op instead of breaking the registry load. Extracted from a production deployment whose invoice line model carries around 180 interdependent stored fields, where the first write on a fresh worker cost about five times what every following write cost. --- base_field_trigger_warmup/README.rst | 147 ++++++ base_field_trigger_warmup/__init__.py | 1 + base_field_trigger_warmup/__manifest__.py | 15 + base_field_trigger_warmup/models/__init__.py | 1 + .../models/base_field_trigger_warmup.py | 120 +++++ .../readme/CONFIGURE.rst | 20 + .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 15 + base_field_trigger_warmup/readme/ROADMAP.rst | 8 + base_field_trigger_warmup/readme/USAGE.rst | 11 + .../static/description/index.html | 491 ++++++++++++++++++ base_field_trigger_warmup/tests/__init__.py | 1 + .../tests/test_field_trigger_warmup.py | 90 ++++ .../odoo/addons/base_field_trigger_warmup | 1 + setup/base_field_trigger_warmup/setup.py | 6 + 15 files changed, 930 insertions(+) create mode 100644 base_field_trigger_warmup/README.rst create mode 100644 base_field_trigger_warmup/__init__.py create mode 100644 base_field_trigger_warmup/__manifest__.py create mode 100644 base_field_trigger_warmup/models/__init__.py create mode 100644 base_field_trigger_warmup/models/base_field_trigger_warmup.py create mode 100644 base_field_trigger_warmup/readme/CONFIGURE.rst create mode 100644 base_field_trigger_warmup/readme/CONTRIBUTORS.rst create mode 100644 base_field_trigger_warmup/readme/DESCRIPTION.rst create mode 100644 base_field_trigger_warmup/readme/ROADMAP.rst create mode 100644 base_field_trigger_warmup/readme/USAGE.rst create mode 100644 base_field_trigger_warmup/static/description/index.html create mode 100644 base_field_trigger_warmup/tests/__init__.py create mode 100644 base_field_trigger_warmup/tests/test_field_trigger_warmup.py create mode 120000 setup/base_field_trigger_warmup/odoo/addons/base_field_trigger_warmup create mode 100644 setup/base_field_trigger_warmup/setup.py diff --git a/base_field_trigger_warmup/README.rst b/base_field_trigger_warmup/README.rst new file mode 100644 index 00000000000..811936c14a3 --- /dev/null +++ b/base_field_trigger_warmup/README.rst @@ -0,0 +1,147 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========================= +Field Trigger Tree Warmup +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c32021bf286316f53f09e2a7ef5dce116ebfce5f52d8584ad1d093a4ca6a9b76 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/16.0/base_field_trigger_warmup + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-base_field_trigger_warmup + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +The ORM resolves the transitive closure of compute triggers lazily: the first +time a field is written, Odoo walks its ``@api.depends`` graph and caches the +resulting trigger tree in the registry. On models with many interdependent +stored computed fields, that first write pays for the whole closure, so the +first request served by each worker is noticeably slower than the ones that +follow, and every restart brings the penalty back. + +This module builds those trees while the registry loads, where no user is +waiting. It has no user interface and no effect on behaviour: it only decides +*when* work the ORM would do anyway is done. + +It was extracted from a production deployment whose invoice line model carries +around 180 interdependent stored fields: the first write on a fresh worker cost +about five times what every following write cost. Warming up the trees at boot +removed the difference. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +By default every model in the registry is warmed up. On a large database that +costs a few seconds of boot time per worker, which is usually a good trade, +but it can be narrowed down. + +To warm up only the models that matter, set the system parameter +``base_field_trigger_warmup.models`` to a comma separated list of model names:: + + base_field_trigger_warmup.models = account.move,account.move.line + +Model names that do not exist in the registry are ignored with a warning. +Setting the parameter to ``*`` or leaving it empty restores the default of +warming up everything. + +To disable the warmup entirely without uninstalling the module, for instance on +a development machine or in a CI pipeline where boot time matters more than the +first request, export:: + + ODOO_FIELD_TRIGGER_WARMUP=0 + +The warmup is always skipped while tests are running. + +Usage +===== + +There is nothing to do: installing the module is enough. + +At the end of each registry load the module logs, at INFO level, how many +trigger trees it built and how long it took:: + + INFO odoo.addons.base_field_trigger_warmup: Warmed up 24868 field + trigger trees in 6.52s + +Use that line to decide whether the default scope is worth its boot cost, and +narrow it down with the system parameter described in the configuration section +if it is not. + +Known issues / Roadmap +====================== + +The module relies on ``Registry.get_field_trigger_tree``, which is private ORM +API. It is called defensively: if a future Odoo version renames or removes it, +the module logs the fact and does nothing instead of breaking the registry +load. Porting to a new version should start by checking that method. + +Warming up the trees hides the symptom of a wide compute graph. When the first +request of a worker is slow enough to need this module, it is also worth +looking at whether the graph itself can be reduced. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* KMEE + +Contributors +~~~~~~~~~~~~ + +* `KMEE `_: + + * Luis Felipe Mileo + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_field_trigger_warmup/__init__.py b/base_field_trigger_warmup/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/base_field_trigger_warmup/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_field_trigger_warmup/__manifest__.py b/base_field_trigger_warmup/__manifest__.py new file mode 100644 index 00000000000..2de4361d6ec --- /dev/null +++ b/base_field_trigger_warmup/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2026 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Field Trigger Tree Warmup", + "summary": "Build the compute dependency trees at boot instead of on the " + "first request", + "version": "16.0.1.0.0", + "development_status": "Beta", + "category": "Tools", + "author": "KMEE, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", + "license": "AGPL-3", + "depends": ["base"], + "installable": True, +} diff --git a/base_field_trigger_warmup/models/__init__.py b/base_field_trigger_warmup/models/__init__.py new file mode 100644 index 00000000000..f582159f032 --- /dev/null +++ b/base_field_trigger_warmup/models/__init__.py @@ -0,0 +1 @@ +from . import base_field_trigger_warmup diff --git a/base_field_trigger_warmup/models/base_field_trigger_warmup.py b/base_field_trigger_warmup/models/base_field_trigger_warmup.py new file mode 100644 index 00000000000..f6d499d467b --- /dev/null +++ b/base_field_trigger_warmup/models/base_field_trigger_warmup.py @@ -0,0 +1,120 @@ +# Copyright 2026 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import os +import time + +from odoo import api, models + +_logger = logging.getLogger(__name__) + +PARAM_MODELS = "base_field_trigger_warmup.models" +ENV_DISABLE = "ODOO_FIELD_TRIGGER_WARMUP" + + +class BaseFieldTriggerWarmup(models.AbstractModel): + """Build the ORM compute dependency trees while the registry loads. + + The ORM resolves the transitive closure of compute triggers lazily, the + first time a field is written. On models with many interdependent stored + computed fields, that first write pays for the whole closure, so the first + request served by each worker is much slower than the following ones. This + model moves that cost to the registry load, where no user is waiting. + """ + + _name = "base.field.trigger.warmup" + _description = "Field Trigger Tree Warmup" + + @api.model + def _warmup_is_enabled(self): + """Warmup is skipped in tests and when the env var is set to 0.""" + if os.environ.get(ENV_DISABLE, "1") == "0": + return False + return not self.env.registry.in_test_mode() + + @api.model + def _warmup_model_names(self): + """Model names to warm up. + + Read from the ``base_field_trigger_warmup.models`` system parameter: a + comma separated list of model names, or ``*`` (the default) for every + model in the registry. Narrowing it down is useful when only a few + models are expensive and boot time matters. + """ + param = ( + self.env["ir.config_parameter"].sudo().get_param(PARAM_MODELS, default="*") + or "" + ).strip() + if param in ("", "*"): + return list(self.env.registry) + wanted = [name.strip() for name in param.split(",") if name.strip()] + known, unknown = [], [] + for name in wanted: + (known if name in self.env.registry else unknown).append(name) + if unknown: + _logger.warning( + "%s lists unknown models, ignored: %s", + PARAM_MODELS, + ", ".join(unknown), + ) + return known + + @api.model + def _warmup_field_trigger_trees(self, model_names=None): + """Build the trigger tree of every field of ``model_names``. + + Returns the number of fields whose tree was built. Failures on a single + field are logged at debug level and skipped: a warmup must never be able + to break the boot. + """ + registry = self.env.registry + # Private ORM API: guard it so an Odoo version that renames or drops it + # degrades to a no-op instead of breaking the registry load. + build_tree = getattr(registry, "get_field_trigger_tree", None) + if build_tree is None: + _logger.info( + "This Odoo build has no Registry.get_field_trigger_tree, " + "nothing to warm up" + ) + return 0 + if model_names is None: + model_names = self._warmup_model_names() + count = 0 + for model_name in model_names: + model = self.env.get(model_name) + if model is None: + continue + for field in model._fields.values(): + try: + build_tree(field) + except Exception: # pylint: disable=except-pass + _logger.debug( + "Could not build the trigger tree of %s.%s", + model_name, + field.name, + exc_info=True, + ) + continue + count += 1 + return count + + def _register_hook(self): + res = super()._register_hook() + registry = self.env.registry + # _register_hook runs once per model that defines it, and the registry + # may be loaded more than once per process. + if getattr(registry, "_field_trigger_warmup_done", False): + return res + registry._field_trigger_warmup_done = True + if not self._warmup_is_enabled(): + return res + start = time.time() + count = self._warmup_field_trigger_trees() + if count: + _logger.info( + "Warmed up %s field trigger trees in %.2fs", + count, + time.time() - start, + ) + return res diff --git a/base_field_trigger_warmup/readme/CONFIGURE.rst b/base_field_trigger_warmup/readme/CONFIGURE.rst new file mode 100644 index 00000000000..ce168823e88 --- /dev/null +++ b/base_field_trigger_warmup/readme/CONFIGURE.rst @@ -0,0 +1,20 @@ +By default every model in the registry is warmed up. On a large database that +costs a few seconds of boot time per worker, which is usually a good trade, +but it can be narrowed down. + +To warm up only the models that matter, set the system parameter +``base_field_trigger_warmup.models`` to a comma separated list of model names:: + + base_field_trigger_warmup.models = account.move,account.move.line + +Model names that do not exist in the registry are ignored with a warning. +Setting the parameter to ``*`` or leaving it empty restores the default of +warming up everything. + +To disable the warmup entirely without uninstalling the module, for instance on +a development machine or in a CI pipeline where boot time matters more than the +first request, export:: + + ODOO_FIELD_TRIGGER_WARMUP=0 + +The warmup is always skipped while tests are running. diff --git a/base_field_trigger_warmup/readme/CONTRIBUTORS.rst b/base_field_trigger_warmup/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..66b6742956e --- /dev/null +++ b/base_field_trigger_warmup/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `KMEE `_: + + * Luis Felipe Mileo diff --git a/base_field_trigger_warmup/readme/DESCRIPTION.rst b/base_field_trigger_warmup/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..f848e492140 --- /dev/null +++ b/base_field_trigger_warmup/readme/DESCRIPTION.rst @@ -0,0 +1,15 @@ +The ORM resolves the transitive closure of compute triggers lazily: the first +time a field is written, Odoo walks its ``@api.depends`` graph and caches the +resulting trigger tree in the registry. On models with many interdependent +stored computed fields, that first write pays for the whole closure, so the +first request served by each worker is noticeably slower than the ones that +follow, and every restart brings the penalty back. + +This module builds those trees while the registry loads, where no user is +waiting. It has no user interface and no effect on behaviour: it only decides +*when* work the ORM would do anyway is done. + +It was extracted from a production deployment whose invoice line model carries +around 180 interdependent stored fields: the first write on a fresh worker cost +about five times what every following write cost. Warming up the trees at boot +removed the difference. diff --git a/base_field_trigger_warmup/readme/ROADMAP.rst b/base_field_trigger_warmup/readme/ROADMAP.rst new file mode 100644 index 00000000000..d181f386d51 --- /dev/null +++ b/base_field_trigger_warmup/readme/ROADMAP.rst @@ -0,0 +1,8 @@ +The module relies on ``Registry.get_field_trigger_tree``, which is private ORM +API. It is called defensively: if a future Odoo version renames or removes it, +the module logs the fact and does nothing instead of breaking the registry +load. Porting to a new version should start by checking that method. + +Warming up the trees hides the symptom of a wide compute graph. When the first +request of a worker is slow enough to need this module, it is also worth +looking at whether the graph itself can be reduced. diff --git a/base_field_trigger_warmup/readme/USAGE.rst b/base_field_trigger_warmup/readme/USAGE.rst new file mode 100644 index 00000000000..8504d60f6e9 --- /dev/null +++ b/base_field_trigger_warmup/readme/USAGE.rst @@ -0,0 +1,11 @@ +There is nothing to do: installing the module is enough. + +At the end of each registry load the module logs, at INFO level, how many +trigger trees it built and how long it took:: + + INFO odoo.addons.base_field_trigger_warmup: Warmed up 24868 field + trigger trees in 6.52s + +Use that line to decide whether the default scope is worth its boot cost, and +narrow it down with the system parameter described in the configuration section +if it is not. diff --git a/base_field_trigger_warmup/static/description/index.html b/base_field_trigger_warmup/static/description/index.html new file mode 100644 index 00000000000..b0771cdf052 --- /dev/null +++ b/base_field_trigger_warmup/static/description/index.html @@ -0,0 +1,491 @@ + + + + + +Field Trigger Tree Warmup + + + +
+ + + +Odoo Community Association + +
+

Field Trigger Tree Warmup

+ +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

The ORM resolves the transitive closure of compute triggers lazily: the first +time a field is written, Odoo walks its @api.depends graph and caches the +resulting trigger tree in the registry. On models with many interdependent +stored computed fields, that first write pays for the whole closure, so the +first request served by each worker is noticeably slower than the ones that +follow, and every restart brings the penalty back.

+

This module builds those trees while the registry loads, where no user is +waiting. It has no user interface and no effect on behaviour: it only decides +when work the ORM would do anyway is done.

+

It was extracted from a production deployment whose invoice line model carries +around 180 interdependent stored fields: the first write on a fresh worker cost +about five times what every following write cost. Warming up the trees at boot +removed the difference.

+

Table of contents

+ +
+

Configuration

+

By default every model in the registry is warmed up. On a large database that +costs a few seconds of boot time per worker, which is usually a good trade, +but it can be narrowed down.

+

To warm up only the models that matter, set the system parameter +base_field_trigger_warmup.models to a comma separated list of model names:

+
+base_field_trigger_warmup.models = account.move,account.move.line
+
+

Model names that do not exist in the registry are ignored with a warning. +Setting the parameter to * or leaving it empty restores the default of +warming up everything.

+

To disable the warmup entirely without uninstalling the module, for instance on +a development machine or in a CI pipeline where boot time matters more than the +first request, export:

+
+ODOO_FIELD_TRIGGER_WARMUP=0
+
+

The warmup is always skipped while tests are running.

+
+
+

Usage

+

There is nothing to do: installing the module is enough.

+

At the end of each registry load the module logs, at INFO level, how many +trigger trees it built and how long it took:

+
+INFO odoo.addons.base_field_trigger_warmup: Warmed up 24868 field
+trigger trees in 6.52s
+
+

Use that line to decide whether the default scope is worth its boot cost, and +narrow it down with the system parameter described in the configuration section +if it is not.

+
+
+

Known issues / Roadmap

+

The module relies on Registry.get_field_trigger_tree, which is private ORM +API. It is called defensively: if a future Odoo version renames or removes it, +the module logs the fact and does nothing instead of breaking the registry +load. Porting to a new version should start by checking that method.

+

Warming up the trees hides the symptom of a wide compute graph. When the first +request of a worker is slow enough to need this module, it is also worth +looking at whether the graph itself can be reduced.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • KMEE
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/base_field_trigger_warmup/tests/__init__.py b/base_field_trigger_warmup/tests/__init__.py new file mode 100644 index 00000000000..c04bbb0788f --- /dev/null +++ b/base_field_trigger_warmup/tests/__init__.py @@ -0,0 +1 @@ +from . import test_field_trigger_warmup diff --git a/base_field_trigger_warmup/tests/test_field_trigger_warmup.py b/base_field_trigger_warmup/tests/test_field_trigger_warmup.py new file mode 100644 index 00000000000..07f73e2dff6 --- /dev/null +++ b/base_field_trigger_warmup/tests/test_field_trigger_warmup.py @@ -0,0 +1,90 @@ +# Copyright 2026 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import os +from unittest.mock import patch + +from odoo.tests.common import TransactionCase + +from odoo.addons.base_field_trigger_warmup.models.base_field_trigger_warmup import ( + ENV_DISABLE, +) + + +class TestFieldTriggerWarmup(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.warmup = cls.env["base.field.trigger.warmup"] + cls.param = cls.env["ir.config_parameter"].sudo() + + def test_warms_up_the_requested_models(self): + """Every field of the given models has its trigger tree built.""" + built = [] + with patch.object( + type(self.env.registry), + "get_field_trigger_tree", + side_effect=lambda field: built.append(field), + ): + count = self.warmup._warmup_field_trigger_trees(["res.partner"]) + self.assertEqual(count, len(self.env["res.partner"]._fields)) + self.assertEqual(count, len(built)) + + def test_a_failing_field_does_not_break_the_warmup(self): + """A field whose tree cannot be built is skipped, not raised.""" + partner_fields = list(self.env["res.partner"]._fields.values()) + broken = partner_fields[0] + + def build(field): + if field is broken: + raise ValueError("cannot build this one") + + with patch.object( + type(self.env.registry), "get_field_trigger_tree", side_effect=build + ): + count = self.warmup._warmup_field_trigger_trees(["res.partner"]) + self.assertEqual(count, len(partner_fields) - 1) + + def test_missing_orm_api_is_a_no_op(self): + """An Odoo build without the private method must not break.""" + registry_type = type(self.env.registry) + with patch.object(registry_type, "get_field_trigger_tree", None, create=True): + self.assertEqual( + self.warmup._warmup_field_trigger_trees(["res.partner"]), 0 + ) + + def test_scope_defaults_to_every_model(self): + self.param.set_param("base_field_trigger_warmup.models", "*") + self.assertEqual( + sorted(self.warmup._warmup_model_names()), + sorted(self.env.registry), + ) + + def test_scope_can_be_narrowed(self): + self.param.set_param( + "base_field_trigger_warmup.models", "res.partner, res.users" + ) + self.assertEqual( + sorted(self.warmup._warmup_model_names()), + ["res.partner", "res.users"], + ) + + def test_unknown_models_are_ignored(self): + self.param.set_param( + "base_field_trigger_warmup.models", "res.partner,no.such.model" + ) + self.assertEqual(self.warmup._warmup_model_names(), ["res.partner"]) + + def test_enabled_by_default(self): + with patch.object(type(self.env.registry), "in_test_mode", return_value=False): + self.assertTrue(self.warmup._warmup_is_enabled()) + + def test_disabled_in_test_mode(self): + with patch.object(type(self.env.registry), "in_test_mode", return_value=True): + self.assertFalse(self.warmup._warmup_is_enabled()) + + def test_disabled_by_environment_variable(self): + with patch.dict(os.environ, {ENV_DISABLE: "0"}), patch.object( + type(self.env.registry), "in_test_mode", return_value=False + ): + self.assertFalse(self.warmup._warmup_is_enabled()) diff --git a/setup/base_field_trigger_warmup/odoo/addons/base_field_trigger_warmup b/setup/base_field_trigger_warmup/odoo/addons/base_field_trigger_warmup new file mode 120000 index 00000000000..64ef49fda41 --- /dev/null +++ b/setup/base_field_trigger_warmup/odoo/addons/base_field_trigger_warmup @@ -0,0 +1 @@ +../../../../base_field_trigger_warmup \ No newline at end of file diff --git a/setup/base_field_trigger_warmup/setup.py b/setup/base_field_trigger_warmup/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/base_field_trigger_warmup/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)