Skip to content

ADR 0003: V3 Association-Model Migration Pattern

Status

Accepted

Date

2026-04-23

Context

dirac-odoo already established progressive SA design layers:

  • V0 introduced basic {actor, sa} stamping for simple PA work surfaces
  • V1 introduced hierarchy and roll-up
  • V2 expanded Odoo object visibility and affiliation for durable objects

That earlier progression clarified the problem space, but it also left a design shortcut in place: some visibility scope was modeled by stamping native Odoo records directly with SA governance fields.

That shortcut is no longer the preferred direction for future visibility-scope design.

For many Odoo objects, the visibility relationship itself has meaning:

  • it may represent assignment, access, or binding
  • it may carry lifecycle and audit data
  • it may need exclusivity or reassignment rules
  • it may change over time without changing the native business object

When those concerns are pushed directly into native Odoo records as stamps, the governance relationship becomes harder to inspect, migrate, and reuse.

The repo therefore needs a new refinement layer that makes the relationship a first-class model.

Decision

1. V3 adopts the association-model migration pattern as the default for new visibility scope

For any new SA-governed visibility scope, the default design pattern is an explicit association model.

The association model is the governance record that bridges:

  • an SA governance object or SA-governed operating context
  • and a native Odoo business object

The native Odoo object remains the business object of record.

The association model becomes the governance object of record for visibility, assignment, access, binding, lifecycle, and audit semantics.

2. New visibility scope should not be introduced by direct native stamping unless a strong exception is documented

For new work, do not start by adding direct sa, actor, or similar governance-stamp fields onto a native Odoo model just to create PA visibility scope.

Use direct native stamping only if a later, explicit exception is justified and documented.

The burden of proof is on the exception, not on the association model.

3. Existing stamp-based visibility models should be refactored toward association models

Past models that currently express SA visibility by direct stamping should be treated as legacy shapes.

They should be refactored toward explicit association models so the governance relationship becomes:

  • inspectable
  • auditable
  • historically traceable
  • easier to reassign or expire
  • less intrusive to native Odoo semantics

This ADR does not require unsafe big-bang replacement.

It does require that legacy stamp-based models be placed on a migration path toward association-based governance.

4. The association type must be explicit in association metadata

The relationship carried by the association model must declare its real semantics explicitly.

Use explicit association-kind metadata such as:

  • assignment
  • access
  • binding

Preferred pattern:

  • use a stable association model for the governed object class
  • store the association kind in a field on that model rather than encoding it into the model name

Examples:

  • ov.sa_sale_order
  • ov.sa_customer
  • ov.sa_location

Typical metadata field examples:

  • association_kind
  • restriction_type
  • another equivalently explicit field name agreed by implementation

This allows:

  • stable model naming
  • easier post-creation evolution of association semantics through lifecycle
  • less model proliferation for closely related governance patterns

The association kind still remains a governed design choice.

It must be:

  • explicit
  • auditable
  • validated by policy and lifecycle rules

Changing association kind over time is allowed only as an intentional governed state transition, not as an informal convenience edit.

5. Two-layer association design is preferred when SA scope and actor scope have different meanings

When a governed object needs both:

  • SA-level organizational scope
  • and actor-level handling inside that scope

model them as two different association layers rather than collapsing both meanings into one row.

The two layers have different semantics:

  • the SA association defines that the object is in scope for the SA at all
  • the actor association defines which actor may handle, access, or own work inside that already valid SA scope

The actor layer must not bypass or replace the SA layer.

6. Actor reach must be a subset of SA reach

An actor inside an SA must never gain broader governed reach than the SA of which that actor is a member.

The preferred implementation pattern is therefore:

  • ov.sa_x
  • bridges SA to native Odoo object x
  • carries explicit metadata such as association_kind
  • ov.actor_sa_x
  • bridges an actor to the ov.sa_x row, not directly to native Odoo object x
  • may also carry narrower actor-level association metadata when needed

This makes the subset rule structural instead of merely procedural.

If actor association attaches only to an active SA-association row, then the actor can never be granted scope outside the already established SA scope.

7. SA association is the governing container for actor association

In this two-layer pattern, the SA-association object is itself the governed container for downstream actor association.

That means:

  • the SA association carries the organizational inclusion decision
  • the SA association carries the primary governance metadata
  • the actor association carries narrower operational metadata for one actor inside that scope

Examples of SA-level metadata that actor-level association must remain subject to include:

  • state
  • effective dates
  • entitlement or access class
  • exclusivity rules
  • deletion semantics for referenced targets
  • archive or expiry state

If the parent SA-association row is expired, revoked, or otherwise inactive, child actor associations must not continue to extend effective reach beyond it.

8. Referenced-target deletion semantics must be explicit

Every association model must state what happens if its referenced target object is deleted.

This is a referential-integrity rule, not an excuse to modify native Odoo class x or add an inverse relationship on x.

Preferred terminology:

  • dependent association
  • the association row must not outlive the referenced target
  • if the target is deleted, the association should also be deleted
  • optional association
  • the association may survive target deletion only if the reference is intentionally nullable and is set to NULL
  • restricted association
  • deletion of the referenced target is blocked while the association exists

These semantics should be declared intentionally for each association type.

Do not leave deletion behavior implicit.

When the design uses two layers, deletion behavior should be coherent across the chain:

  • actor-to-SA-association rows should normally be dependent on the parent SA-association row
  • SA-to-native-object rows should explicitly declare whether they are dependent, optional, or restricted relative to native object x

9. The association model is the visibility source of truth

For PA-facing search, filtering, reporting, and governance operations, the association model should be treated as the primary source of truth for scoped visibility.

The native Odoo object should not remain the hidden carrier of governance logic when the relationship itself is what matters.

10. SA scoping is selective, not universal across Odoo

This ADR does not introduce one universal SA-scoping engine across all native Odoo models.

The design assumption is the opposite:

  • most native Odoo classes do not need SA scoping at all
  • only a minority of classes or records need explicit SA-governed scope
  • some SA scoping should occur at a collection level rather than at every leaf object

Examples of collection-level scope may include:

  • a fleet as the governed collection rather than every underlying item row
  • a governed outlet/channel set rather than every adjacent commercial record
  • an SA-scoped location/folder/container that already bounds child visibility

Therefore:

  • do not spread ov.sa_x mechanically across all Odoo classes
  • do not assume every object inside a governed workflow needs its own association row
  • prefer the smallest governed surface that correctly expresses the business restriction

11. The underlying native Odoo class x must not be modified merely to support SA scope

The preferred V3 pattern is an externalized governance overlay.

That means:

  • ov.sa_x may reference native Odoo object x
  • ov.actor_sa_x may reference ov.sa_x
  • native Odoo object x should remain the native business object of record

By default, do not add SA-governance fields onto native Odoo class x just because x participates in SA scope.

By default, do not add:

  • direct sa_id / actor_id governance fields on x
  • inverse helper relations on x purely for association convenience
  • changes to native Odoo business lifecycle just because an association model exists
  • SA-specific behavior that rewrites the intrinsic Odoo meaning of x

The association model is intentionally external to Odoo's intrinsic design for that class.

The relation is governed externally; the native object does not need to become SA-aware in its own primary model shape.

12. No inverse relationship on x is the default rule

The default V3 stance is:

  • ov.sa_x holds a Many2one or equivalent reference to native x
  • native x does not define an inverse association field merely to satisfy the governance pattern

This rule exists to preserve three boundaries:

  • Odoo remains the owner of native object identity and lifecycle
  • SA governance remains an external scope overlay
  • implementation teams do not gradually reintroduce stamp-first coupling under a different technical name

If an implementation later argues for an inverse relation on x, the burden of proof is high.

That proof must show a concrete operational need beyond convenience, and it must not alter the native business meaning of x.

13. Query efficiency should be evaluated against sparse governed scope, not against the whole native table

The design intent of this ADR is that ov.sa_x tables stay selective.

They are not intended to mirror all rows of native Odoo table x.

Expected operating assumptions:

  • the majority of Odoo classes have no SA scope at all
  • even for a scoped class x, only a minority of rows in x usually participate in governed scope
  • in many cases the governed scope is attached to a collection-level object, which reduces row count further

Under those assumptions, ov.sa_x acts as a selective governance index over the exceptional subset of x, not as a full shadow copy of x.

14. Engineering estimate: the extra hop is usually acceptable when the governed tables stay small

For direct stamp-based scope, a typical query shape might be:

SELECT x.*
FROM x
WHERE x.sa_id = :sa
  AND x.actor_id = :actor;

For the two-layer association pattern, a typical query shape becomes:

SELECT x.*
FROM ov_actor_sa_x ax
JOIN ov_sa_x sx
  ON sx.id = ax.sa_x_id
JOIN x
  ON x.id = sx.x_id
WHERE ax.actor_id = :actor
  AND ax.state = 'active'
  AND sx.state = 'active';

This adds one more indexed join relative to a collapsed direct-stamp design.

In engineering terms, when ax and sx remain selective and properly indexed, that extra hop is usually acceptable.

This ADR therefore expects:

  • some increase in database work relative to a one-row stamp model
  • but usually not a catastrophic increase
  • and often only a modest user-visible latency difference compared with total request overhead

Practical estimate for well-indexed, sparse governed tables:

  • database cost may be roughly 1.2x to 2x of a direct-stamp path
  • end-to-end endpoint latency may still remain within a small operational band because controller, ORM, serialization, and UI overhead already dominate much of the request

These are engineering estimates, not hard SLAs.

They are acceptable precisely because the design assumes sparse governance tables rather than universal cross-model scoping.

15. The real performance danger is row explosion, not the existence of one extra join

The two-layer model becomes costly when teams misuse it.

The main failure modes are:

  • applying SA scope to classes that do not need it
  • creating actor-level rows for broad shared sets that should stay SA-shared or collection-scoped
  • implementing large Python-side id in [...] searches instead of database joins
  • forcing every shared catalog/product/location leaf into per-actor rows
  • using anti-join patterns such as unassigned without proper indexes and policy boundaries

Therefore the key performance discipline is:

  • keep SA scope sparse
  • prefer collection-level scope where the business model allows it
  • create actor-level rows only when actor-level semantics are real

16. Querying rule for developers and agents

When a view is SA-governed or actor-governed under this ADR:

  • do not start from native Odoo object x and try to retrofit actor filtering
  • start from the relevant association model
  • use the association table as the primary visibility index
  • join to native x only after the governed subset has been identified

In other words:

  • My Customers should usually start from the customer association path
  • My Products should start from the governed collection or governed product association path, not from the entire product table
  • My X should not mean search all x, then filter in Python

For this repo, the preferred implementation stance is:

  • keep native Odoo class x unchanged by default
  • keep SA governance externalized in ov.sa_x
  • keep actor narrowing externalized in ov.actor_sa_x
  • keep association kind explicit in association metadata rather than in the model name
  • keep collection-level scope where possible
  • use association rows as selective query helpers

This preserves:

  • native Odoo semantics
  • explicit governance semantics
  • subset-rule correctness
  • acceptable query performance under sparse scope assumptions

Migration Path Advice

Legacy stamp-based visibility should move to the new pattern in controlled stages.

1. Classify the old stamp before refactoring

For each legacy stamped model, first identify what the stamp actually means:

  • assignment
  • shared access
  • durable binding
  • or a mixed legacy shortcut that needs to be split

Do not migrate a field mechanically before its semantics are explicit.

2. Introduce the association model beside the existing stamp

Create the new explicit association model first.

Define:

  • the native Odoo target record
  • the SA-governance owner or context
  • lifecycle fields such as state, date_from, date_to
  • audit fields such as who assigned or granted the relation
  • exclusivity rules when applicable
  • referenced-target deletion semantics such as dependent, optional, or restricted

Where both SA scope and actor scope are needed, define both layers explicitly:

  • the SA-to-object association as the primary scope record
  • the actor-to-SA-association record as the subordinate handling record

Do not attach the actor association directly to the native object when the subset rule matters.

3. Backfill the association records from legacy stamped data

Convert existing native-record stamps into association rows.

Where possible, preserve:

  • effective dates
  • current active state
  • assignment or granting authority
  • any available historical cues from existing logs or workflow metadata

If the old stamp collapsed multiple meanings into one field set, split that data into separate association types rather than reproducing the ambiguity.

4. Move reads to the association model first

Update PA-facing visibility queries, filters, and reports to read from the association model before removing old fields.

This makes the new governance structure operationally real before cleanup.

When the design uses two layers:

  • move reads to the appropriate SA-layer or actor-layer association path first
  • do not keep native x as the primary search surface for governed reads
  • use collection-level association reads where the governed object is really a collection rather than an individual leaf record

5. Freeze new dependence on direct stamps

Once the association model is active, stop expanding business logic that depends on the old direct stamp fields.

Legacy stamp fields may remain temporarily for compatibility, but they should be treated as transitional compatibility surfaces rather than the long-term governance design.

6. Retire or demote the old stamp after cutover

After consumers have moved:

  • remove obsolete stamp fields where safe
  • or keep them only as derived/cache fields if a specific performance or compatibility reason remains

If retained, the association model still remains the authoritative governance source.

7. Refactor incrementally, but do not create new legacy

Migration may proceed model by model.

However, no new visibility scope should be added using the old stamp-first pattern while the migration is underway.

Consequences

  • V3 becomes the current default refinement layer for new SA visibility-scope design.
  • Direct stamp-based visibility is now a legacy pattern, not the preferred future pattern.
  • When both SA scope and actor scope matter, they should usually be modeled as separate but linked association layers rather than collapsed into one record.
  • Actor-level governed reach is structurally bounded by SA-level governed reach when the actor layer points to the SA-association row.
  • Native Odoo class x should usually remain unchanged and need not carry SA awareness merely because an external association exists.
  • Inverse helper relations on x are not the default pattern and should not be added casually.
  • Query cost is justified by sparse, selective association tables; the pattern is not intended for universal scoping of all Odoo rows.
  • Referential integrity behavior should be explicit on association models rather than left as an implementation accident.
  • Governance relationships become easier to audit, reassign, expire, and explain.
  • Native Odoo models remain cleaner because cross-domain semantics move into explicit association records.
  • Earlier V0 and V2 stamp usage remains historically understandable, but it should no longer be expanded as the default pattern for new scope.