Skip to content

[19.0][MIG] Port connector#528

Closed
YoussefEgla wants to merge 1 commit into
OCA:19.0from
YoussefEgla:19.0-port-component-stack
Closed

[19.0][MIG] Port connector#528
YoussefEgla wants to merge 1 commit into
OCA:19.0from
YoussefEgla:19.0-port-component-stack

Conversation

@YoussefEgla
Copy link
Copy Markdown

This PR ports the component, component_event, and connector framework addons from 18.0 to 19.0.

To keep review manageable, the history is intentionally split in 2 commits:

  1. Copy component framework addons from 18.0
  2. Apply Odoo 19 porting changes

This allows reviewers to inspect the first commit as a straight copy and focus on the second commit for the actual 19.0 delta.

What was changed

component

The component addon needed one real runtime adaptation for Odoo 19 and a test compatibility fix:

  • Replaced the removed Odoo module graph API with the Odoo 19 ModuleGraph API in the component builder.
  • Updated test setup so the injected component registry is propagated through an Odoo 19-safe environment rebinding flow.
  • Rebound cached recordsets used by tests after environment rebinding so env identity and registry lookup remain consistent in Odoo 19.
  • Updated addon metadata to 19.0.
  • Updated translation template metadata to 19.0.
  • Synced generated contributor documentation.

component_event

The component_event addon did not require functional runtime changes, but it needed Odoo 19 compatibility updates in metadata and tests:

  • Updated addon metadata to 19.0.
  • Updated translation template metadata to 19.0.
  • Reworked the lightweight test case to use the Odoo 19 test base class expected by the test loader.
  • Removed the old dependency on MetaCase, which is no longer available in Odoo 19.
  • Synced generated contributor documentation.

Runtime behavior of the addon itself remains the same: event collection, listener dispatch, and base model hooks continue to work unchanged on top of the ported component addon.

connector

The connector addon required a small but important Odoo 19 data-model update plus test adjustments:

  • Updated addon metadata to 19.0.
  • Updated translation template metadata to 19.0.
  • Adapted the connector listener test to Odoo 19-safe environment rebinding instead of direct env.context mutation.
  • Updated security data to match the Odoo 19 res.groups model:
    • create a res.groups.privilege
    • assign groups through privilege_id
    • replace legacy users with user_ids
  • Synced generated contributor documentation.

Why these changes are needed

This port mainly addresses three Odoo 19 compatibility areas:

  1. Internal registry and build API changes in the component framework.
  2. Odoo 19 test framework changes:
    • environment objects are no longer safely mutable the old way
    • the old MetaCase pattern is no longer available
    • test loader metadata is expected from Odoo test base classes
  3. Security model changes in res.groups, which now rely on privileges instead of the old category-based setup used in addon XML.

Functional scope

These addons are framework addons rather than business-feature addons, so validation focused on the framework behaviors they provide:

  • component registry initialization
  • component lookup and work_on(...) behavior
  • event collection and dispatch
  • connector locking behavior
  • queue job integration hooks
  • connector listener skip behavior

No migration scripts were needed because the port is focused on framework compatibility and install and runtime behavior on Odoo 19.

Validation performed

component

Manual Odoo shell checks passed for the component framework after installation, including basic registry, build, and component lookup behavior.

component_event

Manual Odoo shell smoke test passed.

Shell snippet used:

from uuid import uuid4

from odoo.addons.component.core import Component, ComponentRegistry, _component_databases

mod = env["ir.module.module"].search([("name", "=", "component_event")], limit=1)
assert mod, "component_event module not found"
assert mod.state == "installed", f"component_event state is {mod.state!r}"

live_registry = _component_databases.get(env.cr.dbname)
assert live_registry, "No live component registry for this database"
assert live_registry.ready, "Live component registry is not ready"
assert "base.event.collecter" in live_registry, "base.event.collecter is missing from live registry"

print("module:", mod.name, mod.state, mod.latest_version)
print("live registry ready:", live_registry.ready)
print("collector present:", "base.event.collecter" in live_registry)

smoke_registry = ComponentRegistry()
smoke_registry.load_components("component")
smoke_registry.load_components("component_event")
smoke_registry.ready = True

seen = []
listener_name = f"smoke.event.listener.{uuid4().hex}"

class SmokeEventListener(Component):
  _name = listener_name
  _inherit = "base.event.listener"
  _apply_on = ["res.partner"]

  def on_smoke_ping(self, record, payload=None):
    seen.append(("ping", record._name, record.id, payload))

  def on_record_create(self, record, fields=None):
    seen.append(("create", record._name, record.id, tuple(sorted(fields or ()))))

  def on_record_write(self, record, fields=None):
    seen.append(("write", record._name, record.id, tuple(sorted(fields or ()))))

  def on_record_unlink(self, record):
    seen.append(("unlink", record._name, record.id))

SmokeEventListener._build_component(smoke_registry)
smoke_registry._cache.clear()
smoke_registry["base.event.collecter"]._cache.clear()

env2 = env(context=dict(env.context, components_registry=smoke_registry))
partner = env2["res.partner"].create({"name": "component_event smoke"})
partner._event("on_smoke_ping").notify(partner, payload={"ok": True})
partner.write({"comment": "component_event smoke"})
partner.unlink()

print("events seen:", seen)

assert any(event[0] == "create" for event in seen), "create event did not fire"
assert any(event[0] == "ping" for event in seen), "custom event did not fire"
assert any(event[0] == "write" for event in seen), "write event did not fire"
assert any(event[0] == "unlink" for event in seen), "unlink event did not fire"

env.cr.rollback()
print("component_event smoke test passed")

Observed output:

module: component_event installed 19.0.1.0.0
live registry ready: True
collector present: True
2026-04-17 19:42:58,885 85 INFO jb-web-feat-connector-framework-31062018 odoo.models.unlink: User #1 deleted mail.message records with IDs: [621]
2026-04-17 19:42:58,908 85 INFO jb-web-feat-connector-framework-31062018 odoo.models.unlink: User #1 deleted res.partner records with IDs: [57]
events seen: [('create', 'res.partner', 57, ('name', 'properties_base_definition_id')), ('ping', 'res.partner', 57, {'ok': True}), ('write', 'res.partner', 57, ('comment',)), ('unlink', 'res.partner', 57)]
component_event smoke test passed

This validated:

  • successful module installation
  • component event collector registration
  • custom event dispatch through _event(...).notify(...)
  • automatic on_record_create
  • automatic on_record_write
  • automatic on_record_unlink

connector

Manual Odoo shell smoke test passed.

Shell snippet used:

from uuid import uuid4
from unittest import mock

from odoo import api
from odoo.modules.registry import Registry

from odoo.addons.component.core import (
  Component,
  ComponentRegistry,
  WorkContext,
  _component_databases,
)
from odoo.addons.component_event.components.event import skip_if
from odoo.addons.component_event.core import EventWorkContext
from odoo.addons.queue_job.exception import RetryableJobError

mod = env["ir.module.module"].search([("name", "=", "connector")], limit=1)
assert mod, "connector module not found"
assert mod.state == "installed", f"connector state is {mod.state!r}"

live_registry = _component_databases.get(env.cr.dbname)
assert live_registry, "No live component registry for this database"
assert live_registry.ready, "Live component registry is not ready"

required_components = [
  "base.connector",
  "base.backend.adapter",
  "base.binder",
  "base.connector.listener",
  "base.record.locker",
  "base.synchronizer",
  "base.importer",
  "base.exporter",
  "base.deleter",
]
missing = [name for name in required_components if name not in live_registry]
assert not missing, f"Missing connector components: {missing}"

assert hasattr(env["queue.job"], "related_action_unwrap_binding"), "queue.job connector extension missing"
env["connector.backend"]
env["external.binding"]

print("module:", mod.name, mod.state, mod.latest_version)
print("live registry ready:", live_registry.ready)
print("required components present:", len(required_components))
print("queue.job extension present:", hasattr(env["queue.job"], "related_action_unwrap_binding"))

smoke_registry = ComponentRegistry()
for module_name in ("component", "component_event", "connector"):
  smoke_registry.load_components(module_name)
smoke_registry.ready = True

db_registry = Registry(env.cr.dbname)
cr1 = db_registry.cursor()
cr2 = db_registry.cursor()

try:
  env1 = api.Environment(cr1, env.uid, {"components_registry": smoke_registry})
  env2 = api.Environment(cr2, env.uid, {"components_registry": smoke_registry})

  backend1 = mock.MagicMock(name="backend1")
  backend1.env = env1
  backend2 = mock.MagicMock(name="backend2")
  backend2.env = env2

  work1 = WorkContext(
    model_name="res.partner",
    collection=backend1,
    components_registry=smoke_registry,
  )
  work2 = WorkContext(
    model_name="res.partner",
    collection=backend2,
    components_registry=smoke_registry,
  )

  base_connector1 = work1.component_by_name("base.connector")
  base_connector2 = work2.component_by_name("base.connector")
  locker1 = work1.component("record.locker")
  locker2 = work2.component("record.locker")

  assert base_connector1._name == "base.connector"
  assert locker1._name == "base.record.locker"
  print("isolated registry component lookup: ok")

  advisory_lock_name = f"connector-smoke-advisory-{uuid4().hex}"
  base_connector1.advisory_lock_or_retry(advisory_lock_name)
  try:
    base_connector2.advisory_lock_or_retry(advisory_lock_name, retry_seconds=3)
  except RetryableJobError:
    print("advisory lock retry path: ok")
  else:
    raise AssertionError("advisory lock retry path failed")

  partner1 = env1.ref("base.main_partner")
  partner2 = env2.ref("base.main_partner")
  locker1.lock(partner1)
  try:
    locker2.lock(partner2)
  except RetryableJobError:
    print("record locker retry path: ok")
  else:
    raise AssertionError("record locker retry path failed")

  seen = []
  suffix = uuid4().hex

  class PlainSmokeListener(Component):
    _name = f"smoke.connector.listener.plain.{suffix}"
    _inherit = "base.event.listener"
    _apply_on = ["res.partner"]

    def on_record_create(self, record, fields=None):
      seen.append(("plain", record.id, tuple(sorted(fields or ()))))

  class SkipSmokeListener(Component):
    _name = f"smoke.connector.listener.skip.{suffix}"
    _inherit = "base.connector.listener"
    _apply_on = ["res.partner"]

    @skip_if(lambda self, record, fields=None: self.no_connector_export(record))
    def on_record_create(self, record, fields=None):
      seen.append(("connector", record.id, tuple(sorted(fields or ()))))

  PlainSmokeListener._build_component(smoke_registry)
  SkipSmokeListener._build_component(smoke_registry)
  smoke_registry._cache.clear()
  smoke_registry["base.event.collecter"]._cache.clear()

  env1_flag = env1(
    context=dict(
      env1.context,
      components_registry=smoke_registry,
      no_connector_export=True,
    )
  )
  partner_flag = env1_flag["res.partner"].browse(partner1.id)
  event_work = EventWorkContext(
    model_name="res.partner",
    env=env1_flag,
    components_registry=smoke_registry,
  )
  collecter = smoke_registry["base.event.collecter"](event_work)
  collecter.collect_events("on_record_create").notify(partner_flag, fields=["name"])

  print("listener events seen:", seen)
  assert any(item[0] == "plain" for item in seen), seen
  assert not any(item[0] == "connector" for item in seen), seen

  print("connector smoke test passed")
finally:
  cr1.rollback()
  cr1.close()
  cr2.rollback()
  cr2.close()

Observed output:

module: connector installed 19.0.1.0.0
live registry ready: True
required components present: 9
queue.job extension present: True
isolated registry component lookup: ok
advisory lock retry path: ok
2026-04-17 19:47:15,292 85 INFO jb-web-feat-connector-framework-31062018 odoo.addons.connector.components.locker: A concurrent job is already working on the same record (res.partner with one id in (1,)). Job delayed later.
record locker retry path: ok
listener events seen: [('plain', 1, ('name',))]
connector smoke test passed

This validated:

  • successful module installation
  • live component registry readiness
  • registration of the expected connector framework components
  • presence of the queue.job extension
  • advisory lock retry behavior
  • record locking retry behavior
  • connector listener integration with component_event
  • correct skip behavior when no_connector_export is set in context

Note: the info log produced by the record locker during the smoke test is expected, because the test intentionally exercises the retry path for concurrent locking.

Notes for reviewers

  • The review is easier if you inspect the second commit on top of the first one.
  • The functional 19.0 porting work is intentionally small and focused.
  • Most README and static description changes are contributor-sync updates rather than functional changes.
Screenshot 2026-04-17 214431 Screenshot 2026-04-17 214814

@OCA-git-bot OCA-git-bot added mod:connector Module connector mod:component Module component mod:component_event Module component_event series:19.0 labels Apr 17, 2026
@YoussefEgla YoussefEgla force-pushed the 19.0-port-component-stack branch 2 times, most recently from 845a81a to 77d6f2e Compare April 17, 2026 20:17
@YoussefEgla YoussefEgla marked this pull request as draft April 20, 2026 13:53
@YoussefEgla YoussefEgla force-pushed the 19.0-port-component-stack branch 2 times, most recently from 10b46fc to 436b157 Compare April 20, 2026 14:03
@OCA-git-bot OCA-git-bot removed mod:component Module component mod:component_event Module component_event labels Apr 20, 2026
@YoussefEgla YoussefEgla force-pushed the 19.0-port-component-stack branch from 436b157 to eeba0fb Compare April 20, 2026 14:06
@YoussefEgla YoussefEgla force-pushed the 19.0-port-component-stack branch from eeba0fb to 1f1c5c8 Compare April 20, 2026 14:10
@simahawk
Copy link
Copy Markdown
Contributor

Thanks for your contrib but component modules are already migrated and connector is pending here #511 since a while. You could help w/ a review if you want ;)

@simahawk simahawk closed this Apr 20, 2026
@YoussefEgla YoussefEgla changed the title [19.0][MIG] Port component, component_event, and connector [19.0][MIG] Port connector Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants