diff --git a/component/README.rst b/component/README.rst
new file mode 100644
index 000000000..b2bce6de8
--- /dev/null
+++ b/component/README.rst
@@ -0,0 +1,160 @@
+==========
+Components
+==========
+
+.. 
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+   !! This file is generated by oca-gen-addon-readme !!
+   !! changes will be overwritten.                   !!
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+   !! source digest: sha256:460c927ffe0fe97c01fdc7decac3df3b2b01700e16dc071eef0c9d2765ba0b1e
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
+    :target: https://odoo-community.org/page/development-status
+    :alt: Production/Stable
+.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
+    :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
+    :alt: License: LGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github
+    :target: https://github.com/OCA/connector/tree/17.0/component
+    :alt: OCA/connector
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+    :target: https://translation.odoo-community.org/projects/connector-17-0/connector-17-0-component
+    :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/connector&target_branch=17.0
+    :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module implements a component system and is a base block for the
+Connector Framework. It can be used without using the full Connector
+though.
+
+Documentation: http://odoo-connector.com/
+
+You may also want to check the `Introduction to Odoo
+Components <https://dev.to/guewen/introduction-to-odoo-components-bn0>`__
+by @guewen.
+
+**Table of contents**
+
+.. contents::
+   :local:
+
+Usage
+=====
+
+As a developer, you have access to a component system. You can find the
+documentation in the code or on http://odoo-connector.com
+
+In a nutshell, you can create components:
+
+::
+
+   from odoo.addons.component.core import Component
+
+   class MagentoPartnerAdapter(Component):
+       _name = 'magento.partner.adapter'
+       _inherit = 'magento.adapter'
+
+       _usage = 'backend.adapter'
+       _collection = 'magento.backend'
+       _apply_on = ['res.partner']
+
+And later, find the component you need at runtime (dynamic dispatch at
+component level):
+
+::
+
+   def run(self, external_id):
+       backend_adapter = self.component(usage='backend.adapter')
+       external_data = backend_adapter.read(external_id)
+
+In order for tests using components to work, you will need to use the
+base class provided by \`odoo.addons.component.tests.common\`:
+
+-  TransactionComponentCase
+
+There are also some specific base classes for testing the component
+registry, using the ComponentRegistryCase as a base class. See the
+docstrings in tests/common.py.
+
+Changelog
+=========
+
+16.0.1.0.0 (2022-10-04)
+-----------------------
+
+-  [MIGRATION] from 15.0
+
+15.0.1.0.0 (2021-11-25)
+-----------------------
+
+-  [MIGRATION] from 14.0
+
+14.0.1.0.0 (2020-10-22)
+-----------------------
+
+-  [MIGRATION] from 13.0
+
+13.0.1.0.0 (2019-10-23)
+-----------------------
+
+-  [MIGRATION] from 12.0
+
+12.0.1.0.0 (2018-10-02)
+-----------------------
+
+-  [MIGRATION] from 11.0 branched at rev. 324e006
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues <https://github.com/OCA/connector/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 <https://github.com/OCA/connector/issues/new?body=module:%20component%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Camptocamp
+
+Contributors
+------------
+
+-  Guewen Baconnier <guewen.baconnier@camptocamp.com>
+-  Laurent Mignon <laurent.mignon@acsone.eu>
+-  Simone Orsi <simone.orsi@camptocamp.com>
+
+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.
+
+.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px
+    :target: https://github.com/guewen
+    :alt: guewen
+
+Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
+
+|maintainer-guewen| 
+
+This module is part of the `OCA/connector <https://github.com/OCA/connector/tree/17.0/component>`_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/component/__init__.py b/component/__init__.py
new file mode 100644
index 000000000..70fab2841
--- /dev/null
+++ b/component/__init__.py
@@ -0,0 +1,5 @@
+from . import core
+
+from . import components
+from . import builder
+from . import models
diff --git a/component/__manifest__.py b/component/__manifest__.py
new file mode 100644
index 000000000..484fb8a3e
--- /dev/null
+++ b/component/__manifest__.py
@@ -0,0 +1,22 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+{
+    "name": "Components",
+    "summary": "Add capabilities to register and use decoupled components,"
+    " as an alternative to model classes",
+    "version": "17.0.1.0.0",
+    "author": "Camptocamp," "Odoo Community Association (OCA)",
+    "website": "https://github.com/OCA/connector",
+    "license": "LGPL-3",
+    "category": "Generic Modules",
+    "depends": ["base"],
+    "external_dependencies": {
+        "python": [
+            "cachetools",
+        ]
+    },
+    "installable": True,
+    "development_status": "Production/Stable",
+    "maintainers": ["guewen"],
+}
diff --git a/component/builder.py b/component/builder.py
new file mode 100644
index 000000000..9f5cd5f9b
--- /dev/null
+++ b/component/builder.py
@@ -0,0 +1,96 @@
+# Copyright 2019 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+"""
+
+Components Builder
+==================
+
+Build the components at the build of a registry.
+
+"""
+import odoo
+from odoo import models
+
+from .core import DEFAULT_CACHE_SIZE, ComponentRegistry, _component_databases
+
+
+class ComponentBuilder(models.AbstractModel):
+    """Build the component classes
+
+    And register them in a global registry.
+
+    Every time an Odoo registry is built, the know components are cleared and
+    rebuilt as well.  The Component classes are built using the same mechanism
+    than Odoo's Models: a final class is created, taking every Components with
+    a ``_name`` and applying Components with an ``_inherits`` upon them.
+
+    The final Component classes are registered in global registry.
+
+    This class is an Odoo model, allowing us to hook the build of the
+    components at the end of the Odoo's registry loading, using
+    ``_register_hook``. This method is called after all modules are loaded, so
+    we are sure that we have all the components Classes and in the correct
+    order.
+
+    """
+
+    _name = "component.builder"
+    _description = "Component Builder"
+
+    _components_registry_cache_size = DEFAULT_CACHE_SIZE
+
+    def _register_hook(self):
+        # This method is called by Odoo when the registry is built,
+        # so in case the registry is rebuilt (cache invalidation, ...),
+        # we have to to rebuild the components. We use a new
+        # registry so we have an empty cache and we'll add components in it.
+        components_registry = self._init_global_registry()
+        self.build_registry(components_registry)
+        components_registry.ready = True
+
+    def _init_global_registry(self):
+        components_registry = ComponentRegistry(
+            cachesize=self._components_registry_cache_size
+        )
+        _component_databases[self.env.cr.dbname] = components_registry
+        return components_registry
+
+    def build_registry(self, components_registry, states=None, exclude_addons=None):
+        if not states:
+            states = ("installed", "to upgrade")
+        # lookup all the installed (or about to be) addons and generate
+        # the graph, so we can load the components following the order
+        # of the addons' dependencies
+        graph = odoo.modules.graph.Graph()
+        graph.add_module(self.env.cr, "base")
+
+        query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s "
+        params = [tuple(states)]
+        if exclude_addons:
+            query += " AND name NOT IN %s "
+            params.append(tuple(exclude_addons))
+        self.env.cr.execute(query, params)
+
+        module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph]
+        graph.add_modules(self.env.cr, module_list)
+
+        for module in graph:
+            self.load_components(module.name, components_registry=components_registry)
+
+    def load_components(self, module, components_registry=None):
+        """Build every component known by MetaComponent for an odoo module
+
+        The final component (composed by all the Component classes in this
+        module) will be pushed into the registry.
+
+        :param module: the name of the addon for which we want to load
+                       the components
+        :type module: str | unicode
+        :param registry: the registry in which we want to put the Component
+        :type registry: :py:class:`~.core.ComponentRegistry`
+        """
+        components_registry = (
+            components_registry or _component_databases[self.env.cr.dbname]
+        )
+        components_registry.load_components(module)
diff --git a/component/components/__init__.py b/component/components/__init__.py
new file mode 100644
index 000000000..0e4444933
--- /dev/null
+++ b/component/components/__init__.py
@@ -0,0 +1 @@
+from . import base
diff --git a/component/components/base.py b/component/components/base.py
new file mode 100644
index 000000000..0996ac03d
--- /dev/null
+++ b/component/components/base.py
@@ -0,0 +1,15 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from ..core import AbstractComponent
+
+
+class BaseComponent(AbstractComponent):
+    """This is the base component for every component
+
+    It is implicitely inherited by all components.
+
+    All your base are belong to us
+    """
+
+    _name = "base"
diff --git a/component/core.py b/component/core.py
new file mode 100644
index 000000000..a842f0f92
--- /dev/null
+++ b/component/core.py
@@ -0,0 +1,937 @@
+# Copyright 2017 Camptocamp SA
+# Copyright 2017 Odoo
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+"""
+
+Core
+====
+
+Core classes for the components.
+The most common classes used publicly are:
+
+* :class:`Component`
+* :class:`AbstractComponent`
+* :class:`WorkContext`
+
+"""
+
+import logging
+import operator
+from collections import OrderedDict, defaultdict
+
+from odoo import models
+from odoo.tools import LastOrderedSet, OrderedSet
+
+from .exception import NoComponentError, RegistryNotReadyError, SeveralComponentError
+
+_logger = logging.getLogger(__name__)
+
+try:
+    from cachetools import LRUCache, cachedmethod
+except ImportError:
+    _logger.debug("Cannot import 'cachetools'.")
+
+
+# The Cache size represents the number of items, so the number
+# of components (include abstract components) we will keep in the LRU
+# cache. We would need stats to know what is the average but this is a bit
+# early.
+DEFAULT_CACHE_SIZE = 512
+
+
+# this is duplicated from odoo.models.MetaModel._get_addon_name() which we
+# unfortunately can't use because it's an instance method and should have been
+# a @staticmethod
+def _get_addon_name(full_name):
+    # The (Odoo) module name can be in the ``odoo.addons`` namespace
+    # or not. For instance, module ``sale`` can be imported as
+    # ``odoo.addons.sale`` (the right way) or ``sale`` (for backward
+    # compatibility).
+    module_parts = full_name.split(".")
+    if len(module_parts) > 2 and module_parts[:2] == ["odoo", "addons"]:
+        addon_name = full_name.split(".")[2]
+    else:
+        addon_name = full_name.split(".")[0]
+    return addon_name
+
+
+class ComponentDatabases(dict):
+    """Holds a registry of components for each database"""
+
+
+class ComponentRegistry:
+    """Store all the components and allow to find them using criteria
+
+    The key is the ``_name`` of the components.
+
+    This is an OrderedDict, because we want to keep the registration order of
+    the components, addons loaded first have their components found first.
+
+    The :attr:`ready` attribute must be set to ``True`` when all the components
+    are loaded.
+
+    """
+
+    def __init__(self, cachesize=DEFAULT_CACHE_SIZE):
+        self._cache = LRUCache(maxsize=cachesize)
+        self._components = OrderedDict()
+        self._loaded_modules = set()
+        self.ready = False
+
+    def __getitem__(self, key):
+        return self._components[key]
+
+    def __setitem__(self, key, value):
+        self._components[key] = value
+
+    def __contains__(self, key):
+        return key in self._components
+
+    def get(self, key, default=None):
+        return self._components.get(key, default)
+
+    def __iter__(self):
+        return iter(self._components)
+
+    def load_components(self, module):
+        if module in self._loaded_modules:
+            return
+        for component_class in MetaComponent._modules_components[module]:
+            component_class._build_component(self)
+        self._loaded_modules.add(module)
+
+    @cachedmethod(operator.attrgetter("_cache"))
+    def lookup(self, collection_name=None, usage=None, model_name=None):
+        """Find and return a list of components for a usage
+
+        If a component is not registered in a particular collection (no
+        ``_collection``), it will be returned in any case (as far as
+        the ``usage`` and ``model_name`` match).  This is useful to share
+        generic components across different collections.
+
+        If no collection name is given, components from any collection
+        will be returned.
+
+        Then, the components of a collection are filtered by usage and/or
+        model. The ``_usage`` is mandatory on the components. When the
+        ``_model_name`` is empty, it means it can be used for every models,
+        and it will ignore the ``model_name`` argument.
+
+        The abstract components are never returned.
+
+        This is a rather low-level function, usually you will use the
+        high-level :meth:`AbstractComponent.component`,
+        :meth:`AbstractComponent.many_components` or even
+        :meth:`AbstractComponent.component_by_name`.
+
+        :param collection_name: the name of the collection the component is
+                                registered into.
+        :param usage: the usage of component we are looking for
+        :param model_name: filter on components that apply on this model
+
+        """
+
+        # keep the order so addons loaded first have components used first
+        candidates = (
+            component
+            for component in self._components.values()
+            if not component._abstract
+        )
+
+        if collection_name is not None:
+            candidates = (
+                component
+                for component in candidates
+                if (
+                    component._collection == collection_name
+                    or component._collection is None
+                )
+            )
+
+        if usage is not None:
+            candidates = (
+                component for component in candidates if component._usage == usage
+            )
+
+        if model_name is not None:
+            candidates = (
+                c
+                for c in candidates
+                if c.apply_on_models is None or model_name in c.apply_on_models
+            )
+
+        return list(candidates)
+
+
+# We will store a ComponentRegistry per database here,
+# it will be cleared and updated when the odoo's registry is rebuilt
+_component_databases = ComponentDatabases()
+
+
+class WorkContext:
+    """Transport the context required to work with components
+
+    It is propagated through all the components, so any
+    data or instance (like a random RPC client) that need
+    to be propagated transversally to the components
+    should be kept here.
+
+    Including:
+
+    .. attribute:: model_name
+
+        Name of the model we are working with. It means that any lookup for a
+        component will be done for this model. It also provides a shortcut
+        as a `model` attribute to use directly with the Odoo model from
+        the components
+
+    .. attribute:: collection
+
+        The collection we are working with. The collection is an Odoo
+        Model that inherit from 'collection.base'. The collection attribute
+        can be a record or an "empty" model.
+
+    .. attribute:: model
+
+        Odoo Model for ``model_name`` with the same Odoo
+        :class:`~odoo.api.Environment` than the ``collection`` attribute.
+
+    This is also the entrypoint to work with the components.
+
+    ::
+
+        collection = self.env['my.collection'].browse(1)
+        work = WorkContext(model_name='res.partner', collection=collection)
+        component = work.component(usage='record.importer')
+
+    Usually you will use the context manager on the ``collection.base`` Model:
+
+    ::
+
+        collection = self.env['my.collection'].browse(1)
+        with collection.work_on('res.partner') as work:
+            component = work.component(usage='record.importer')
+
+    It supports any arbitrary keyword arguments that will become attributes of
+    the instance, and be propagated throughout all the components.
+
+    ::
+
+        collection = self.env['my.collection'].browse(1)
+        with collection.work_on('res.partner', hello='world') as work:
+            assert work.hello == 'world'
+
+    When you need to work on a different model, a new work instance will be
+    created for you when you are using the high-level API. This is what
+    happens under the hood:
+
+    ::
+
+        collection = self.env['my.collection'].browse(1)
+        with collection.work_on('res.partner', hello='world') as work:
+            assert work.model_name == 'res.partner'
+            assert work.hello == 'world'
+            work2 = work.work_on('res.users')
+            # => spawn a new WorkContext with a copy of the attributes
+            assert work2.model_name == 'res.users'
+            assert work2.hello == 'world'
+
+    """
+
+    def __init__(
+        self, model_name=None, collection=None, components_registry=None, **kwargs
+    ):
+        self.collection = collection
+        self.model_name = model_name
+        self.model = self.env[model_name]
+        # lookup components in an alternative registry, used by the tests
+        if components_registry is not None:
+            self.components_registry = components_registry
+        else:
+            dbname = self.env.cr.dbname
+            try:
+                self.components_registry = _component_databases[dbname]
+            except KeyError as exc:
+                msg = (
+                    "No component registry for database %s. "
+                    "Probably because the Odoo registry has not been built "
+                    "yet."
+                )
+                _logger.error(
+                    msg,
+                    dbname,
+                )
+                raise RegistryNotReadyError(msg) from exc
+        self._propagate_kwargs = ["collection", "model_name", "components_registry"]
+        for attr_name, value in kwargs.items():
+            setattr(self, attr_name, value)
+            self._propagate_kwargs.append(attr_name)
+
+    @property
+    def env(self):
+        """Return the current Odoo env
+
+        This is the environment of the current collection.
+        """
+        return self.collection.env
+
+    def work_on(self, model_name=None, collection=None):
+        """Create a new work context for another model keeping attributes
+
+        Used when one need to lookup components for another model.
+        """
+        kwargs = {
+            attr_name: getattr(self, attr_name) for attr_name in self._propagate_kwargs
+        }
+        if collection is not None:
+            kwargs["collection"] = collection
+        if model_name is not None:
+            kwargs["model_name"] = model_name
+        return self.__class__(**kwargs)
+
+    def _component_class_by_name(self, name):
+        components_registry = self.components_registry
+        component_class = components_registry.get(name)
+        if not component_class:
+            raise NoComponentError("No component with name '%s' found." % name)
+        return component_class
+
+    def component_by_name(self, name, model_name=None):
+        """Return a component by its name
+
+        If the component exists, an instance of it will be returned,
+        initialized with the current :class:`WorkContext`.
+
+        A :exc:`odoo.addons.component.exception.NoComponentError` is raised
+        if:
+
+        * no component with this name exists
+        * the ``_apply_on`` of the found component does not match
+          with the current working model
+
+        In the latter case, it can be an indication that you need to switch to
+        a different model, you can do so by providing the ``model_name``
+        argument.
+
+        """
+        if isinstance(model_name, models.BaseModel):
+            model_name = model_name._name
+        component_class = self._component_class_by_name(name)
+        work_model = model_name or self.model_name
+        if (
+            component_class._collection
+            and self.collection._name != component_class._collection
+        ):
+            raise NoComponentError(
+                "Component with name '{}' can't be used for collection '{}'.".format(
+                    name, self.collection._name
+                )
+            )
+
+        if (
+            component_class.apply_on_models
+            and work_model not in component_class.apply_on_models
+        ):
+            if len(component_class.apply_on_models) == 1:
+                hint_models = f"'{component_class.apply_on_models[0]}'"
+            else:
+                hint_models = f"<one of {component_class.apply_on_models!r}>"
+            raise NoComponentError(
+                "Component with name '{}' can't be used for model '{}'.\n"
+                "Hint: you might want to use: "
+                "component_by_name('{}', model_name={})".format(
+                    name, work_model, name, hint_models
+                )
+            )
+
+        if work_model == self.model_name:
+            work_context = self
+        else:
+            work_context = self.work_on(model_name)
+        return component_class(work_context)
+
+    def _lookup_components(self, usage=None, model_name=None, **kw):
+        component_classes = self.components_registry.lookup(
+            self.collection._name, usage=usage, model_name=model_name
+        )
+        matching_components = []
+        for cls in component_classes:
+            try:
+                matching = cls._component_match(
+                    self, usage=usage, model_name=model_name, **kw
+                )
+            except TypeError as err:
+                # Backward compat
+                _logger.info(str(err))
+                _logger.info(
+                    "The signature of %s._component_match has changed. "
+                    "Please, adapt your code as "
+                    "(self, usage=usage, model_name=model_name, **kw)",
+                    cls.__name__,
+                )
+                matching = cls._component_match(self)
+            if matching:
+                matching_components.append(cls)
+        return matching_components
+
+    def _filter_components_by_collection(self, component_classes):
+        return [c for c in component_classes if c._collection == self.collection._name]
+
+    def _filter_components_by_model(self, component_classes, model_name):
+        return [
+            c
+            for c in component_classes
+            if c.apply_on_models and model_name in c.apply_on_models
+        ]
+
+    def _ensure_model_name(self, model_name):
+        """Make sure model name is a string or fallback to current ctx value."""
+        if isinstance(model_name, models.BaseModel):
+            model_name = model_name._name
+        return model_name or self.model_name
+
+    def _matching_components(self, usage=None, model_name=None, **kw):
+        """Retrieve matching components and their work context."""
+        component_classes = self._lookup_components(
+            usage=usage, model_name=model_name, **kw
+        )
+        if model_name == self.model_name:
+            work_context = self
+        else:
+            work_context = self.work_on(model_name)
+        return component_classes, work_context
+
+    def component(self, usage=None, model_name=None, **kw):
+        """Find a component by usage and model for the current collection
+
+        It searches a component using the rules of
+        :meth:`ComponentRegistry.lookup`. When a component is found,
+        it initialize it with the current :class:`WorkContext` and returned.
+
+        A component with a ``_apply_on`` matching the asked ``model_name``
+        takes precedence over a generic component without ``_apply_on``.
+        A component with a ``_collection`` matching the current collection
+        takes precedence over a generic component without ``_collection``.
+        This behavior allows to define generic components across collections
+        and/or models and override them only for a particular collection and/or
+        model.
+
+        A :exc:`odoo.addons.component.exception.SeveralComponentError` is
+        raised if more than one component match for the provided
+        ``usage``/``model_name``.
+
+        A :exc:`odoo.addons.component.exception.NoComponentError` is raised
+        if no component is found for the provided ``usage``/``model_name``.
+
+        """
+        model_name = self._ensure_model_name(model_name)
+        component_classes, work_context = self._matching_components(
+            usage=usage, model_name=model_name, **kw
+        )
+        if not component_classes:
+            raise NoComponentError(
+                f"No component found for collection '{self.collection._name}', "
+                f"usage '{usage}', model_name '{model_name}'."
+            )
+        elif len(component_classes) > 1:
+            # If we have more than one component, try to find the one
+            # specifically linked to the collection...
+            component_classes = self._filter_components_by_collection(component_classes)
+        if len(component_classes) > 1:
+            # ... or try to find the one specifically linked to the model
+            component_classes = self._filter_components_by_model(
+                component_classes, model_name
+            )
+        if len(component_classes) != 1:
+            raise SeveralComponentError(
+                "Several components found for collection '{}', "
+                "usage '{}', model_name '{}'. Found: {}".format(
+                    self.collection._name,
+                    usage or "",
+                    model_name or "",
+                    component_classes,
+                )
+            )
+        return component_classes[0](work_context)
+
+    def many_components(self, usage=None, model_name=None, **kw):
+        """Find many components by usage and model for the current collection
+
+        It searches a component using the rules of
+        :meth:`ComponentRegistry.lookup`. When components are found, they
+        initialized with the current :class:`WorkContext` and returned as a
+        list.
+
+        If no component is found, an empty list is returned.
+
+        """
+        model_name = self._ensure_model_name(model_name)
+        component_classes, work_context = self._matching_components(
+            usage=usage, model_name=model_name, **kw
+        )
+        return [comp(work_context) for comp in component_classes]
+
+    def __str__(self):
+        return f"WorkContext({self.model_name}, {repr(self.collection)})"
+
+    __repr__ = __str__
+
+
+class MetaComponent(type):
+    """Metaclass for Components
+
+    Every new :class:`Component` will be added to ``_modules_components``,
+    that will be used by the component builder.
+
+    """
+
+    _modules_components = defaultdict(list)
+
+    def __init__(cls, name, bases, attrs):
+        if not cls._register:
+            cls._register = True
+            super().__init__(name, bases, attrs)
+            return
+
+        # If components are declared in tests, exclude them from the
+        # "components of the addon" list. If not, when we use the
+        # "load_components" method, all the test components would be loaded.
+        # This should never be an issue when running the app normally, as the
+        # Python tests should never be executed. But this is an issue when a
+        # test creates a test components for the purpose of the test, then a
+        # second tests uses the "load_components" to load all the addons of the
+        # module: it will load the component of the previous test.
+        if "tests" in cls.__module__.split("."):
+            return
+
+        if not hasattr(cls, "_module"):
+            cls._module = _get_addon_name(cls.__module__)
+
+        cls._modules_components[cls._module].append(cls)
+
+    @property
+    def apply_on_models(cls):
+        # None means all models
+        if cls._apply_on is None:
+            return None
+        # always return a list, used for the lookup
+        elif isinstance(cls._apply_on, str):
+            return [cls._apply_on]
+        return cls._apply_on
+
+
+class AbstractComponent(metaclass=MetaComponent):
+    """Main Component Model
+
+    All components have a Python inheritance either on
+    :class:`AbstractComponent` or either on :class:`Component`.
+
+    Abstract Components will not be returned by lookups on components, however
+    they can be used as a base for other Components through inheritance (using
+    ``_inherit``).
+
+    Inheritance mechanism
+        The inheritance mechanism is like the Odoo's one for Models.  Each
+        component has a ``_name``. This is the absolute minimum in a Component
+        class.
+
+        ::
+
+            class MyComponent(Component):
+                _name = 'my.component'
+
+                def speak(self, message):
+                    print message
+
+        Every component implicitly inherit from the `'base'` component.
+
+        There are two close but distinct inheritance types, which look
+        familiar if you already know Odoo.  The first uses ``_inherit`` with
+        an existing name, the name of the component we want to extend.  With
+        the following example, ``my.component`` is now able to speak and to
+        yell.
+
+        ::
+
+            class MyComponent(Component):  # name of the class does not matter
+                _inherit = 'my.component'
+
+                def yell(self, message):
+                    print message.upper()
+
+        The second has a different ``_name``, it creates a new component,
+        including the behavior of the inherited component, but without
+        modifying it. In the following example, ``my.component`` is still able
+        to speak and to yell (brough by the previous inherit), but not to
+        sing.  ``another.component`` is able to speak, to yell and to sing.
+
+        ::
+
+            class AnotherComponent(Component):
+                _name = 'another.component'
+                _inherit = 'my.component'
+
+                def sing(self, message):
+                    print message.upper()
+
+    Registration and lookups
+        It is handled by 3 attributes on the class:
+
+        _collection
+            The name of the collection where we want to register the
+            component.  This is not strictly mandatory as a component can be
+            shared across several collections. But usually, you want to set a
+            collection to segregate the components for a domain.  A collection
+            can be for instance ``magento.backend``. It is also the name of a
+            model that inherits from ``collection.base``.  See also
+            :class:`~WorkContext` and
+            :class:`~odoo.addons.component.models.collection.Collection`.
+
+        _apply_on
+            List of names or name of the Odoo model(s) for which the component
+            can be used.  When not set, the component can be used on any model.
+
+        _usage
+           The collection and the model (``_apply_on``) will help to filter
+           the candidate components according to our working context (e.g. I'm
+           working on ``magento.backend`` with the model
+           ``magento.res.partner``).  The usage will define **what** kind of
+           task the component we are looking for serves to. For instance, it
+           might be ``record.importer``, ``export.mapper```... but you can be
+           as creative as you want.
+
+        Now, to get a component, you'll likely use
+        :meth:`WorkContext.component` when you start to work with components
+        in your flow, but then from within your components, you are more
+        likely to use one of:
+
+        * :meth:`component`
+        * :meth:`many_components`
+        * :meth:`component_by_name` (more rarely though)
+
+        Declaration of some Components can look like::
+
+            class FooBar(models.Model):
+                _name = 'foo.bar.collection'
+                _inherit = 'collection.base'  # this inherit is required
+
+
+            class FooBarBase(AbstractComponent):
+                _name = 'foo.bar.base'
+                _collection = 'foo.bar.collection'  # name of the model above
+
+
+            class Foo(Component):
+                _name = 'foo'
+                _inherit = 'foo.bar.base'  # we will inherit the _collection
+                _apply_on = 'res.users'
+                _usage = 'speak'
+
+                def utter(self, message):
+                    print message
+
+
+            class Bar(Component):
+                _name = 'bar'
+                _inherit = 'foo.bar.base'  # we will inherit the _collection
+                _apply_on = 'res.users'
+                _usage = 'yell'
+
+                def utter(self, message):
+                    print message.upper() + '!!!'
+
+
+            class Vocalizer(Component):
+                _name = 'vocalizer'
+                _inherit = 'foo.bar.base'
+                _usage = 'vocalizer'
+                # can be used for any model
+
+                def vocalize(action, message):
+                    self.component(usage=action).utter(message)
+
+
+        And their usage::
+
+            >>> coll = self.env['foo.bar.collection'].browse(1)
+            >>> with coll.work_on('res.users') as work:
+            ...     vocalizer = work.component(usage='vocalizer')
+            ...     vocalizer.vocalize('speak', 'hello world')
+            ...
+            hello world
+            ...     vocalizer.vocalize('yell', 'hello world')
+            HELLO WORLD!!!
+
+    Hints:
+
+    * If you want to create components without ``_apply_on``, choose a
+      ``_usage`` that will not conflict other existing components.
+    * Unless this is what you want and in that case you use
+      :meth:`many_components` which will return all components for a usage
+      with a matching or a not set ``_apply_on``.
+    * It is advised to namespace the names of the components (e.g.
+      ``magento.xxx``) to prevent conflicts between addons.
+
+    """
+
+    _register = False
+    _abstract = True
+
+    # used for inheritance
+    _name = None  #: Name of the component
+
+    #: Name or list of names of the component(s) to inherit from
+    _inherit = None
+
+    #: name of the collection to subscribe in
+    _collection = None
+
+    #: List of models on which the component can be applied.
+    #: None means any Model, can be a list ['res.users', ...]
+    _apply_on = None
+
+    #: Component purpose ('import.mapper', ...).
+    _usage = None
+
+    def __init__(self, work_context):
+        super().__init__()
+        self.work = work_context
+
+    @classmethod
+    def _component_match(cls, work, usage=None, model_name=None, **kw):
+        """Evaluated on candidate components
+
+        When a component lookup is done and candidate(s) have
+        been found for a usage, a final call is done on this method.
+        If the method return False, the candidate component is ignored.
+
+        It can be used for instance to dynamically choose a component
+        according to a value in the :class:`WorkContext`.
+
+        Beware, if the lookups from usage, model and collection are
+        cached, the calls to :meth:`_component_match` are executed
+        each time we get components. Heavy computation should be
+        avoided.
+
+        :param work: the :class:`WorkContext` we are working with
+
+        """
+        return True
+
+    @property
+    def collection(self):
+        """Collection we are working with"""
+        return self.work.collection
+
+    @property
+    def env(self):
+        """Current Odoo environment, the one of the collection record"""
+        return self.work.env
+
+    @property
+    def model(self):
+        """The model instance we are working with"""
+        return self.work.model
+
+    def component_by_name(self, name, model_name=None):
+        """Return a component by its name
+
+        Shortcut to meth:`~WorkContext.component_by_name`
+        """
+        return self.work.component_by_name(name, model_name=model_name)
+
+    def component(self, usage=None, model_name=None, **kw):
+        """Return a component
+
+        Shortcut to meth:`~WorkContext.component`
+        """
+        return self.work.component(usage=usage, model_name=model_name, **kw)
+
+    def many_components(self, usage=None, model_name=None, **kw):
+        """Return several components
+
+        Shortcut to meth:`~WorkContext.many_components`
+        """
+        return self.work.many_components(usage=usage, model_name=model_name, **kw)
+
+    def __str__(self):
+        return "Component(%s)" % self._name
+
+    __repr__ = __str__
+
+    @classmethod
+    def _build_component(cls, registry):
+        """Instantiate a given Component in the components registry.
+
+        This method is called at the end of the Odoo's registry build.  The
+        caller is :meth:`component.builder.ComponentBuilder.load_components`.
+
+        It generates new classes, which will be the Component classes we will
+        be using.  The new classes are generated following the inheritance
+        of ``_inherit``. It ensures that the ``__bases__`` of the generated
+        Component classes follow the ``_inherit`` chain.
+
+        Once a Component class is created, it adds it in the Component Registry
+        (:class:`ComponentRegistry`), so it will be available for
+        lookups.
+
+        At the end of new class creation, a hook method
+        :meth:`_complete_component_build` is called, so you can customize
+        further the created components. An example can be found in
+        :meth:`odoo.addons.connector.components.mapper.Mapper._complete_component_build`
+
+        The following code is roughly the same than the Odoo's one for
+        building Models.
+
+        """
+
+        # In the simplest case, the component's registry class inherits from
+        # cls and the other classes that define the component in a flat
+        # hierarchy.  The registry contains the instance ``component`` (on the
+        # left). Its class, ``ComponentClass``, carries inferred metadata that
+        # is shared between all the component's instances for this registry
+        # only.
+        #
+        #   class A1(Component):                    Component
+        #       _name = 'a'                           / | \
+        #                                            A3 A2 A1
+        #   class A2(Component):                      \ | /
+        #       _inherit = 'a'                    ComponentClass
+        #
+        #   class A3(Component):
+        #       _inherit = 'a'
+        #
+        # When a component is extended by '_inherit', its base classes are
+        # modified to include the current class and the other inherited
+        # component classes.
+        # Note that we actually inherit from other ``ComponentClass``, so that
+        # extensions to an inherited component are immediately visible in the
+        # current component class, like in the following example:
+        #
+        #   class A1(Component):
+        #       _name = 'a'                          Component
+        #                                            /  / \  \
+        #   class B1(Component):                    /  A2 A1  \
+        #       _name = 'b'                        /   \  /    \
+        #                                         B2 ComponentA B1
+        #   class B2(Component):                   \     |     /
+        #       _name = 'b'                         \    |    /
+        #       _inherit = ['b', 'a']                \   |   /
+        #                                            ComponentB
+        #   class A2(Component):
+        #       _inherit = 'a'
+
+        # determine inherited components
+        parents = cls._inherit
+        if isinstance(parents, str):
+            parents = [parents]
+        elif parents is None:
+            parents = []
+
+        if cls._name in registry and not parents:
+            raise TypeError(
+                f"Component {cls._name} (in class {cls}) already exists. "
+                "Consider using _inherit instead of _name "
+                "or using a different _name."
+            )
+
+        # determine the component's name
+        name = cls._name or (len(parents) == 1 and parents[0])
+
+        if not name:
+            raise TypeError("Component %r must have a _name" % cls)
+
+        # all components except 'base' implicitly inherit from 'base'
+        if name != "base":
+            parents = list(parents) + ["base"]
+
+        # create or retrieve the component's class
+        if name in parents:
+            if name not in registry:
+                raise TypeError("Component %r does not exist in registry." % name)
+            ComponentClass = registry[name]
+            ComponentClass._build_component_check_base(cls)
+            check_parent = ComponentClass._build_component_check_parent
+        else:
+            ComponentClass = type(
+                name,
+                (AbstractComponent,),
+                {
+                    "_name": name,
+                    "_register": False,
+                    # names of children component
+                    "_inherit_children": OrderedSet(),
+                },
+            )
+            check_parent = cls._build_component_check_parent
+
+        # determine all the classes the component should inherit from
+        bases = LastOrderedSet([cls])
+        for parent in parents:
+            if parent not in registry:
+                raise TypeError(
+                    f"Component {name} inherits from non-existing component {parent}."
+                )
+            parent_class = registry[parent]
+            if parent == name:
+                for base in parent_class.__bases__:
+                    bases.add(base)
+            else:
+                check_parent(cls, parent_class)
+                bases.add(parent_class)
+                parent_class._inherit_children.add(name)
+        ComponentClass.__bases__ = tuple(bases)
+
+        ComponentClass._complete_component_build()
+
+        registry[name] = ComponentClass
+
+        return ComponentClass
+
+    @classmethod
+    def _build_component_check_base(cls, extend_cls):
+        """Check whether ``cls`` can be extended with ``extend_cls``."""
+        if cls._abstract and not extend_cls._abstract:
+            msg = (
+                "%s transforms the abstract component %r into a "
+                "non-abstract component. "
+                "That class should either inherit from AbstractComponent, "
+                "or set a different '_name'."
+            )
+            raise TypeError(msg % (extend_cls, cls._name))
+
+    @classmethod
+    def _build_component_check_parent(component_class, cls, parent_class):  # noqa: B902
+        """Check whether ``model_class`` can inherit from ``parent_class``."""
+        if component_class._abstract and not parent_class._abstract:
+            msg = (
+                "In %s, the abstract Component %r cannot inherit "
+                "from the non-abstract Component %r."
+            )
+            raise TypeError(msg % (cls, component_class._name, parent_class._name))
+
+    @classmethod
+    def _complete_component_build(cls):
+        """Complete build of the new component class
+
+        After the component has been built from its bases, this method is
+        called, and can be used to customize the class before it can be used.
+
+        Nothing is done in the base Component, but a Component can inherit
+        the method to add its own behavior.
+        """
+
+
+class Component(AbstractComponent):
+    """Concrete Component class
+
+    This is the class you inherit from when you want your component to
+    be registered in the component collections.
+
+    Look in :class:`AbstractComponent` for more details.
+
+    """
+
+    _register = False
+    _abstract = False
diff --git a/component/exception.py b/component/exception.py
new file mode 100644
index 000000000..cb4c67853
--- /dev/null
+++ b/component/exception.py
@@ -0,0 +1,18 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+
+class ComponentException(Exception):
+    """Base Exception for the components"""
+
+
+class NoComponentError(ComponentException):
+    """No component has been found"""
+
+
+class SeveralComponentError(ComponentException):
+    """More than one component have been found"""
+
+
+class RegistryNotReadyError(ComponentException):
+    """Component registry not ready yet for given DB."""
diff --git a/component/i18n/am.po b/component/i18n/am.po
new file mode 100644
index 000000000..a4252cda4
--- /dev/null
+++ b/component/i18n/am.po
@@ -0,0 +1,32 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Amharic (https://www.transifex.com/oca/teams/23907/am/)\n"
+"Language: am\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "ID"
diff --git a/component/i18n/ca.po b/component/i18n/ca.po
new file mode 100644
index 000000000..e0912afb8
--- /dev/null
+++ b/component/i18n/ca.po
@@ -0,0 +1,32 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n"
+"Language: ca\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "ID"
diff --git a/component/i18n/component.pot b/component/i18n/component.pot
new file mode 100644
index 000000000..89d0c298c
--- /dev/null
+++ b/component/i18n/component.pot
@@ -0,0 +1,24 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* component
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
diff --git a/component/i18n/de.po b/component/i18n/de.po
new file mode 100644
index 000000000..90e817b58
--- /dev/null
+++ b/component/i18n/de.po
@@ -0,0 +1,38 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: German (https://www.transifex.com/oca/teams/23907/de/)\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "Display Name"
+#~ msgstr "Anzeigebezeichnung"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Zuletzt aktualisiert am"
diff --git a/component/i18n/el_GR.po b/component/i18n/el_GR.po
new file mode 100644
index 000000000..34877acbb
--- /dev/null
+++ b/component/i18n/el_GR.po
@@ -0,0 +1,33 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/"
+"el_GR/)\n"
+"Language: el_GR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "Κωδικός"
diff --git a/component/i18n/es.po b/component/i18n/es.po
new file mode 100644
index 000000000..a95ceef75
--- /dev/null
+++ b/component/i18n/es.po
@@ -0,0 +1,39 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2023-08-02 13:09+0000\n"
+"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
+"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.17\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr "Colección abstracta de base"
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr "Constructor de componentes"
+
+#~ msgid "Display Name"
+#~ msgstr "Nombre mostrado"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Última modificación el"
diff --git a/component/i18n/es_ES.po b/component/i18n/es_ES.po
new file mode 100644
index 000000000..9b339546e
--- /dev/null
+++ b/component/i18n/es_ES.po
@@ -0,0 +1,33 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/"
+"es_ES/)\n"
+"Language: es_ES\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "ID"
diff --git a/component/i18n/fi.po b/component/i18n/fi.po
new file mode 100644
index 000000000..ed847137f
--- /dev/null
+++ b/component/i18n/fi.po
@@ -0,0 +1,38 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Finnish (https://www.transifex.com/oca/teams/23907/fi/)\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "Display Name"
+#~ msgstr "Nimi"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Viimeksi muokattu"
diff --git a/component/i18n/fr.po b/component/i18n/fr.po
new file mode 100644
index 000000000..0ee0ec5a3
--- /dev/null
+++ b/component/i18n/fr.po
@@ -0,0 +1,40 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+# Nicolas JEUDY <njeudy@panda-chi.io>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-02-01 01:48+0000\n"
+"PO-Revision-Date: 2018-06-28 07:13+0000\n"
+"Last-Translator: Guewen Baconnier <guewen.baconnier@camptocamp.com>\n"
+"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 3.0.1\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr "Abstract Model inital pour une collection"
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr "Constructeur de composants"
+
+#~ msgid "Display Name"
+#~ msgstr "Nom affiché"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Dernière modification le"
diff --git a/component/i18n/gl.po b/component/i18n/gl.po
new file mode 100644
index 000000000..522dc6234
--- /dev/null
+++ b/component/i18n/gl.po
@@ -0,0 +1,32 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n"
+"Language: gl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "ID"
diff --git a/component/i18n/it.po b/component/i18n/it.po
new file mode 100644
index 000000000..323840804
--- /dev/null
+++ b/component/i18n/it.po
@@ -0,0 +1,39 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2022-12-28 20:45+0000\n"
+"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
+"Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.14.1\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr "Raccolta estratto base"
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr "Costruttore componente"
+
+#~ msgid "Display Name"
+#~ msgstr "Nome da visualizzare"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Ultima modifica il"
diff --git a/component/i18n/pt.po b/component/i18n/pt.po
new file mode 100644
index 000000000..b56a934da
--- /dev/null
+++ b/component/i18n/pt.po
@@ -0,0 +1,32 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "ID"
diff --git a/component/i18n/pt_BR.po b/component/i18n/pt_BR.po
new file mode 100644
index 000000000..69926da9f
--- /dev/null
+++ b/component/i18n/pt_BR.po
@@ -0,0 +1,40 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2020-08-12 20:00+0000\n"
+"Last-Translator: Rodrigo Macedo <rmsolucoeseminformatic4@gmail.com>\n"
+"Language-Team: Portuguese (Brazil) (https://www.transifex.com/oca/"
+"teams/23907/pt_BR/)\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 3.10\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr "Coleção Base Abstrata"
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr "Construtor de Componentes"
+
+#~ msgid "Display Name"
+#~ msgstr "Exibir Nome"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Última modificação em"
diff --git a/component/i18n/pt_PT.po b/component/i18n/pt_PT.po
new file mode 100644
index 000000000..a63f8c9f2
--- /dev/null
+++ b/component/i18n/pt_PT.po
@@ -0,0 +1,33 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/"
+"teams/23907/pt_PT/)\n"
+"Language: pt_PT\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "ID"
diff --git a/component/i18n/sl.po b/component/i18n/sl.po
new file mode 100644
index 000000000..cac8313fb
--- /dev/null
+++ b/component/i18n/sl.po
@@ -0,0 +1,39 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n"
+"Language: sl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
+"%100==4 ? 2 : 3);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "Display Name"
+#~ msgstr "Prikazni naziv"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Zadnjič spremenjeno"
diff --git a/component/i18n/tr.po b/component/i18n/tr.po
new file mode 100644
index 000000000..90c594d4c
--- /dev/null
+++ b/component/i18n/tr.po
@@ -0,0 +1,32 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr ""
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr ""
+
+#~ msgid "ID"
+#~ msgstr "ID"
diff --git a/component/i18n/zh_CN.po b/component/i18n/zh_CN.po
new file mode 100644
index 000000000..47a55bd74
--- /dev/null
+++ b/component/i18n/zh_CN.po
@@ -0,0 +1,36 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* component
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 13.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2019-09-01 06:14+0000\n"
+"Last-Translator: 黎伟杰 <674416404@qq.com>\n"
+"Language-Team: none\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: component
+#: model:ir.model,name:component.model_collection_base
+msgid "Base Abstract Collection"
+msgstr "基础抽象集合"
+
+#. module: component
+#: model:ir.model,name:component.model_component_builder
+msgid "Component Builder"
+msgstr "组件构建器"
+
+#~ msgid "Display Name"
+#~ msgstr "显示名称"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Last Modified on"
+#~ msgstr "最后修改时间"
diff --git a/component/models/__init__.py b/component/models/__init__.py
new file mode 100644
index 000000000..97ad61232
--- /dev/null
+++ b/component/models/__init__.py
@@ -0,0 +1 @@
+from . import collection
diff --git a/component/models/collection.py b/component/models/collection.py
new file mode 100644
index 000000000..e9816354e
--- /dev/null
+++ b/component/models/collection.py
@@ -0,0 +1,101 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+"""
+
+Collection Model
+================
+
+This is the base Model shared by all the Collections.
+In the context of the Connector, a collection is the Backend.
+The `_name` given to the Collection Model will be the name
+to use in the `_collection` of the Components usable for the Backend.
+
+"""
+
+from contextlib import contextmanager
+
+from odoo import models
+
+from ..core import WorkContext
+
+
+class Collection(models.AbstractModel):
+    """The model on which components are subscribed
+
+    It would be for instance the ``backend`` for the connectors.
+
+    Example::
+
+        class MagentoBackend(models.Model):
+            _name = 'magento.backend'  # name of the collection
+            _inherit = 'collection.base'
+
+
+        class MagentoSaleImporter(Component):
+            _name = 'magento.sale.importer'
+            _apply_on = 'magento.sale.order'
+            _collection = 'magento.backend'  # name of the collection
+
+            def run(self, magento_id):
+                mapper = self.component(usage='import.mapper')
+                extra_mappers = self.many_components(
+                    usage='import.mapper.extra',
+                )
+                # ...
+
+    Use it::
+
+        >>> backend = self.env['magento.backend'].browse(1)
+        >>> with backend.work_on('magento.sale.order') as work:
+        ...     importer = work.component(usage='magento.sale.importer')
+        ...     importer.run(1)
+
+    See also: :class:`odoo.addons.component.core.WorkContext`
+
+
+    """
+
+    _name = "collection.base"
+    _description = "Base Abstract Collection"
+
+    @contextmanager
+    def work_on(self, model_name, **kwargs):
+        """Entry-point for the components, context manager
+
+        Start a work using the components on the model.
+        Any keyword argument will be assigned to the work context.
+        See documentation of :class:`odoo.addons.component.core.WorkContext`.
+
+        It is a context manager, so you can attach objects and clean them
+        at the end of the work session, such as::
+
+            @contextmanager
+            def work_on(self, model_name, **kwargs):
+                self.ensure_one()
+                magento_location = MagentoLocation(
+                    self.location,
+                    self.username,
+                    self.password,
+                )
+                # We create a Magento Client API here, so we can create the
+                # client once (lazily on the first use) and propagate it
+                # through all the sync session, instead of recreating a client
+                # in each backend adapter usage.
+                with MagentoAPI(magento_location) as magento_api:
+                    _super = super(MagentoBackend, self)
+                    # from the components we'll be able to do:
+                    # self.work.magento_api
+                    with _super.work_on(
+                            model_name, magento_api=magento_api, **kwargs
+                            ) as work:
+                        yield work
+
+        """
+        self.ensure_one()
+        # Allow propagation of custom component registry via context
+        # TODO: maybe to be moved to `WorkContext.__init__`
+        components_registry = self.env.context.get("components_registry")
+        if components_registry:
+            kwargs["components_registry"] = components_registry
+        yield WorkContext(model_name=model_name, collection=self, **kwargs)
diff --git a/component/pyproject.toml b/component/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/component/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/component/readme/CONTRIBUTORS.md b/component/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..6e153f77b
--- /dev/null
+++ b/component/readme/CONTRIBUTORS.md
@@ -0,0 +1,3 @@
+- Guewen Baconnier \<<guewen.baconnier@camptocamp.com>\>
+- Laurent Mignon \<<laurent.mignon@acsone.eu>\>
+- Simone Orsi \<<simone.orsi@camptocamp.com>\>
diff --git a/component/readme/DESCRIPTION.md b/component/readme/DESCRIPTION.md
new file mode 100644
index 000000000..8ac7a8552
--- /dev/null
+++ b/component/readme/DESCRIPTION.md
@@ -0,0 +1,9 @@
+This module implements a component system and is a base block for the
+Connector Framework. It can be used without using the full Connector
+though.
+
+Documentation: <http://odoo-connector.com/>
+
+You may also want to check the [Introduction to Odoo
+Components](https://dev.to/guewen/introduction-to-odoo-components-bn0)
+by @guewen.
diff --git a/component/readme/HISTORY.md b/component/readme/HISTORY.md
new file mode 100644
index 000000000..e98452fda
--- /dev/null
+++ b/component/readme/HISTORY.md
@@ -0,0 +1,19 @@
+## 16.0.1.0.0 (2022-10-04)
+
+- \[MIGRATION\] from 15.0
+
+## 15.0.1.0.0 (2021-11-25)
+
+- \[MIGRATION\] from 14.0
+
+## 14.0.1.0.0 (2020-10-22)
+
+- \[MIGRATION\] from 13.0
+
+## 13.0.1.0.0 (2019-10-23)
+
+- \[MIGRATION\] from 12.0
+
+## 12.0.1.0.0 (2018-10-02)
+
+- \[MIGRATION\] from 11.0 branched at rev. 324e006
diff --git a/component/readme/USAGE.md b/component/readme/USAGE.md
new file mode 100644
index 000000000..92816e93e
--- /dev/null
+++ b/component/readme/USAGE.md
@@ -0,0 +1,30 @@
+As a developer, you have access to a component system. You can find the
+documentation in the code or on <http://odoo-connector.com>
+
+In a nutshell, you can create components:
+
+    from odoo.addons.component.core import Component
+
+    class MagentoPartnerAdapter(Component):
+        _name = 'magento.partner.adapter'
+        _inherit = 'magento.adapter'
+
+        _usage = 'backend.adapter'
+        _collection = 'magento.backend'
+        _apply_on = ['res.partner']
+
+And later, find the component you need at runtime (dynamic dispatch at
+component level):
+
+    def run(self, external_id):
+        backend_adapter = self.component(usage='backend.adapter')
+        external_data = backend_adapter.read(external_id)
+
+In order for tests using components to work, you will need to use the
+base class provided by \`odoo.addons.component.tests.common\`:
+
+- TransactionComponentCase
+
+There are also some specific base classes for testing the component
+registry, using the ComponentRegistryCase as a base class. See the
+docstrings in tests/common.py.
diff --git a/component/static/description/icon.png b/component/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/component/static/description/icon.png differ
diff --git a/component/static/description/index.html b/component/static/description/index.html
new file mode 100644
index 000000000..10a694620
--- /dev/null
+++ b/component/static/description/index.html
@@ -0,0 +1,505 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
+<title>Components</title>
+<style type="text/css">
+
+/*
+:Author: David Goodger (goodger@python.org)
+:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
+:Copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the HTML output of Docutils.
+
+See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
+customize this style sheet.
+*/
+
+/* used to remove borders from tables and images */
+.borderless, table.borderless td, table.borderless th {
+  border: 0 }
+
+table.borderless td, table.borderless th {
+  /* Override padding for "table.docutils td" with "! important".
+     The right padding separates the table cells. */
+  padding: 0 0.5em 0 0 ! important }
+
+.first {
+  /* Override more specific margin styles with "! important". */
+  margin-top: 0 ! important }
+
+.last, .with-subtitle {
+  margin-bottom: 0 ! important }
+
+.hidden {
+  display: none }
+
+.subscript {
+  vertical-align: sub;
+  font-size: smaller }
+
+.superscript {
+  vertical-align: super;
+  font-size: smaller }
+
+a.toc-backref {
+  text-decoration: none ;
+  color: black }
+
+blockquote.epigraph {
+  margin: 2em 5em ; }
+
+dl.docutils dd {
+  margin-bottom: 0.5em }
+
+object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
+  overflow: hidden;
+}
+
+/* Uncomment (and remove this text!) to get bold-faced definition list terms
+dl.docutils dt {
+  font-weight: bold }
+*/
+
+div.abstract {
+  margin: 2em 5em }
+
+div.abstract p.topic-title {
+  font-weight: bold ;
+  text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+  margin: 2em ;
+  border: medium outset ;
+  padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+  font-weight: bold ;
+  font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title, .code .error {
+  color: red ;
+  font-weight: bold ;
+  font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+   compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+  margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+  margin-top: 0.5em }
+*/
+
+div.dedication {
+  margin: 2em 5em ;
+  text-align: center ;
+  font-style: italic }
+
+div.dedication p.topic-title {
+  font-weight: bold ;
+  font-style: normal }
+
+div.figure {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+div.footer, div.header {
+  clear: both;
+  font-size: smaller }
+
+div.line-block {
+  display: block ;
+  margin-top: 1em ;
+  margin-bottom: 1em }
+
+div.line-block div.line-block {
+  margin-top: 0 ;
+  margin-bottom: 0 ;
+  margin-left: 1.5em }
+
+div.sidebar {
+  margin: 0 0 0.5em 1em ;
+  border: medium outset ;
+  padding: 1em ;
+  background-color: #ffffee ;
+  width: 40% ;
+  float: right ;
+  clear: right }
+
+div.sidebar p.rubric {
+  font-family: sans-serif ;
+  font-size: medium }
+
+div.system-messages {
+  margin: 5em }
+
+div.system-messages h1 {
+  color: red }
+
+div.system-message {
+  border: medium outset ;
+  padding: 1em }
+
+div.system-message p.system-message-title {
+  color: red ;
+  font-weight: bold }
+
+div.topic {
+  margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+  margin-top: 0.4em }
+
+h1.title {
+  text-align: center }
+
+h2.subtitle {
+  text-align: center }
+
+hr.docutils {
+  width: 75% }
+
+img.align-left, .figure.align-left, object.align-left, table.align-left {
+  clear: left ;
+  float: left ;
+  margin-right: 1em }
+
+img.align-right, .figure.align-right, object.align-right, table.align-right {
+  clear: right ;
+  float: right ;
+  margin-left: 1em }
+
+img.align-center, .figure.align-center, object.align-center {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+table.align-center {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.align-left {
+  text-align: left }
+
+.align-center {
+  clear: both ;
+  text-align: center }
+
+.align-right {
+  text-align: right }
+
+/* reset inner alignment in figures */
+div.align-right {
+  text-align: inherit }
+
+/* div.align-center * { */
+/*   text-align: left } */
+
+.align-top    {
+  vertical-align: top }
+
+.align-middle {
+  vertical-align: middle }
+
+.align-bottom {
+  vertical-align: bottom }
+
+ol.simple, ul.simple {
+  margin-bottom: 1em }
+
+ol.arabic {
+  list-style: decimal }
+
+ol.loweralpha {
+  list-style: lower-alpha }
+
+ol.upperalpha {
+  list-style: upper-alpha }
+
+ol.lowerroman {
+  list-style: lower-roman }
+
+ol.upperroman {
+  list-style: upper-roman }
+
+p.attribution {
+  text-align: right ;
+  margin-left: 50% }
+
+p.caption {
+  font-style: italic }
+
+p.credits {
+  font-style: italic ;
+  font-size: smaller }
+
+p.label {
+  white-space: nowrap }
+
+p.rubric {
+  font-weight: bold ;
+  font-size: larger ;
+  color: maroon ;
+  text-align: center }
+
+p.sidebar-title {
+  font-family: sans-serif ;
+  font-weight: bold ;
+  font-size: larger }
+
+p.sidebar-subtitle {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+p.topic-title {
+  font-weight: bold }
+
+pre.address {
+  margin-bottom: 0 ;
+  margin-top: 0 ;
+  font: inherit }
+
+pre.literal-block, pre.doctest-block, pre.math, pre.code {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+pre.code .ln { color: grey; } /* line numbers */
+pre.code, code { background-color: #eeeeee }
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+span.classifier {
+  font-family: sans-serif ;
+  font-style: oblique }
+
+span.classifier-delimiter {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+span.interpreted {
+  font-family: sans-serif }
+
+span.option {
+  white-space: nowrap }
+
+span.pre {
+  white-space: pre }
+
+span.problematic {
+  color: red }
+
+span.section-subtitle {
+  /* font-size relative to parent (h1..h6 element) */
+  font-size: 80% }
+
+table.citation {
+  border-left: solid 1px gray;
+  margin-left: 1px }
+
+table.docinfo {
+  margin: 2em 4em }
+
+table.docutils {
+  margin-top: 0.5em ;
+  margin-bottom: 0.5em }
+
+table.footnote {
+  border-left: solid 1px black;
+  margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+  padding-left: 0.5em ;
+  padding-right: 0.5em ;
+  vertical-align: top }
+
+table.docutils th.field-name, table.docinfo th.docinfo-name {
+  font-weight: bold ;
+  text-align: left ;
+  white-space: nowrap ;
+  padding-left: 0 }
+
+/* "booktabs" style (no vertical lines) */
+table.docutils.booktabs {
+  border: 0px;
+  border-top: 2px solid;
+  border-bottom: 2px solid;
+  border-collapse: collapse;
+}
+table.docutils.booktabs * {
+  border: 0px;
+}
+table.docutils.booktabs th {
+  border-bottom: thin solid;
+  text-align: left;
+}
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+  font-size: 100% }
+
+ul.auto-toc {
+  list-style-type: none }
+
+</style>
+</head>
+<body>
+<div class="document" id="components">
+<h1 class="title">Components</h1>
+
+<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!! This file is generated by oca-gen-addon-readme !!
+!! changes will be overwritten.                   !!
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!! source digest: sha256:460c927ffe0fe97c01fdc7decac3df3b2b01700e16dc071eef0c9d2765ba0b1e
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
+<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/connector/tree/17.0/component"><img alt="OCA/connector" src="https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/connector-17-0/connector-17-0-component"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/connector&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
+<p>This module implements a component system and is a base block for the
+Connector Framework. It can be used without using the full Connector
+though.</p>
+<p>Documentation: <a class="reference external" href="http://odoo-connector.com/">http://odoo-connector.com/</a></p>
+<p>You may also want to check the <a class="reference external" href="https://dev.to/guewen/introduction-to-odoo-components-bn0">Introduction to Odoo
+Components</a>
+by &#64;guewen.</p>
+<p><strong>Table of contents</strong></p>
+<div class="contents local topic" id="contents">
+<ul class="simple">
+<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
+<li><a class="reference internal" href="#changelog" id="toc-entry-2">Changelog</a><ul>
+<li><a class="reference internal" href="#section-1" id="toc-entry-3">16.0.1.0.0 (2022-10-04)</a></li>
+<li><a class="reference internal" href="#section-2" id="toc-entry-4">15.0.1.0.0 (2021-11-25)</a></li>
+<li><a class="reference internal" href="#section-3" id="toc-entry-5">14.0.1.0.0 (2020-10-22)</a></li>
+<li><a class="reference internal" href="#section-4" id="toc-entry-6">13.0.1.0.0 (2019-10-23)</a></li>
+<li><a class="reference internal" href="#section-5" id="toc-entry-7">12.0.1.0.0 (2018-10-02)</a></li>
+</ul>
+</li>
+<li><a class="reference internal" href="#bug-tracker" id="toc-entry-8">Bug Tracker</a></li>
+<li><a class="reference internal" href="#credits" id="toc-entry-9">Credits</a><ul>
+<li><a class="reference internal" href="#authors" id="toc-entry-10">Authors</a></li>
+<li><a class="reference internal" href="#contributors" id="toc-entry-11">Contributors</a></li>
+<li><a class="reference internal" href="#maintainers" id="toc-entry-12">Maintainers</a></li>
+</ul>
+</li>
+</ul>
+</div>
+<div class="section" id="usage">
+<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
+<p>As a developer, you have access to a component system. You can find the
+documentation in the code or on <a class="reference external" href="http://odoo-connector.com">http://odoo-connector.com</a></p>
+<p>In a nutshell, you can create components:</p>
+<pre class="literal-block">
+from odoo.addons.component.core import Component
+
+class MagentoPartnerAdapter(Component):
+    _name = 'magento.partner.adapter'
+    _inherit = 'magento.adapter'
+
+    _usage = 'backend.adapter'
+    _collection = 'magento.backend'
+    _apply_on = ['res.partner']
+</pre>
+<p>And later, find the component you need at runtime (dynamic dispatch at
+component level):</p>
+<pre class="literal-block">
+def run(self, external_id):
+    backend_adapter = self.component(usage='backend.adapter')
+    external_data = backend_adapter.read(external_id)
+</pre>
+<p>In order for tests using components to work, you will need to use the
+base class provided by `odoo.addons.component.tests.common`:</p>
+<ul class="simple">
+<li>TransactionComponentCase</li>
+</ul>
+<p>There are also some specific base classes for testing the component
+registry, using the ComponentRegistryCase as a base class. See the
+docstrings in tests/common.py.</p>
+</div>
+<div class="section" id="changelog">
+<h1><a class="toc-backref" href="#toc-entry-2">Changelog</a></h1>
+<div class="section" id="section-1">
+<h2><a class="toc-backref" href="#toc-entry-3">16.0.1.0.0 (2022-10-04)</a></h2>
+<ul class="simple">
+<li>[MIGRATION] from 15.0</li>
+</ul>
+</div>
+<div class="section" id="section-2">
+<h2><a class="toc-backref" href="#toc-entry-4">15.0.1.0.0 (2021-11-25)</a></h2>
+<ul class="simple">
+<li>[MIGRATION] from 14.0</li>
+</ul>
+</div>
+<div class="section" id="section-3">
+<h2><a class="toc-backref" href="#toc-entry-5">14.0.1.0.0 (2020-10-22)</a></h2>
+<ul class="simple">
+<li>[MIGRATION] from 13.0</li>
+</ul>
+</div>
+<div class="section" id="section-4">
+<h2><a class="toc-backref" href="#toc-entry-6">13.0.1.0.0 (2019-10-23)</a></h2>
+<ul class="simple">
+<li>[MIGRATION] from 12.0</li>
+</ul>
+</div>
+<div class="section" id="section-5">
+<h2><a class="toc-backref" href="#toc-entry-7">12.0.1.0.0 (2018-10-02)</a></h2>
+<ul class="simple">
+<li>[MIGRATION] from 11.0 branched at rev. 324e006</li>
+</ul>
+</div>
+</div>
+<div class="section" id="bug-tracker">
+<h1><a class="toc-backref" href="#toc-entry-8">Bug Tracker</a></h1>
+<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/connector/issues">GitHub Issues</a>.
+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
+<a class="reference external" href="https://github.com/OCA/connector/issues/new?body=module:%20component%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
+<p>Do not contact contributors directly about support or help with technical issues.</p>
+</div>
+<div class="section" id="credits">
+<h1><a class="toc-backref" href="#toc-entry-9">Credits</a></h1>
+<div class="section" id="authors">
+<h2><a class="toc-backref" href="#toc-entry-10">Authors</a></h2>
+<ul class="simple">
+<li>Camptocamp</li>
+</ul>
+</div>
+<div class="section" id="contributors">
+<h2><a class="toc-backref" href="#toc-entry-11">Contributors</a></h2>
+<ul class="simple">
+<li>Guewen Baconnier &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;</li>
+<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
+<li>Simone Orsi &lt;<a class="reference external" href="mailto:simone.orsi&#64;camptocamp.com">simone.orsi&#64;camptocamp.com</a>&gt;</li>
+</ul>
+</div>
+<div class="section" id="maintainers">
+<h2><a class="toc-backref" href="#toc-entry-12">Maintainers</a></h2>
+<p>This module is maintained by the OCA.</p>
+<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
+<p>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.</p>
+<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
+<p><a class="reference external image-reference" href="https://github.com/guewen"><img alt="guewen" src="https://github.com/guewen.png?size=40px" /></a></p>
+<p>This module is part of the <a class="reference external" href="https://github.com/OCA/connector/tree/17.0/component">OCA/connector</a> project on GitHub.</p>
+<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
+</div>
+</div>
+</div>
+</body>
+</html>
diff --git a/component/tests/__init__.py b/component/tests/__init__.py
new file mode 100644
index 000000000..29c286e26
--- /dev/null
+++ b/component/tests/__init__.py
@@ -0,0 +1,5 @@
+from . import test_build_component
+from . import test_component
+from . import test_lookup
+from . import test_work_on
+from . import test_utils
diff --git a/component/tests/common.py b/component/tests/common.py
new file mode 100644
index 000000000..547a1f198
--- /dev/null
+++ b/component/tests/common.py
@@ -0,0 +1,212 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import copy
+from contextlib import contextmanager
+
+import odoo
+from odoo import api
+from odoo.tests import common
+
+from odoo.addons.component.core import ComponentRegistry, MetaComponent, _get_addon_name
+
+
+@contextmanager
+def new_rollbacked_env():
+    registry = odoo.registry(common.get_db_name())
+    uid = odoo.SUPERUSER_ID
+    cr = registry.cursor()
+    try:
+        yield api.Environment(cr, uid, {})
+    finally:
+        cr.rollback()  # we shouldn't have to commit anything
+        cr.close()
+
+
+class ComponentMixin:
+    @classmethod
+    def setUpComponent(cls):
+        with new_rollbacked_env() as env:
+            builder = env["component.builder"]
+            # build the components of every installed addons
+            comp_registry = builder._init_global_registry()
+            cls._components_registry = comp_registry
+            # ensure that we load only the components of the 'installed'
+            # modules, not 'to install', which means we load only the
+            # dependencies of the tested addons, not the siblings or
+            # children addons
+            builder.build_registry(comp_registry, states=("installed",))
+            # build the components of the current tested addon
+            current_addon = _get_addon_name(cls.__module__)
+            env["component.builder"].load_components(current_addon)
+            if hasattr(cls, "env"):
+                cls.env.context = dict(
+                    cls.env.context, components_registry=cls._components_registry
+                )
+
+    # pylint: disable=W8106
+    def setUp(self):
+        # should be ready only during tests, never during installation
+        # of addons
+        self._components_registry.ready = True
+
+        @self.addCleanup
+        def notready():
+            self._components_registry.ready = False
+
+
+class TransactionComponentCase(common.TransactionCase, ComponentMixin):
+    """A TransactionCase that loads all the components
+
+    It it used like an usual Odoo's TransactionCase, but it ensures
+    that all the components of the current addon and its dependencies
+    are loaded.
+
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.setUpComponent()
+
+    # pylint: disable=W8106
+    def setUp(self):
+        # resolve an inheritance issue (common.TransactionCase does not call
+        # super)
+        common.TransactionCase.setUp(self)
+        ComponentMixin.setUp(self)
+        # There's no env on setUpClass of TransactionCase, must do it here.
+        self.env.context = dict(
+            self.env.context, components_registry=self._components_registry
+        )
+
+
+class ComponentRegistryCase:
+    """This test case can be used as a base for writings tests on components
+
+    This test case is meant to test components in a special component registry,
+    where you want to have maximum control on which components are loaded
+    or not, or when you want to create additional components in your tests.
+
+    If you only want to *use* the components of the tested addon in your tests,
+    then consider using:
+
+    * :class:`TransactionComponentCase`
+
+    This test case creates a special
+    :class:`odoo.addons.component.core.ComponentRegistry` for the purpose of
+    the tests. By default, it loads all the components of the dependencies, but
+    not the components of the current addon (which you have to handle
+    manually). In your tests, you can add more components in 2 manners.
+
+    All the components of an Odoo module::
+
+        self._load_module_components('connector')
+
+    Only specific components::
+
+        self._build_components(MyComponent1, MyComponent2)
+
+    Note: for the lookups of the components, the default component
+    registry is a global registry for the database. Here, you will
+    need to explicitly pass ``self.comp_registry`` in the
+    :class:`~odoo.addons.component.core.WorkContext`::
+
+        work = WorkContext(model_name='res.users',
+                           collection='my.collection',
+                           components_registry=self.comp_registry)
+
+    Or::
+
+        collection_record = self.env['my.collection'].browse(1)
+        with collection_record.work_on(
+                'res.partner',
+                components_registry=self.comp_registry) as work:
+
+    """
+
+    @staticmethod
+    def _setup_registry(class_or_instance):
+        # keep the original classes registered by the metaclass
+        # so we'll restore them at the end of the tests, it avoid
+        # to pollute it with Stub / Test components
+        class_or_instance._original_components = copy.deepcopy(
+            MetaComponent._modules_components
+        )
+
+        # it will be our temporary component registry for our test session
+        class_or_instance.comp_registry = ComponentRegistry()
+
+        # it builds the 'final component' for every component of the
+        # 'component' addon and push them in the component registry
+        class_or_instance.comp_registry.load_components("component")
+        # build the components of every installed addons already installed
+        # but the current addon (when running with pytest/nosetest, we
+        # simulate the --test-enable behavior by excluding the current addon
+        # which is in 'to install' / 'to upgrade' with --test-enable).
+        current_addon = _get_addon_name(class_or_instance.__module__)
+        with new_rollbacked_env() as env:
+            env["component.builder"].build_registry(
+                class_or_instance.comp_registry,
+                states=("installed",),
+                exclude_addons=[current_addon],
+            )
+
+        # Fake that we are ready to work with the registry
+        # normally, it is set to True and the end of the build
+        # of the components. Here, we'll add components later in
+        # the components registry, but we don't mind for the tests.
+        class_or_instance.comp_registry.ready = True
+        if hasattr(class_or_instance, "env"):
+            # let it propagate via ctx
+            class_or_instance.env.context = dict(
+                class_or_instance.env.context,
+                components_registry=class_or_instance.comp_registry,
+            )
+
+    @staticmethod
+    def _teardown_registry(class_or_instance):
+        # restore the original metaclass' classes
+        MetaComponent._modules_components = class_or_instance._original_components
+
+    def _load_module_components(self, module):
+        self.comp_registry.load_components(module)
+
+    def _build_components(self, *classes):
+        for cls in classes:
+            cls._build_component(self.comp_registry)
+
+
+class TransactionComponentRegistryCase(common.TransactionCase, ComponentRegistryCase):
+    """Adds Odoo Transaction in the base Component TestCase.
+
+    This class doesn't set up the registry for you.
+    You're supposed to explicitly call `_setup_registry` and `_teardown_registry`
+    when you need it, either on setUpClass and tearDownClass or setUp and tearDown.
+
+    class MyTestCase(TransactionComponentRegistryCase):
+        def setUp(self):
+            super().setUp()
+            self._setup_registry(self)
+
+        def tearDown(self):
+            self._teardown_registry(self)
+            super().tearDown()
+
+    class MyTestCase(TransactionComponentRegistryCase):
+        @classmethod
+        def setUpClass(cls):
+            super().setUpClass()
+            cls._setup_registry(cls)
+
+        @classmethod
+        def tearDownClass(cls):
+            cls._teardown_registry(cls)
+            super().tearDownClass()
+    """
+
+    # pylint: disable=W8106
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.collection = cls.env["collection.base"]
diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py
new file mode 100644
index 000000000..a77f15cfe
--- /dev/null
+++ b/component/tests/test_build_component.py
@@ -0,0 +1,285 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+# Tell pylint to not bother us for all our fake component classes
+# pylint: disable=consider-merging-classes-inherited
+
+from unittest import mock
+
+from odoo.addons.component.core import AbstractComponent, Component
+
+from .common import TransactionComponentRegistryCase
+
+
+class TestBuildComponent(TransactionComponentRegistryCase):
+    """Test build of components
+
+    All the tests in this suite are based on the same principle with
+    variations:
+
+    * Create new Components (classes inheriting from
+      :class:`component.core.Component` or
+      :class:`component.core.AbstractComponent`
+    * Call :meth:`component.core.Component._build_component` on them
+      in order to build the 'final class' composed from all the ``_inherit``
+      and push it in the components registry (``self.comp_registry`` here)
+    * Assert that classes are built, registered, have correct ``__bases__``...
+
+    """
+
+    def setUp(self):
+        super().setUp()
+        self._setup_registry(self)
+
+    def tearDown(self):
+        self._teardown_registry(self)
+        super().tearDown()
+
+    def test_no_name(self):
+        """Ensure that a component has a _name"""
+
+        class Component1(Component):
+            pass
+
+        msg = ".*must have a _name.*"
+        with self.assertRaisesRegex(TypeError, msg):
+            Component1._build_component(self.comp_registry)
+
+    def test_register(self):
+        """Able to register components in components registry"""
+
+        class Component1(Component):
+            _name = "component1"
+
+        class Component2(Component):
+            _name = "component2"
+
+        # build the 'final classes' for the components and check that we find
+        # them in the components registry
+        Component1._build_component(self.comp_registry)
+        Component2._build_component(self.comp_registry)
+        self.assertEqual(["base", "component1", "component2"], list(self.comp_registry))
+
+    def test_inherit_bases(self):
+        """Check __bases__ of Component with _inherit"""
+
+        class Component1(Component):
+            _name = "component1"
+
+        class Component2(Component):
+            _inherit = "component1"
+
+        class Component3(Component):
+            _inherit = "component1"
+
+        Component1._build_component(self.comp_registry)
+        Component2._build_component(self.comp_registry)
+        Component3._build_component(self.comp_registry)
+        self.assertEqual(
+            (Component3, Component2, Component1, self.comp_registry["base"]),
+            self.comp_registry["component1"].__bases__,
+        )
+
+    def test_prototype_inherit_bases(self):
+        """Check __bases__ of Component with _inherit and different _name"""
+
+        class Component1(Component):
+            _name = "component1"
+
+        class Component2(Component):
+            _name = "component2"
+            _inherit = "component1"
+
+        class Component3(Component):
+            _name = "component3"
+            _inherit = "component1"
+
+        class Component4(Component):
+            _name = "component4"
+            _inherit = ["component2", "component3"]
+
+        Component1._build_component(self.comp_registry)
+        Component2._build_component(self.comp_registry)
+        Component3._build_component(self.comp_registry)
+        Component4._build_component(self.comp_registry)
+        self.assertEqual(
+            (Component1, self.comp_registry["base"]),
+            self.comp_registry["component1"].__bases__,
+        )
+        self.assertEqual(
+            (Component2, self.comp_registry["component1"], self.comp_registry["base"]),
+            self.comp_registry["component2"].__bases__,
+        )
+        self.assertEqual(
+            (Component3, self.comp_registry["component1"], self.comp_registry["base"]),
+            self.comp_registry["component3"].__bases__,
+        )
+        self.assertEqual(
+            (
+                Component4,
+                self.comp_registry["component2"],
+                self.comp_registry["component3"],
+                self.comp_registry["base"],
+            ),
+            self.comp_registry["component4"].__bases__,
+        )
+
+    # pylint: disable=W8110
+    def test_custom_build(self):
+        """Check that we can hook at the end of a Component build"""
+
+        class Component1(Component):
+            _name = "component1"
+
+            @classmethod
+            def _complete_component_build(cls):
+                # This method should be called after the Component
+                # is built, and before it is pushed in the registry
+                cls._build_done = True
+
+        Component1._build_component(self.comp_registry)
+        # we inspect that our custom build has been executed
+        self.assertTrue(self.comp_registry["component1"]._build_done)
+
+    def test_inherit_attrs(self):
+        """Check attributes inheritance of Components with _inherit"""
+
+        class Component1(Component):
+            _name = "component1"
+
+            msg = "ping"
+
+            def say(self):
+                return "foo"
+
+        class Component2(Component):
+            _name = "component2"
+            _inherit = "component1"
+
+            msg = "pong"
+
+            def say(self):
+                return super().say() + " bar"
+
+        Component1._build_component(self.comp_registry)
+        Component2._build_component(self.comp_registry)
+        # we initialize the components, normally we should pass
+        # an instance of WorkContext, but we don't need a real one
+        # for this test
+        component1 = self.comp_registry["component1"](mock.Mock())
+        component2 = self.comp_registry["component2"](mock.Mock())
+        self.assertEqual("ping", component1.msg)
+        self.assertEqual("pong", component2.msg)
+        self.assertEqual("foo", component1.say())
+        self.assertEqual("foo bar", component2.say())
+
+    def test_duplicate_component(self):
+        """Check that we can't have 2 components with the same name"""
+
+        class Component1(Component):
+            _name = "component1"
+
+        class Component2(Component):
+            _name = "component1"
+
+        Component1._build_component(self.comp_registry)
+        msg = "Component.*already exists.*"
+        with self.assertRaisesRegex(TypeError, msg):
+            Component2._build_component(self.comp_registry)
+
+    def test_no_parent(self):
+        """Ensure we can't _inherit a non-existent component"""
+
+        class Component1(Component):
+            _name = "component1"
+            _inherit = "component1"
+
+        msg = "Component.*does not exist in registry.*"
+        with self.assertRaisesRegex(TypeError, msg):
+            Component1._build_component(self.comp_registry)
+
+    def test_no_parent2(self):
+        """Ensure we can't _inherit by prototype a non-existent component"""
+
+        class Component1(Component):
+            _name = "component1"
+
+        class Component2(Component):
+            _name = "component2"
+            _inherit = ["component1", "component3"]
+
+        Component1._build_component(self.comp_registry)
+        msg = "Component.*inherits from non-existing component.*"
+        with self.assertRaisesRegex(TypeError, msg):
+            Component2._build_component(self.comp_registry)
+
+    def test_add_inheritance(self):
+        """Ensure we can add a new inheritance"""
+
+        class Component1(Component):
+            _name = "component1"
+
+        class Component2(Component):
+            _name = "component2"
+
+        class Component2bis(Component):
+            _name = "component2"
+            _inherit = ["component2", "component1"]
+
+        Component1._build_component(self.comp_registry)
+        Component2._build_component(self.comp_registry)
+        Component2bis._build_component(self.comp_registry)
+
+        self.assertEqual(
+            (
+                Component2bis,
+                Component2,
+                self.comp_registry["component1"],
+                self.comp_registry["base"],
+            ),
+            self.comp_registry["component2"].__bases__,
+        )
+
+    def test_check_parent_component_over_abstract(self):
+        """Component can inherit from AbstractComponent"""
+
+        class Component1(AbstractComponent):
+            _name = "component1"
+
+        class Component2(Component):
+            _name = "component2"
+            _inherit = "component1"
+
+        Component1._build_component(self.comp_registry)
+        Component2._build_component(self.comp_registry)
+        self.assertTrue(self.comp_registry["component1"]._abstract)
+        self.assertFalse(self.comp_registry["component2"]._abstract)
+
+    def test_check_parent_abstract_over_component(self):
+        """Prevent AbstractComponent to inherit from Component"""
+
+        class Component1(Component):
+            _name = "component1"
+
+        class Component2(AbstractComponent):
+            _name = "component2"
+            _inherit = "component1"
+
+        Component1._build_component(self.comp_registry)
+        msg = ".*cannot inherit from the non-abstract.*"
+        with self.assertRaisesRegex(TypeError, msg):
+            Component2._build_component(self.comp_registry)
+
+    def test_check_transform_abstract_to_component(self):
+        """Prevent AbstractComponent to be transformed to Component"""
+
+        class Component1(AbstractComponent):
+            _name = "component1"
+
+        class Component1bis(Component):
+            _inherit = "component1"
+
+        Component1._build_component(self.comp_registry)
+        msg = ".*transforms the abstract component.*into a non-abstract.*"
+        with self.assertRaisesRegex(TypeError, msg):
+            Component1bis._build_component(self.comp_registry)
diff --git a/component/tests/test_component.py b/component/tests/test_component.py
new file mode 100644
index 000000000..69f168957
--- /dev/null
+++ b/component/tests/test_component.py
@@ -0,0 +1,393 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from contextlib import contextmanager
+
+from odoo.addons.component.core import Component
+from odoo.addons.component.exception import NoComponentError, SeveralComponentError
+
+from .common import TransactionComponentRegistryCase
+
+
+class TestComponent(TransactionComponentRegistryCase):
+    """Test usage of components
+
+    These tests are a bit more broad that mere unit tests.
+    We test the chain odoo Model -> generate a WorkContext instance -> Work
+    with Component.
+
+    Tests are inside Odoo transactions, so we can work
+    with Odoo's env / models.
+    """
+
+    def setUp(self):
+        super().setUp()
+        self._setup_registry(self)
+        self._setUpComponents()
+
+    def tearDown(self):
+        self._teardown_registry(self)
+        super().tearDown()
+
+    def _setUpComponents(self):
+        # create some Component to play with
+        class Component1(Component):
+            _name = "component1"
+            _collection = "collection.base"
+            _usage = "for.test"
+            _apply_on = ["res.partner"]
+
+        class Component2(Component):
+            _name = "component2"
+            _collection = "collection.base"
+            _usage = "for.test"
+            _apply_on = ["res.users"]
+
+        # build the components and register them in our
+        # test component registry
+        Component1._build_component(self.comp_registry)
+        Component2._build_component(self.comp_registry)
+
+        # our collection, in a less abstract use case, it
+        # could be a record of 'magento.backend' for instance
+        self.collection_record = self.collection.new()
+
+        @contextmanager
+        def get_base():
+            # Our WorkContext, it will be passed along in every
+            # components so we can share data transversally.
+            # We are working with res.partner in the following tests,
+            # unless we change it in the test.
+            with self.collection_record.work_on(
+                "res.partner",
+                # we use a custom registry only
+                # for the sake of the tests
+                components_registry=self.comp_registry,
+            ) as work:
+                # We get the 'base' component, handy to test the base
+                # methods component, many_components, ...
+                yield work.component_by_name("base")
+
+        self.get_base = get_base
+
+    def test_component_attrs(self):
+        """Basic access to a Component's attribute"""
+        with self.get_base() as base:
+            # as we are working on res.partner, we should get 'component1'
+            comp = base.work.component(usage="for.test")
+            # but this is not what we test here, we test the attributes:
+            self.assertEqual(self.collection_record, comp.collection)
+            self.assertEqual(base.work, comp.work)
+            self.assertEqual(self.env, comp.env)
+            self.assertEqual(self.env["res.partner"], comp.model)
+
+    def test_component_get_by_name_same_model(self):
+        """Use component_by_name with current working model"""
+        with self.get_base() as base:
+            # we ask a component directly by it's name, considering
+            # we work with res.partner, we should get 'component1'
+            # this is ok because it's _apply_on contains res.partner
+            comp = base.component_by_name("component1")
+            self.assertEqual("component1", comp._name)
+            self.assertEqual(self.env["res.partner"], comp.model)
+
+    def test_component_get_by_name_other_model(self):
+        """Use component_by_name with another model"""
+        with self.get_base() as base:
+            # we ask a component directly by it's name, but we
+            # want to work with 'res.users', this is ok since
+            # component2's _apply_on contains res.users
+            comp = base.component_by_name("component2", model_name="res.users")
+            self.assertEqual("component2", comp._name)
+            self.assertEqual(self.env["res.users"], comp.model)
+            # what happens under the hood, is that a new WorkContext
+            # has been created for this model, with all the other values
+            # identical to the previous WorkContext (the one for res.partner)
+            # We can check that with:
+            self.assertNotEqual(base.work, comp.work)
+            self.assertEqual("res.partner", base.work.model_name)
+            self.assertEqual("res.users", comp.work.model_name)
+
+    def test_component_get_by_name_wrong_model(self):
+        """Use component_by_name with a model not in _apply_on"""
+        msg = (
+            "Component with name 'component2' can't be used "
+            "for model 'res.partner'.*"
+        )
+        with self.get_base() as base:
+            with self.assertRaisesRegex(NoComponentError, msg):
+                # we ask for the model 'component2' but we are working
+                # with res.partner, and it only accepts res.users
+                base.component_by_name("component2")
+
+    def test_component_get_by_name_not_exist(self):
+        """Use component_by_name on a component that do not exist"""
+        msg = "No component with name 'foo' found."
+        with self.get_base() as base:
+            with self.assertRaisesRegex(NoComponentError, msg):
+                base.component_by_name("foo")
+
+    def test_component_by_usage_same_model(self):
+        """Use component(usage=...) on the same model"""
+        # we ask for a component having _usage == 'for.test', and
+        # model being res.partner (the model in the current WorkContext)
+        with self.get_base() as base:
+            comp = base.component(usage="for.test")
+            self.assertEqual("component1", comp._name)
+            self.assertEqual(self.env["res.partner"], comp.model)
+
+    def test_component_by_usage_other_model(self):
+        """Use component(usage=...) on a different model (name)"""
+        # we ask for a component having _usage == 'for.test', and
+        # a different model (res.users)
+        with self.get_base() as base:
+            comp = base.component(usage="for.test", model_name="res.users")
+            self.assertEqual("component2", comp._name)
+            self.assertEqual(self.env["res.users"], comp.model)
+            # what happens under the hood, is that a new WorkContext
+            # has been created for this model, with all the other values
+            # identical to the previous WorkContext (the one for res.partner)
+            # We can check that with:
+            self.assertNotEqual(base.work, comp.work)
+            self.assertEqual("res.partner", base.work.model_name)
+            self.assertEqual("res.users", comp.work.model_name)
+
+    def test_component_by_usage_other_model_env(self):
+        """Use component(usage=...) on a different model (instance)"""
+        with self.get_base() as base:
+            comp = base.component(usage="for.test", model_name=self.env["res.users"])
+            self.assertEqual("component2", comp._name)
+            self.assertEqual(self.env["res.users"], comp.model)
+
+    def test_component_error_several(self):
+        """Use component(usage=...) when more than one generic component match"""
+
+        # we create 1 new Component with _usage 'for.test', in the same
+        # collection and no _apply_on, and we remove the _apply_on of component
+        # 1 so they are generic components for a collection
+        class Component3(Component):
+            _name = "component3"
+            _collection = "collection.base"
+            _usage = "for.test"
+
+        class Component1(Component):
+            _inherit = "component1"
+            _collection = "collection.base"
+            _usage = "for.test"
+            _apply_on = None
+
+        Component3._build_component(self.comp_registry)
+        Component1._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            with self.assertRaises(SeveralComponentError):
+                # When a component has no _apply_on, it means it can be applied
+                # on *any* model. Here, the candidates components would be:
+                # component3 (because it has no _apply_on so apply in any case)
+                # component4 (for the same reason)
+                base.component(usage="for.test")
+
+    def test_component_error_several_same_model(self):
+        """Use component(usage=...) when more than one component match a model"""
+
+        # we create a new Component with _usage 'for.test', in the same
+        # collection and no _apply_on
+        class Component3(Component):
+            _name = "component3"
+            _collection = "collection.base"
+            _usage = "for.test"
+            _apply_on = ["res.partner"]
+
+        Component3._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            with self.assertRaises(SeveralComponentError):
+                # Here, the candidates components would be:
+                # component1 (because we are working with res.partner),
+                # component3 (for the same reason)
+                base.component(usage="for.test")
+
+    def test_component_specific_model(self):
+        """Use component(usage=...) when more than one component match but
+        only one for the specific model"""
+
+        # we create a new Component with _usage 'for.test', in the same
+        # collection and no _apply_on. This is a generic component for the
+        # collection
+        class Component3(Component):
+            _name = "component3"
+            _collection = "collection.base"
+            _usage = "for.test"
+
+        Component3._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            # When a component has no _apply_on, it means it can be applied on
+            # *any* model. Here, the candidates components would be:
+            # component1 # (because we are working with res.partner),
+            # component3 (because it # has no _apply_on so apply in any case).
+            # When a component is specifically linked to a model with
+            # _apply_on, it takes precedence over a generic component. It
+            # allows to create a generic implementation (component3 here) and
+            # override it only for a given model. So in this case, the final
+            # component is component1.
+            comp = base.component(usage="for.test")
+            self.assertEqual("component1", comp._name)
+
+    def test_component_specific_collection(self):
+        """Use component(usage=...) when more than one component match but
+        only one for the specific collection"""
+
+        # we create a new Component with _usage 'for.test', without collection
+        # and no _apply_on
+        class Component3(Component):
+            _name = "component3"
+            _usage = "for.test"
+
+        Component3._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            # When a component has no _apply_on, it means it can be applied
+            # on *any* model. Here, the candidates components would be:
+            # component1 (because we are working with res.partner),
+            # component3 (because it has no _apply_on so apply in any case).
+            # When a component has no _collection, it means it can be applied
+            # on all model if no component is found for the current collection:
+            # component3 must be ignored since a component (component1) exists
+            # and is specificaly linked to the expected collection.
+            comp = base.component(usage="for.test")
+            self.assertEqual("component1", comp._name)
+
+    def test_component_specific_collection_specific_model(self):
+        """Use component(usage=...) when more than one component match but
+        only one for the specific model and collection"""
+
+        # we create a new Component with _usage 'for.test', without collection
+        # and no _apply_on. This is a component generic for all collections and
+        # models
+        class Component3(Component):
+            _name = "component3"
+            _usage = "for.test"
+
+        Component3._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            # When a component has no _apply_on, it means it can be applied on
+            # *any* model, no _collection, it can be applied on *any*
+            # collection.
+            # Here, the candidates components would be:
+            # component1 (because we are working with res.partner),
+            # component3 (because it has no _apply_on and no _collection so
+            # apply in any case).
+            # When a component is specifically linked to a model with
+            # _apply_on, it takes precedence over a generic component, the same
+            # happens for collection. It allows to create a generic
+            # implementation (component3 here) and override it only for a given
+            # collection and model. So in this case, the final component is
+            # component1.
+            comp = base.component(usage="for.test")
+            self.assertEqual("component1", comp._name)
+
+    def test_many_components(self):
+        """Use many_components(usage=...) on the same model"""
+
+        class Component3(Component):
+            _name = "component3"
+            _collection = "collection.base"
+            _usage = "for.test"
+
+        Component3._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            comps = base.many_components(usage="for.test")
+
+        # When a component has no _apply_on, it means it can be applied
+        # on *any* model. So here, both component1 and component3 match
+        self.assertEqual(["component1", "component3"], [c._name for c in comps])
+
+    def test_many_components_other_model(self):
+        """Use many_components(usage=...) on a different model (name)"""
+
+        class Component3(Component):
+            _name = "component3"
+            _collection = "collection.base"
+            _apply_on = "res.users"
+            _usage = "for.test"
+
+        Component3._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            comps = base.many_components(usage="for.test", model_name="res.users")
+
+        self.assertEqual(["component2", "component3"], [c._name for c in comps])
+
+    def test_many_components_other_model_env(self):
+        """Use many_components(usage=...) on a different model (instance)"""
+
+        class Component3(Component):
+            _name = "component3"
+            _collection = "collection.base"
+            _apply_on = "res.users"
+            _usage = "for.test"
+
+        Component3._build_component(self.comp_registry)
+
+        with self.get_base() as base:
+            comps = base.many_components(
+                usage="for.test", model_name=self.env["res.users"]
+            )
+
+        self.assertEqual(["component2", "component3"], [c._name for c in comps])
+
+    def test_no_component(self):
+        """No component found for asked usage"""
+        with self.get_base() as base:
+            with self.assertRaises(NoComponentError):
+                base.component(usage="foo")
+
+    def test_no_many_component(self):
+        """No component found for asked usage for many_components()"""
+        with self.get_base() as base:
+            self.assertEqual([], base.many_components(usage="foo"))
+
+    def test_work_on_component(self):
+        """Check WorkContext.component() (shortcut to Component.component)"""
+        with self.get_base() as base:
+            comp = base.work.component(usage="for.test")
+            self.assertEqual("component1", comp._name)
+
+    def test_work_on_many_components(self):
+        """Check WorkContext.many_components()
+
+        (shortcut to Component.many_components)
+        """
+        with self.get_base() as base:
+            comps = base.work.many_components(usage="for.test")
+            self.assertEqual("component1", comps[0]._name)
+
+    def test_component_match(self):
+        """Lookup with match method"""
+
+        class Foo(Component):
+            _name = "foo"
+            _collection = "collection.base"
+            _usage = "speaker"
+            _apply_on = ["res.partner"]
+
+            @classmethod
+            def _component_match(cls, work, **kw):
+                return False
+
+        class Bar(Component):
+            _name = "bar"
+            _collection = "collection.base"
+            _usage = "speaker"
+            _apply_on = ["res.partner"]
+
+        self._build_components(Foo, Bar)
+
+        with self.get_base() as base:
+            # both components would we returned without the
+            # _component_match method
+            comp = base.component(usage="speaker", model_name=self.env["res.partner"])
+            self.assertEqual("bar", comp._name)
diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py
new file mode 100644
index 000000000..082ee68e9
--- /dev/null
+++ b/component/tests/test_lookup.py
@@ -0,0 +1,193 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo.addons.component.core import AbstractComponent, Component
+
+from .common import TransactionComponentRegistryCase
+
+
+class TestLookup(TransactionComponentRegistryCase):
+    """Test the ComponentRegistry
+
+    Tests in this testsuite mainly do:
+
+    * Create new Components (classes inheriting from
+      :class:`component.core.Component` or
+      :class:`component.core.AbstractComponent`
+    * Call :meth:`component.core.Component._build_component` on them
+      in order to build the 'final class' composed from all the ``_inherit``
+      and push it in the components registry (``self.comp_registry`` here)
+    * Use the lookup method of the components registry and check
+      that we get the correct result
+
+    """
+
+    def setUp(self):
+        super().setUp()
+        self._setup_registry(self)
+
+    def tearDown(self):
+        self._teardown_registry(self)
+        super().tearDown()
+
+    def test_lookup_collection(self):
+        """Lookup components of a collection"""
+
+        # we register 2 components in foobar and one in other
+        class Foo(Component):
+            _name = "foo"
+            _collection = "foobar"
+
+        class Bar(Component):
+            _name = "bar"
+            _collection = "foobar"
+
+        class Homer(Component):
+            _name = "homer"
+            _collection = "other"
+
+        self._build_components(Foo, Bar, Homer)
+
+        # we should no see the component in 'other'
+        components = self.comp_registry.lookup("foobar")
+        self.assertEqual(["foo", "bar"], [c._name for c in components])
+
+    def test_lookup_usage(self):
+        """Lookup components by usage"""
+
+        class Foo(Component):
+            _name = "foo"
+            _collection = "foobar"
+            _usage = "speaker"
+
+        class Bar(Component):
+            _name = "bar"
+            _collection = "foobar"
+            _usage = "speaker"
+
+        class Baz(Component):
+            _name = "baz"
+            _collection = "foobar"
+            _usage = "listener"
+
+        self._build_components(Foo, Bar, Baz)
+
+        components = self.comp_registry.lookup("foobar", usage="listener")
+        self.assertEqual("baz", components[0]._name)
+
+        components = self.comp_registry.lookup("foobar", usage="speaker")
+        self.assertEqual(["foo", "bar"], [c._name for c in components])
+
+    def test_lookup_no_component(self):
+        """No component"""
+        # we just expect an empty list when no component match, the error
+        # handling is handled at an higher level
+        self.assertEqual([], self.comp_registry.lookup("something", usage="something"))
+
+    def test_get_by_name(self):
+        """Get component by name"""
+
+        class Foo(AbstractComponent):
+            _name = "foo"
+            _collection = "foobar"
+
+        self._build_components(Foo)
+        # this is just a dict access
+        self.assertEqual("foo", self.comp_registry["foo"]._name)
+
+    def test_lookup_abstract(self):
+        """Do not include abstract components in lookup"""
+
+        class Foo(AbstractComponent):
+            _name = "foo"
+            _collection = "foobar"
+            _usage = "speaker"
+
+        class Bar(Component):
+            _name = "bar"
+            _inherit = "foo"
+
+        self._build_components(Foo, Bar)
+
+        comp_registry = self.comp_registry
+
+        # we should never have 'foo' in the returned components
+        # as it is abstract
+        components = comp_registry.lookup("foobar", usage="speaker")
+        self.assertEqual("bar", components[0]._name)
+
+        components = comp_registry.lookup("foobar", usage="speaker")
+        self.assertEqual(["bar"], [c._name for c in components])
+
+    def test_lookup_model_name(self):
+        """Lookup with model names"""
+
+        class Foo(Component):
+            _name = "foo"
+            _collection = "foobar"
+            _usage = "speaker"
+            # support list
+            _apply_on = ["res.partner"]
+
+        class Bar(Component):
+            _name = "bar"
+            _collection = "foobar"
+            _usage = "speaker"
+            # support string
+            _apply_on = "res.users"
+
+        class Any(Component):
+            # can be used with any model as far as we look it up
+            # with its usage
+            _name = "any"
+            _collection = "foobar"
+            _usage = "listener"
+
+        self._build_components(Foo, Bar, Any)
+
+        components = self.comp_registry.lookup(
+            "foobar", usage="speaker", model_name="res.partner"
+        )
+        self.assertEqual("foo", components[0]._name)
+
+        components = self.comp_registry.lookup(
+            "foobar", usage="speaker", model_name="res.users"
+        )
+        self.assertEqual("bar", components[0]._name)
+
+        components = self.comp_registry.lookup(
+            "foobar", usage="listener", model_name="res.users"
+        )
+        self.assertEqual("any", components[0]._name)
+
+    def test_lookup_cache(self):
+        """Lookup uses a cache"""
+
+        class Foo(Component):
+            _name = "foo"
+            _collection = "foobar"
+
+        self._build_components(Foo)
+
+        components = self.comp_registry.lookup("foobar")
+        self.assertEqual(["foo"], [c._name for c in components])
+
+        # we add a new component
+        class Bar(Component):
+            _name = "bar"
+            _collection = "foobar"
+
+        self._build_components(Bar)
+
+        # As the lookups are cached, we should still see only foo,
+        # even if we added a new component.
+        # We do this for testing, but in a real use case, we can't
+        # add new Component classes on the fly, and when we install
+        # new addons, the registry is rebuilt and cache cleared.
+        components = self.comp_registry.lookup("foobar")
+        self.assertEqual(["foo"], [c._name for c in components])
+
+        self.comp_registry._cache.clear()
+        # now we should find them both as the cache has been cleared
+        components = self.comp_registry.lookup("foobar")
+        self.assertEqual(["foo", "bar"], [c._name for c in components])
diff --git a/component/tests/test_utils.py b/component/tests/test_utils.py
new file mode 100644
index 000000000..f630a83e6
--- /dev/null
+++ b/component/tests/test_utils.py
@@ -0,0 +1,20 @@
+# Copyright 2023 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from unittest import mock
+
+from odoo.addons.component.utils import is_component_registry_ready
+
+from .common import TransactionComponentRegistryCase
+
+
+class TestUtils(TransactionComponentRegistryCase):
+    def test_registry_ready(self):
+        path = "odoo.addons.component.utils.get_component_registry"
+        with mock.patch(path) as mocked:
+            mocked.return_value = None
+            self.assertFalse(is_component_registry_ready(self.env.cr.dbname))
+            self._setup_registry(self)
+            mocked.return_value = self.comp_registry
+            self.assertTrue(is_component_registry_ready(self.env.cr.dbname))
+            self._teardown_registry(self)
diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py
new file mode 100644
index 000000000..2004ba7e5
--- /dev/null
+++ b/component/tests/test_work_on.py
@@ -0,0 +1,73 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo.addons.component.core import ComponentRegistry, WorkContext
+
+from .common import TransactionComponentRegistryCase
+
+
+class TestWorkOn(TransactionComponentRegistryCase):
+    """Test on WorkContext
+
+    This model is mostly a container, so we check the access
+    to the attributes and properties.
+
+    """
+
+    def setUp(self):
+        super().setUp()
+        self._setup_registry(self)
+
+    def tearDown(self):
+        self._teardown_registry(self)
+        super().tearDown()
+
+    def test_collection_work_on(self):
+        """Create a new instance and test attributes access"""
+        collection_record = self.collection.new()
+        with collection_record.work_on("res.partner") as work:
+            self.assertEqual(collection_record, work.collection)
+            self.assertEqual("collection.base", work.collection._name)
+            self.assertEqual("res.partner", work.model_name)
+            self.assertEqual(self.env["res.partner"], work.model)
+            self.assertEqual(self.env, work.env)
+
+    def test_collection_work_on_registry_via_context(self):
+        """Test propagation of registry via context"""
+        registry = ComponentRegistry()
+        collection_record = self.collection.with_context(
+            components_registry=registry
+        ).new()
+        with collection_record.work_on("res.partner") as work:
+            self.assertEqual(collection_record, work.collection)
+            self.assertEqual("collection.base", work.collection._name)
+            self.assertEqual("res.partner", work.model_name)
+            self.assertEqual(self.env["res.partner"], work.model)
+            self.assertEqual(work.env, collection_record.env)
+            self.assertEqual(work.components_registry, registry)
+
+    def test_propagate_work_on(self):
+        """Check custom attributes and their propagation"""
+        registry = ComponentRegistry()
+        work = WorkContext(
+            model_name="res.partner",
+            collection=self.collection,
+            # we can customize the lookup registry, but used mostly for tests
+            components_registry=registry,
+            # we can pass our own keyword args that will set as attributes
+            test_keyword="value",
+        )
+        self.assertIs(registry, work.components_registry)
+        # check that our custom keyword is set as attribute
+        self.assertEqual("value", work.test_keyword)
+
+        # when we want to work on another model, work_on() create
+        # another instance and propagate the attributes to it
+        work2 = work.work_on("res.users")
+        self.assertNotEqual(work, work2)
+        self.assertEqual(self.env, work2.env)
+        self.assertEqual(self.collection, work2.collection)
+        self.assertEqual("res.users", work2.model_name)
+        self.assertIs(registry, work2.components_registry)
+        # test_keyword has been propagated to the new WorkContext instance
+        self.assertEqual("value", work2.test_keyword)
diff --git a/component/utils.py b/component/utils.py
new file mode 100644
index 000000000..66e955125
--- /dev/null
+++ b/component/utils.py
@@ -0,0 +1,14 @@
+# Copyright 2023 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from .core import _component_databases
+
+
+def get_component_registry(dbname):
+    return _component_databases.get(dbname)
+
+
+def is_component_registry_ready(dbname):
+    """Return True if the registry is ready to be used."""
+    comp_registry = get_component_registry(dbname)
+    return comp_registry.ready if comp_registry else False
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 000000000..d3dfeea70
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+# generated from manifests external_dependencies
+cachetools
diff --git a/test_component/README.rst b/test_component/README.rst
new file mode 100644
index 000000000..c0de17b6c
--- /dev/null
+++ b/test_component/README.rst
@@ -0,0 +1,88 @@
+================
+Components Tests
+================
+
+.. 
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+   !! This file is generated by oca-gen-addon-readme !!
+   !! changes will be overwritten.                   !!
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+   !! source digest: sha256:e5242838a85bb1c42a72f2a385daa15a092c74b01c97229f90a291a2440090b0
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
+    :target: https://odoo-community.org/page/development-status
+    :alt: Production/Stable
+.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
+    :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
+    :alt: License: LGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github
+    :target: https://github.com/OCA/connector/tree/17.0/test_component
+    :alt: OCA/connector
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+    :target: https://translation.odoo-community.org/projects/connector-17-0/connector-17-0-test_component
+    :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/connector&target_branch=17.0
+    :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This addon is not meant to be installed, except for running the tests.
+It extends the Odoo Models in order to run automated tests on the
+Connector framework
+
+The basic tests are integrated within the ``component`` addon.
+
+**Table of contents**
+
+.. contents::
+   :local:
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues <https://github.com/OCA/connector/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 <https://github.com/OCA/connector/issues/new?body=module:%20test_component%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Camptocamp
+
+Contributors
+------------
+
+-  Guewen Baconnier (Camptocamp)
+
+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.
+
+.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px
+    :target: https://github.com/guewen
+    :alt: guewen
+
+Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
+
+|maintainer-guewen| 
+
+This module is part of the `OCA/connector <https://github.com/OCA/connector/tree/17.0/test_component>`_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/test_component/__init__.py b/test_component/__init__.py
new file mode 100644
index 000000000..0f00a6730
--- /dev/null
+++ b/test_component/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import components
diff --git a/test_component/__manifest__.py b/test_component/__manifest__.py
new file mode 100644
index 000000000..abe2867cc
--- /dev/null
+++ b/test_component/__manifest__.py
@@ -0,0 +1,17 @@
+# Copyright 2019 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+{
+    "name": "Components Tests",
+    "summary": "Automated tests for Components, do not install.",
+    "version": "17.0.1.0.0",
+    "author": "Camptocamp,Odoo Community Association (OCA)",
+    "license": "LGPL-3",
+    "category": "Hidden",
+    "depends": ["component"],
+    "website": "https://github.com/OCA/connector",
+    "data": ["security/ir.model.access.csv"],
+    "installable": True,
+    "development_status": "Production/Stable",
+    "maintainers": ["guewen"],
+}
diff --git a/test_component/components/__init__.py b/test_component/components/__init__.py
new file mode 100644
index 000000000..b7d85d947
--- /dev/null
+++ b/test_component/components/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from . import components
diff --git a/test_component/components/components.py b/test_component/components/components.py
new file mode 100644
index 000000000..7cfa0a4c4
--- /dev/null
+++ b/test_component/components/components.py
@@ -0,0 +1,35 @@
+# Copyright 2017 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo.addons.component.core import AbstractComponent, Component
+
+
+class BaseComponent(AbstractComponent):
+    _inherit = "base"
+
+    def test_inherit_base(self):
+        return "test_inherit_base"
+
+
+class Mapper(AbstractComponent):
+    _name = "mapper"
+
+    def test_inherit_component(self):
+        return "test_inherit_component"
+
+
+class ImportTestMapper(Component):
+    _name = "test.mapper"
+    _inherit = "mapper"
+    _usage = "import.mapper"
+    _collection = "test.component.collection"
+
+    def name(self):
+        return "test.mapper"
+
+
+class UserTestComponent(Component):
+    _name = "test.user.component"
+    _apply_on = ["res.users"]
+    _usage = "test1"
+    _collection = "test.component.collection"
diff --git a/test_component/i18n/am.po b/test_component/i18n/am.po
new file mode 100644
index 000000000..c4afe89b6
--- /dev/null
+++ b/test_component/i18n/am.po
@@ -0,0 +1,64 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Amharic (https://www.transifex.com/oca/teams/23907/am/)\n"
+"Language: am\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Creado por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Creado en"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Última actualización por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Última actualización en"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/ca.po b/test_component/i18n/ca.po
new file mode 100644
index 000000000..701b3810a
--- /dev/null
+++ b/test_component/i18n/ca.po
@@ -0,0 +1,64 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n"
+"Language: ca\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Creat per"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Creat a"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Darrear modificació per"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Darrera modificació el"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/de.po b/test_component/i18n/de.po
new file mode 100644
index 000000000..26759e22c
--- /dev/null
+++ b/test_component/i18n/de.po
@@ -0,0 +1,64 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: German (https://www.transifex.com/oca/teams/23907/de/)\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Angelegt durch"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Angelegt am"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "Anzeigebezeichnung"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "Zuletzt aktualisiert am"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Zuletzt aktualisiert durch"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Zuletzt aktualisiert am"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/el_GR.po b/test_component/i18n/el_GR.po
new file mode 100644
index 000000000..2b42cfef5
--- /dev/null
+++ b/test_component/i18n/el_GR.po
@@ -0,0 +1,65 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/"
+"el_GR/)\n"
+"Language: el_GR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Δημιουργήθηκε από "
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Δημιουργήθηκε στις"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "Κωδικός"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Τελευταία ενημέρωση από"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Τελευταία ενημέρωση στις"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/es.po b/test_component/i18n/es.po
new file mode 100644
index 000000000..ae4919555
--- /dev/null
+++ b/test_component/i18n/es.po
@@ -0,0 +1,65 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2023-08-02 13:09+0000\n"
+"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
+"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.17\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Creado por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Creado en"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "Nombre mostrado"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "Última modificación el"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Última actualización por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Última actualización el"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr "Nombre"
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr "Colección de componentes de prueba"
diff --git a/test_component/i18n/es_ES.po b/test_component/i18n/es_ES.po
new file mode 100644
index 000000000..f1c793a79
--- /dev/null
+++ b/test_component/i18n/es_ES.po
@@ -0,0 +1,65 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/"
+"es_ES/)\n"
+"Language: es_ES\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Creado por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Creado en"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Última actualización por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Última actualización en"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/fi.po b/test_component/i18n/fi.po
new file mode 100644
index 000000000..e11e3bddf
--- /dev/null
+++ b/test_component/i18n/fi.po
@@ -0,0 +1,64 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Finnish (https://www.transifex.com/oca/teams/23907/fi/)\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Luonut"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Luotu"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "Nimi"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "Viimeksi muokattu"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Viimeksi päivittänyt"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Viimeksi päivitetty"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/fr.po b/test_component/i18n/fr.po
new file mode 100644
index 000000000..9fb8b3875
--- /dev/null
+++ b/test_component/i18n/fr.po
@@ -0,0 +1,68 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+# Nicolas JEUDY <njeudy@panda-chi.io>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-02-01 01:48+0000\n"
+"PO-Revision-Date: 2018-02-01 01:48+0000\n"
+"Last-Translator: Nicolas JEUDY <njeudy@panda-chi.io>, 2018\n"
+"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Créé par"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Créé le"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "Nom affiché"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "Dernière modification le"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Dernière mise à jour par"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Dernière mise à jour le"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr "Nom"
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
+
+#~ msgid "test.component.collection"
+#~ msgstr "test.component.collection"
diff --git a/test_component/i18n/gl.po b/test_component/i18n/gl.po
new file mode 100644
index 000000000..9646523ee
--- /dev/null
+++ b/test_component/i18n/gl.po
@@ -0,0 +1,64 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n"
+"Language: gl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Creado por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Creado en"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "ültima actualización por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Última actualización en"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/it.po b/test_component/i18n/it.po
new file mode 100644
index 000000000..427da2d75
--- /dev/null
+++ b/test_component/i18n/it.po
@@ -0,0 +1,65 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2023-09-22 16:38+0000\n"
+"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
+"Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.17\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Creato da"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Creato il"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "Nome visualizzato"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "Ultima modifica il"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Ultimo aggiornamento di"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Ultimo aggiornamento il"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr "Nome"
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr "Test collezone componente"
diff --git a/test_component/i18n/pt.po b/test_component/i18n/pt.po
new file mode 100644
index 000000000..3d6eea655
--- /dev/null
+++ b/test_component/i18n/pt.po
@@ -0,0 +1,64 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Criado por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Criado em"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Atualizado pela última vez por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Atualizado pela última vez em"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/pt_BR.po b/test_component/i18n/pt_BR.po
new file mode 100644
index 000000000..23294c0fa
--- /dev/null
+++ b/test_component/i18n/pt_BR.po
@@ -0,0 +1,69 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2019-08-26 15:01+0000\n"
+"Last-Translator: Rodrigo Macedo <rmsolucoeseminformatic4@gmail.com>\n"
+"Language-Team: Portuguese (Brazil) (https://www.transifex.com/oca/"
+"teams/23907/pt_BR/)\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Criado por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Criado em "
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "Exibir Nome"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "Última modificação no"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Última atualização por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Última atualização em "
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr "Nome"
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
+
+#~ msgid "test.component.collection"
+#~ msgstr "test.component.collection"
diff --git a/test_component/i18n/pt_PT.po b/test_component/i18n/pt_PT.po
new file mode 100644
index 000000000..f064aa2fe
--- /dev/null
+++ b/test_component/i18n/pt_PT.po
@@ -0,0 +1,65 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/"
+"teams/23907/pt_PT/)\n"
+"Language: pt_PT\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Criado por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Criado em"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Atualizado pela última vez por"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Atualizado pela última vez em"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/sl.po b/test_component/i18n/sl.po
new file mode 100644
index 000000000..e95b52222
--- /dev/null
+++ b/test_component/i18n/sl.po
@@ -0,0 +1,65 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n"
+"Language: sl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
+"%100==4 ? 2 : 3);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Ustvaril"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Ustvarjeno"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "Prikazni naziv"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "Zadnjič spremenjeno"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Zadnji posodobil"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Zadnjič posodobljeno"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/test_component.pot b/test_component/i18n/test_component.pot
new file mode 100644
index 000000000..e45992c8c
--- /dev/null
+++ b/test_component/i18n/test_component.pot
@@ -0,0 +1,59 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* test_component
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/tr.po b/test_component/i18n/tr.po
new file mode 100644
index 000000000..2dea99fd4
--- /dev/null
+++ b/test_component/i18n/tr.po
@@ -0,0 +1,64 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * test_component
+#
+# Translators:
+# OCA Transbot <transbot@odoo-community.org>, 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 13.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-05 16:56+0000\n"
+"PO-Revision-Date: 2018-01-05 16:56+0000\n"
+"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2018\n"
+"Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "Oluşturan"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "Oluşturuldu"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "Son güncelleyen"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "Son güncelleme"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr ""
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
diff --git a/test_component/i18n/zh_CN.po b/test_component/i18n/zh_CN.po
new file mode 100644
index 000000000..bc4613568
--- /dev/null
+++ b/test_component/i18n/zh_CN.po
@@ -0,0 +1,65 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* test_component
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 13.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2019-09-01 06:14+0000\n"
+"Last-Translator: 黎伟杰 <674416404@qq.com>\n"
+"Language-Team: none\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_uid
+msgid "Created by"
+msgstr "创建者"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__create_date
+msgid "Created on"
+msgstr "创建时间"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__display_name
+msgid "Display Name"
+msgstr "显示名称"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__id
+msgid "ID"
+msgstr "ID"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection____last_update
+msgid "Last Modified on"
+msgstr "最后修改时间"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_uid
+msgid "Last Updated by"
+msgstr "最后更新者"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__write_date
+msgid "Last Updated on"
+msgstr "最后更新时间"
+
+#. module: test_component
+#: model:ir.model.fields,field_description:test_component.field_test_component_collection__name
+msgid "Name"
+msgstr "名称"
+
+#. module: test_component
+#: model:ir.model,name:test_component.model_test_component_collection
+msgid "Test Component Collection"
+msgstr ""
+
+#~ msgid "test.component.collection"
+#~ msgstr "test.component.collection"
diff --git a/test_component/models/__init__.py b/test_component/models/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/test_component/models/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/test_component/models/models.py b/test_component/models/models.py
new file mode 100644
index 000000000..e0b94335f
--- /dev/null
+++ b/test_component/models/models.py
@@ -0,0 +1,12 @@
+# Copyright 2016 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo import fields, models
+
+
+class TestComponentCollection(models.Model):
+    _name = "test.component.collection"
+    _description = "Test Component Collection"
+    _inherit = ["collection.base"]
+
+    name = fields.Char()
diff --git a/test_component/pyproject.toml b/test_component/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/test_component/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/test_component/readme/CONTRIBUTORS.md b/test_component/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..16f2383d6
--- /dev/null
+++ b/test_component/readme/CONTRIBUTORS.md
@@ -0,0 +1 @@
+- Guewen Baconnier (Camptocamp)
diff --git a/test_component/readme/DESCRIPTION.md b/test_component/readme/DESCRIPTION.md
new file mode 100644
index 000000000..e3b5adab0
--- /dev/null
+++ b/test_component/readme/DESCRIPTION.md
@@ -0,0 +1,5 @@
+This addon is not meant to be installed, except for running the tests.
+It extends the Odoo Models in order to run automated tests on the
+Connector framework
+
+The basic tests are integrated within the `component` addon.
diff --git a/test_component/security/ir.model.access.csv b/test_component/security/ir.model.access.csv
new file mode 100644
index 000000000..374136bee
--- /dev/null
+++ b/test_component/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_test_component_collection,access_test_component_collection,model_test_component_collection,,1,1,1,1
diff --git a/test_component/static/description/icon.png b/test_component/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/test_component/static/description/icon.png differ
diff --git a/test_component/static/description/index.html b/test_component/static/description/index.html
new file mode 100644
index 000000000..d44d93424
--- /dev/null
+++ b/test_component/static/description/index.html
@@ -0,0 +1,426 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
+<title>Components Tests</title>
+<style type="text/css">
+
+/*
+:Author: David Goodger (goodger@python.org)
+:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
+:Copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the HTML output of Docutils.
+
+See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
+customize this style sheet.
+*/
+
+/* used to remove borders from tables and images */
+.borderless, table.borderless td, table.borderless th {
+  border: 0 }
+
+table.borderless td, table.borderless th {
+  /* Override padding for "table.docutils td" with "! important".
+     The right padding separates the table cells. */
+  padding: 0 0.5em 0 0 ! important }
+
+.first {
+  /* Override more specific margin styles with "! important". */
+  margin-top: 0 ! important }
+
+.last, .with-subtitle {
+  margin-bottom: 0 ! important }
+
+.hidden {
+  display: none }
+
+.subscript {
+  vertical-align: sub;
+  font-size: smaller }
+
+.superscript {
+  vertical-align: super;
+  font-size: smaller }
+
+a.toc-backref {
+  text-decoration: none ;
+  color: black }
+
+blockquote.epigraph {
+  margin: 2em 5em ; }
+
+dl.docutils dd {
+  margin-bottom: 0.5em }
+
+object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
+  overflow: hidden;
+}
+
+/* Uncomment (and remove this text!) to get bold-faced definition list terms
+dl.docutils dt {
+  font-weight: bold }
+*/
+
+div.abstract {
+  margin: 2em 5em }
+
+div.abstract p.topic-title {
+  font-weight: bold ;
+  text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+  margin: 2em ;
+  border: medium outset ;
+  padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+  font-weight: bold ;
+  font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title, .code .error {
+  color: red ;
+  font-weight: bold ;
+  font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+   compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+  margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+  margin-top: 0.5em }
+*/
+
+div.dedication {
+  margin: 2em 5em ;
+  text-align: center ;
+  font-style: italic }
+
+div.dedication p.topic-title {
+  font-weight: bold ;
+  font-style: normal }
+
+div.figure {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+div.footer, div.header {
+  clear: both;
+  font-size: smaller }
+
+div.line-block {
+  display: block ;
+  margin-top: 1em ;
+  margin-bottom: 1em }
+
+div.line-block div.line-block {
+  margin-top: 0 ;
+  margin-bottom: 0 ;
+  margin-left: 1.5em }
+
+div.sidebar {
+  margin: 0 0 0.5em 1em ;
+  border: medium outset ;
+  padding: 1em ;
+  background-color: #ffffee ;
+  width: 40% ;
+  float: right ;
+  clear: right }
+
+div.sidebar p.rubric {
+  font-family: sans-serif ;
+  font-size: medium }
+
+div.system-messages {
+  margin: 5em }
+
+div.system-messages h1 {
+  color: red }
+
+div.system-message {
+  border: medium outset ;
+  padding: 1em }
+
+div.system-message p.system-message-title {
+  color: red ;
+  font-weight: bold }
+
+div.topic {
+  margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+  margin-top: 0.4em }
+
+h1.title {
+  text-align: center }
+
+h2.subtitle {
+  text-align: center }
+
+hr.docutils {
+  width: 75% }
+
+img.align-left, .figure.align-left, object.align-left, table.align-left {
+  clear: left ;
+  float: left ;
+  margin-right: 1em }
+
+img.align-right, .figure.align-right, object.align-right, table.align-right {
+  clear: right ;
+  float: right ;
+  margin-left: 1em }
+
+img.align-center, .figure.align-center, object.align-center {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+table.align-center {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.align-left {
+  text-align: left }
+
+.align-center {
+  clear: both ;
+  text-align: center }
+
+.align-right {
+  text-align: right }
+
+/* reset inner alignment in figures */
+div.align-right {
+  text-align: inherit }
+
+/* div.align-center * { */
+/*   text-align: left } */
+
+.align-top    {
+  vertical-align: top }
+
+.align-middle {
+  vertical-align: middle }
+
+.align-bottom {
+  vertical-align: bottom }
+
+ol.simple, ul.simple {
+  margin-bottom: 1em }
+
+ol.arabic {
+  list-style: decimal }
+
+ol.loweralpha {
+  list-style: lower-alpha }
+
+ol.upperalpha {
+  list-style: upper-alpha }
+
+ol.lowerroman {
+  list-style: lower-roman }
+
+ol.upperroman {
+  list-style: upper-roman }
+
+p.attribution {
+  text-align: right ;
+  margin-left: 50% }
+
+p.caption {
+  font-style: italic }
+
+p.credits {
+  font-style: italic ;
+  font-size: smaller }
+
+p.label {
+  white-space: nowrap }
+
+p.rubric {
+  font-weight: bold ;
+  font-size: larger ;
+  color: maroon ;
+  text-align: center }
+
+p.sidebar-title {
+  font-family: sans-serif ;
+  font-weight: bold ;
+  font-size: larger }
+
+p.sidebar-subtitle {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+p.topic-title {
+  font-weight: bold }
+
+pre.address {
+  margin-bottom: 0 ;
+  margin-top: 0 ;
+  font: inherit }
+
+pre.literal-block, pre.doctest-block, pre.math, pre.code {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+pre.code .ln { color: grey; } /* line numbers */
+pre.code, code { background-color: #eeeeee }
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+span.classifier {
+  font-family: sans-serif ;
+  font-style: oblique }
+
+span.classifier-delimiter {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+span.interpreted {
+  font-family: sans-serif }
+
+span.option {
+  white-space: nowrap }
+
+span.pre {
+  white-space: pre }
+
+span.problematic {
+  color: red }
+
+span.section-subtitle {
+  /* font-size relative to parent (h1..h6 element) */
+  font-size: 80% }
+
+table.citation {
+  border-left: solid 1px gray;
+  margin-left: 1px }
+
+table.docinfo {
+  margin: 2em 4em }
+
+table.docutils {
+  margin-top: 0.5em ;
+  margin-bottom: 0.5em }
+
+table.footnote {
+  border-left: solid 1px black;
+  margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+  padding-left: 0.5em ;
+  padding-right: 0.5em ;
+  vertical-align: top }
+
+table.docutils th.field-name, table.docinfo th.docinfo-name {
+  font-weight: bold ;
+  text-align: left ;
+  white-space: nowrap ;
+  padding-left: 0 }
+
+/* "booktabs" style (no vertical lines) */
+table.docutils.booktabs {
+  border: 0px;
+  border-top: 2px solid;
+  border-bottom: 2px solid;
+  border-collapse: collapse;
+}
+table.docutils.booktabs * {
+  border: 0px;
+}
+table.docutils.booktabs th {
+  border-bottom: thin solid;
+  text-align: left;
+}
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+  font-size: 100% }
+
+ul.auto-toc {
+  list-style-type: none }
+
+</style>
+</head>
+<body>
+<div class="document" id="components-tests">
+<h1 class="title">Components Tests</h1>
+
+<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!! This file is generated by oca-gen-addon-readme !!
+!! changes will be overwritten.                   !!
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!! source digest: sha256:e5242838a85bb1c42a72f2a385daa15a092c74b01c97229f90a291a2440090b0
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
+<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/connector/tree/17.0/test_component"><img alt="OCA/connector" src="https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/connector-17-0/connector-17-0-test_component"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/connector&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
+<p>This addon is not meant to be installed, except for running the tests.
+It extends the Odoo Models in order to run automated tests on the
+Connector framework</p>
+<p>The basic tests are integrated within the <tt class="docutils literal">component</tt> addon.</p>
+<p><strong>Table of contents</strong></p>
+<div class="contents local topic" id="contents">
+<ul class="simple">
+<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
+<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
+<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
+<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
+<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
+</ul>
+</li>
+</ul>
+</div>
+<div class="section" id="bug-tracker">
+<h1><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h1>
+<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/connector/issues">GitHub Issues</a>.
+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
+<a class="reference external" href="https://github.com/OCA/connector/issues/new?body=module:%20test_component%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
+<p>Do not contact contributors directly about support or help with technical issues.</p>
+</div>
+<div class="section" id="credits">
+<h1><a class="toc-backref" href="#toc-entry-2">Credits</a></h1>
+<div class="section" id="authors">
+<h2><a class="toc-backref" href="#toc-entry-3">Authors</a></h2>
+<ul class="simple">
+<li>Camptocamp</li>
+</ul>
+</div>
+<div class="section" id="contributors">
+<h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
+<ul class="simple">
+<li>Guewen Baconnier (Camptocamp)</li>
+</ul>
+</div>
+<div class="section" id="maintainers">
+<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
+<p>This module is maintained by the OCA.</p>
+<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
+<p>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.</p>
+<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
+<p><a class="reference external image-reference" href="https://github.com/guewen"><img alt="guewen" src="https://github.com/guewen.png?size=40px" /></a></p>
+<p>This module is part of the <a class="reference external" href="https://github.com/OCA/connector/tree/17.0/test_component">OCA/connector</a> project on GitHub.</p>
+<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
+</div>
+</div>
+</div>
+</body>
+</html>
diff --git a/test_component/tests/__init__.py b/test_component/tests/__init__.py
new file mode 100644
index 000000000..8c49d833d
--- /dev/null
+++ b/test_component/tests/__init__.py
@@ -0,0 +1,2 @@
+from . import test_components
+from . import test_component_collection
diff --git a/test_component/tests/test_component_collection.py b/test_component/tests/test_component_collection.py
new file mode 100644
index 000000000..5cb985a61
--- /dev/null
+++ b/test_component/tests/test_component_collection.py
@@ -0,0 +1,24 @@
+# Copyright 2013-2019 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo.addons.component.tests.common import TransactionComponentCase
+from odoo.addons.test_component.components.components import UserTestComponent
+
+
+class TestComponentCollection(TransactionComponentCase):
+    def setUp(self):
+        super().setUp()
+        self.collection = self.env["test.component.collection"].create({"name": "Test"})
+
+    def tearDown(self):
+        super().tearDown()
+
+    def test_component_by_name(self):
+        with self.collection.work_on("res.users") as work:
+            component = work.component_by_name(name="test.user.component")
+            self.assertEqual(UserTestComponent._name, component._name)
+
+    def test_components_usage(self):
+        with self.collection.work_on("res.users") as work:
+            component = work.component(usage="test1")
+            self.assertEqual(UserTestComponent._name, component._name)
diff --git a/test_component/tests/test_components.py b/test_component/tests/test_components.py
new file mode 100644
index 000000000..20b21c0ca
--- /dev/null
+++ b/test_component/tests/test_components.py
@@ -0,0 +1,30 @@
+# Copyright 2019 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo.addons.component.tests.common import TransactionComponentCase
+
+
+class TestComponentInheritance(TransactionComponentCase):
+    def setUp(self):
+        super().setUp()
+        self.collection = self.env["test.component.collection"].create({"name": "Test"})
+
+    def test_inherit_base(self):
+        with self.collection.work_on("res.users") as work:
+            component = work.component_by_name("base")
+            self.assertEqual("test_inherit_base", component.test_inherit_base())
+
+    def test_inherit_component(self):
+        with self.collection.work_on("res.users") as work:
+            component = work.component_by_name("mapper")
+            self.assertEqual(
+                "test_inherit_component", component.test_inherit_component()
+            )
+
+    def test_inherit_prototype_component(self):
+        with self.collection.work_on("res.users") as work:
+            component = work.component_by_name("test.mapper")
+            self.assertEqual(
+                "test_inherit_component", component.test_inherit_component()
+            )
+            self.assertEqual("test.mapper", component.name())