ADR 0003: V3 Association-Model Migration Pattern¶
Status¶
Accepted
Date¶
2026-04-23
Context¶
dirac-odoo already established progressive SA design layers:
V0introduced basic{actor, sa}stamping for simple PA work surfacesV1introduced hierarchy and roll-upV2expanded 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:
assignmentaccessbinding
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_orderov.sa_customerov.sa_location
Typical metadata field examples:
association_kindrestriction_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 associationdefines that the object is in scope for the SA at all - the
actor associationdefines 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
SAto native Odoo objectx - carries explicit metadata such as
association_kind ov.actor_sa_x- bridges an actor to the
ov.sa_xrow, not directly to native Odoo objectx - 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_xmechanically 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_xmay reference native Odoo objectxov.actor_sa_xmay referenceov.sa_x- native Odoo object
xshould 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_idgovernance fields onx - inverse helper relations on
xpurely 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_xholds aMany2oneor equivalent reference to nativex- native
xdoes 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 inxusually 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.2xto2xof 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
unassignedwithout 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
xand try to retrofit actor filtering - start from the relevant association model
- use the association table as the primary visibility index
- join to native
xonly after the governed subset has been identified
In other words:
My Customersshould usually start from the customer association pathMy Productsshould start from the governed collection or governed product association path, not from the entire product tableMy Xshould not meansearch all x, then filter in Python
17. Recommended implementation stance¶
For this repo, the preferred implementation stance is:
- keep native Odoo class
xunchanged 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:
assignmentshared accessdurable 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, orrestricted
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
xas 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¶
V3becomes 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
xshould usually remain unchanged and need not carry SA awareness merely because an external association exists. - Inverse helper relations on
xare 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
V0andV2stamp usage remains historically understandable, but it should no longer be expanded as the default pattern for new scope.