SA Contact Model¶
Purpose¶
This document defines how SA governance applies to Odoo contact records.
It is a second-layer implementation note under the broader SA model. It should be read together with:
- Serviced Account Mini Model for the primary SA developer model
- PA SA Context and Attribution Model for the established PA context and
{actor, sa}attribution flow - Terminology for canonical definitions of
SA,PA,actor,sa, and membership
This document is intentionally narrower than those sources. Its job is to state what contact governance means for res.partner, what fields are needed, what should remain native Odoo, and what audit rules must exist.
Scope¶
This note covers:
- customer and person records anchored in
res.partner - PA-driven creation and management of SA-governed contacts
- attribution, visibility, reassignment, and audit expectations
This note does not redefine:
- the SA hierarchy model
- the membership model
- asset governance
- Odoo accounting, CRM, or stock behavior
Governing References¶
The current contact model inherits these already-established decisions from the repo:
res.partnerremains the identity anchor for both people and organizations.- SA authority lives on membership, not on the partner record itself.
- PA establishes the current
{actor, sa}context before CRUD execution. {actor, sa}is both attribution data and an operational selector for later reads and updates.- If
actoris set, that actor must be a valid member of the stamped SA.
Those rules come primarily from:
Contact Modeling Position¶
Keep res.partner Native¶
The contact model should stay conservative.
Do not create a separate SA-native contact wrapper for v0.
Use native res.partner as the system-of-record anchor for:
- organizations
- people
- addresses
- contact methods
- standard Odoo CRM and commercial relationships
This follows the same design discipline established elsewhere in the repo:
- native Odoo remains the transaction and identity substrate
- SA adds governance boundaries around native objects
What SA Adds¶
For contacts, SA adds governance metadata and rules, not a replacement contact model.
The minimum SA extension to contact governance is:
- one SA boundary field
- one actor assignment field
- audit fields sufficient to reconstruct governance changes
Proposed Contact Governance Fields¶
For SA-governed res.partner records, the current implementation direction should be:
| Field | Type | Purpose | Guidance |
|---|---|---|---|
x_serviced_account_id |
Many2one(ov.serviced_account) |
SA governance boundary for the contact | Nullable for non-PA/native records; required for PA-governed contact records |
x_actor_partner_id |
Many2one(res.partner) |
Current assigned actor inside the SA | Nullable; if set, must resolve to a valid active membership in the stamped SA |
x_pa_created |
Boolean | Distinguish PA-governed creation path from ordinary Odoo creation | Optional but strongly recommended |
x_last_governance_change_at |
Datetime | Last change to SA/actor governance fields | Recommended |
x_last_governance_change_by |
Many2one(res.partner) |
Human actor responsible for latest governance change | Recommended |
Naming note:
- The exact field names are still implementation-level choices.
- The important part is semantic consistency with the repo’s established
{actor, sa}model.
Semantics of the Contact Stamp¶
The governing pair for contacts is:
{actor, sa}
Mapped for res.partner as:
actor -> x_actor_partner_idsa -> x_serviced_account_id
Interpretation:
{actor = null, sa = X}means the contact belongs to SAXand is shared or unassigned inside that SA{actor = A, sa = X}means the contact belongs to SAXand is currently assigned to actorA
This pair describes current governance state. It is not sufficient by itself as full history.
Contact Types and How Governance Applies¶
Organization Contacts¶
For organization records in res.partner:
- the contact may represent a customer company, branch, partner organization, or account-level node
- the stamped SA identifies which serviced account governs that organization record in PA workflows
This does not mean the organization itself becomes the SA object.
The SA object remains ov.serviced_account. The partner record is still the contact/identity record.
Person Contacts¶
For person records in res.partner:
- the contact may be a customer-side user, operator, buyer, account contact, or internal operational person
- the stamped SA indicates in which governance boundary the person contact is currently being managed
Important distinction:
- a person may exist in
res.partneronce - that same person may participate in multiple governed contexts through membership or commercial relationships
- therefore SA governance on a contact record must be treated as application governance for the record, not a universal statement about the person’s entire identity
Creation Paths¶
PA-Created Contacts¶
When PA creates a new governed contact:
- PA must already have an active
{actor, sa}context. - The created
res.partnerrecord must be stamped with that SA. - The created
res.partnerrecord should normally also be stamped with the creating actor. - The creation event should be written to a governance audit trail.
Default rule:
- PA-created customer/person contacts should start as
{current_actor, current_sa}
Exception:
- some apps may intentionally create shared contacts as
{actor = null, sa = current_sa}
That exception must be explicit in app policy, not accidental.
Native Odoo or Legacy Contacts¶
Contacts created outside PA may have no SA stamp.
That is acceptable.
The model should not require backfilling every historical res.partner row before rollout.
Governance rule:
- non-PA-native contacts may remain unstamped until they enter an SA-governed workflow
Visibility Rules for Contacts¶
Contact visibility follows the same pattern already established for other SA-governed records.
Base Rule¶
A contact is visible in PA only when:
contact.x_serviced_account_id == current_sa
Actor Rule¶
Inside the SA boundary:
sais the inclusive selectoractoris the exclusive selector
Supported Policy Shapes¶
The current contact implementation should support these policy modes:
| Policy | Meaning |
|---|---|
assigned_only |
actor sees only contacts where x_actor_partner_id == current_actor |
assigned_plus_unassigned |
actor sees contacts assigned to self or unassigned contacts within the SA |
sa_wide |
actor sees all contacts in the current SA |
Suggested initial hierarchy direction:
- an ordinary member should use
assigned_onlyorassigned_plus_unassigneddepending on app policy - a member with subordinates may get broader supervision/reporting inside the current SA through hierarchy-based team roll-up
- the current
sa_managermay additionally get descendant-SA reach following the broader SA hierarchy rule already defined in the main SA model
Rationale:
- contact governance should follow the same hierarchy semantics as the broader SA model
- this avoids inventing one visibility model for contacts and another for the rest of the SA-governed objects
- hierarchy remains more reliable than semantic labels because the reporting path is derived from explicit structure rather than interpretation of titles
Separation From Odoo Native Security¶
This model does not eliminate Odoo ACLs and record rules.
It changes where business governance is decided.
The expected split is:
| Layer | Responsibility |
|---|---|
| Odoo ACL / record-rule layer | Enforce technical read/write boundaries for the proxy execution strategy |
| PA app layer | Establish current context, decide app policy, and prevent out-of-scope operations before execution |
| SA membership logic | Determine whether the current actor is allowed to operate in the current SA |
Practical rule:
- do not rely on plain Odoo salesperson ownership as the contact-governance model
- do not rely on
company_idalone to represent SA scope
Integrity Rules¶
The following rules should be treated as non-negotiable.
Rule 1: Membership Integrity¶
If x_actor_partner_id is not null, that person must hold a valid active membership in x_serviced_account_id.
Broken state example:
x_actor_partner_id = Ax_serviced_account_id = SA_X- actor
Ahas no valid membership inSA_X
That is an integrity failure.
Rule 2: Normalization on Membership Removal¶
If membership removal invalidates the current stamp:
- set
x_actor_partner_id = null
Do not leave a broken {actor, sa} pair on the contact.
Rule 3: Explicit Reassignment¶
If a contact is moved from actor A to actor B inside the same SA:
- the operation must validate membership for
B - the reassignment must be auditable
Rule 4: SA Transfer¶
If a contact is moved from SA X to SA Y:
- the operation must be explicit
- actor assignment must be revalidated
- if the existing actor is not valid in
Y, normalize actor to null or assign a valid actor inY
This is not a casual field edit. It is a governance transfer.
Audit Model¶
The earlier draft correctly identified that the live {actor, sa} stamp is not enough for history.
That now becomes an implementation requirement.
Minimum Audit Requirement¶
Every change to contact governance should produce a durable audit event containing at least:
- contact record id
- previous
sa - new
sa - previous
actor - new
actor - operation type
- effective timestamp
- responsible human actor
- execution channel such as
PAor administrative Odoo action
Preferred Implementation Direction¶
The cleanest direction is a separate audit model, for example:
ov.governance_audit_log
Minimum event classes:
contact_createdcontact_assignment_changedcontact_unassignedcontact_sa_transferredmembership_normalization
Why separate audit storage is preferred:
- the live fields on
res.partnershould represent current state - history is append-only and should not depend on reconstructing chatter or generic write logs
- governance events will likely need cross-object analysis later, not only contact-specific review
Operational Guidance for v0¶
If implementation starts now, the contact prototype should prove these behaviors:
- A PA-created contact can be stamped with
{actor, sa}. - Contacts are filterable by current SA first, then actor policy.
- Reassignment to another actor validates membership.
- Membership removal normalizes invalid actor assignments to null.
- Governance changes create append-only audit events.
- Non-PA legacy contacts can coexist without immediate forced migration.
Risks That Remain Open¶
These items are still open design questions, but they no longer block this document from being implementation-guiding:
- exact field names and whether the governance stamp should be provided by a reusable mixin
- whether supervisory contact views belong fully in PA or partly in Odoo back office
- the exact hierarchy-based authority boundary for contact reassignment and SA transfer
- whether some contact classes should be immutable across SA transfer without approval flow
Recommended Implementation Position¶
The current recommended position for contacts is:
- keep
res.partneras the native contact model - add a conservative SA governance stamp to SA-managed contacts
- treat
{actor, sa}as current-state governance metadata - require a separate audit log for historical truth
- derive permission from membership and PA context, not from partner ownership alone
That position is consistent with the established SA/PA governance documents already in this repo and keeps the contact model aligned with the broader design principle:
- governance is added around native Odoo objects, not by replacing them