ADR 0005: V3 SA-POS Governance — Customer Protection, Agent Identity, and Reporting Privileges¶
Status¶
Draft
Date¶
2026-06-12 (updated 2026-06-13)
Context¶
dirac-odoo establishes SA (Service Account) as an access governance layer.
The POS channel (Point of Sale) is the primary transactional interface for
field operations — retail shops, swap stations, and agent-led sales.
Unlike B2B sales (which flow through sale.order with pre-negotiated terms),
POS transactions are instantaneous, serve walk-in customers, and must balance
operational speed with governance traceability.
This ADR defines the SA-POS governance model: how customers are identified and protected, how agents are identified and scoped, what the admin role does, how SA scope defines ACL, and what reporting privileges exist at each level.
1. Customer Identification — Protection, Not Surveillance¶
Design Principle¶
All customers are identified, and their identity is maintained on records. Privacy issues notwithstanding, this requirement is for their protection — against product quality issues, and against sales-agent dynamics.
Why Identification Is Mandatory¶
-
Product Quality Protection: If a batch defect is discovered, the company must be able to identify every customer who purchased affected products. Anonymous sales make this impossible.
-
Sales-Agent Dynamics Protection: The relationship is between the customer and the company, not between the customer and the field agent. If an agent leaves or is replaced, the customer's entitlement to service, warranty, and support must survive. Customer identification severs the agent-customer dependency.
-
Entitlement to Claims: A customer's identity record serves as the legal basis for warranty claims, service requests, and dispute resolution. "I bought it here" is not sufficient — "I am partner #12345" is.
Identity Methods (Africa Context)¶
Acceptable identity anchors, in priority order:
| Method | Reason | Data Stored |
|---|---|---|
| Phone number | Dominant in African markets, supports M-Pesa lookup | res.partner.phone |
| Service card | Identifies product ownership (solar home system, battery swap) | res.partner.ref or custom field |
| National ID | Fallback for customers without phone | res.partner.ref |
Anonymous sales are prohibited. If a customer refuses identification, the sale cannot proceed.
Auto-Admission at POS¶
Walk-in customers are admitted to the SA at the moment of first transaction:
- Cashier identifies customer (phone/service card)
- System checks
ov.sa_customerfor this partner + this SA - If no record exists → auto-create
ov.sa_customerwithassociation_kind='binding' - Proceed with checkout
The act of being served constitutes admission to the SA's functionality scope. No pre-registration required, but identification is mandatory.
2. Sales Agent Identification — System Level + SA Context Level¶
Design Principle¶
All sales agents are identified at both system level and explicit context SA level. Context of action must carry the SA stamp
{sa, actor}, and access to Odoo functional models is via association tables.
Two-Level Identity¶
| Level | What It Means | Odoo Mechanism |
|---|---|---|
| System level | Agent has an Odoo identity (res.partner + ov.membership) | res.users login, res.partner record |
| SA context level | Agent is a member of specific SAs with defined role | ov.membership record, ov.actor_sa_* records |
Action Context Stamp: {sa, actor}¶
Every POS transaction must be stamped with:
{
'sa': ov.serviced_account.id, # Which SA authorized this transaction
'actor': res.partner.id, # Which agent executed this transaction
'timestamp': datetime, # When
'pos_order_id': pos.order.id # Which transaction
}
This stamp is stored in the association tables (ov.sa_pos_order +
ov.actor_sa_pos_order), not in pos.order itself (per ADR 0003: no
modification to native Odoo models).
Access via Association Tables (Not Direct)¶
Agents do not access pos.order directly. They access it through:
Agent → ov.actor_sa_pos_order → ov.sa_pos_order → pos.order
This indirection is not overhead — it is the enforcement mechanism.
The association tables ARE the ACL. If an agent is not in
ov.actor_sa_pos_order for a given SA, they cannot see those orders.
3. Admin Role in SA Context — External, Not Internal¶
Design Principle¶
The admin role in the SA context is NOT internal to SAs. This role exists to setup and maintain the associations.
What "Admin" Means in SA Context¶
The term "admin" in SA context refers to:
- Creating/updating
ov.serviced_accountrecords (SA node definitions) - Creating/updating
ov.membershiprecords (who belongs to which SA) - Creating/updating
ov.sa_*association records (which SA governs which Odoo records) - Configuring
ov.sa_pos_config(which POS terminal belongs to which SA)
Admin Is NOT a Role Within an SA¶
An SA has these internal roles (defined in ov.membership):
| Role | Can Do |
|---|---|
customer |
Receive services, buy products |
staff |
Execute transactions within SA scope |
agent |
Execute transactions + admit new customers |
sa_manager |
All above + aggregate reports within SA + manage membership |
"Admin" (the person who sets up SAs and associations) is outside this hierarchy. They are not a "super-SA-manager" — they are the association maintainer.
Separation of Duties¶
| Role | Belongs To | Can Modify Associations? |
|---|---|---|
| SA Manager | Inside the SA | Only within their SA |
| Admin (Association Maintainer) | Outside all SAs | All SAs |
This prevents any single SA from gaining control over the association infrastructure.
4. SA Scope — Field Decision, Defines ACL¶
Design Principle¶
SA scope is a field decision, but carries heavy responsibility, because this defines ACL.
What "Field Decision" Means¶
The commercial/field team decides:
- How many SAs to create (one per region? per store? per functionality?)
- Which agents belong to which SA
- What
scope_policyeach membership has (assigned_only/assigned_plus_unassigned/sa_wide)
These are business decisions, not technical decisions. But they have security consequences — because SA scope = access scope.
SA Scope IS ACL¶
There is no separate "ACL configuration" step. The act of:
- Creating an SA (
ov.serviced_account) - Adding a member to it (
ov.membership) - Setting
scope_policyon the membership
... is the ACL configuration. The ir.rule (Track B in ADR 0006) then
enforces this ACL on every query.
Responsibility of Field Team¶
Because SA scope defines ACL, the field team must:
- Not over-scope: Don't give
scope_policy='sa_wide'to agents who only needassigned_only - Not under-scope: Don't give
scope_policy='assigned_only'to agents who need to see unassigned transactions (e.g., float stock) - Audit memberships: Regularly review
ov.membershiprecords for stale/incorrect entries
The admin (association maintainer) can audit, but the field team owns the decisions.
5. POS as Transactional Model — Unit Operation¶
Design Principle¶
POS is a transactional model. From the SA perspective, a POS order is a unit operation, though its state can be parked or on-hold (whatever Odoo allows).
POS Order as Unit of Governance¶
Each pos.order record is a governance unit:
- It is created atomically (one checkout = one
pos.order) - It is attributed to exactly one SA (the cashier's active SA)
- It is attributed to exactly one primary actor (the cashier)
- Its governance lifetime = the
pos.orderlifetime
State: Parked / On-Hold (Odoo Native)¶
Odoo POS supports "parking" an order (save for later, resume). From SA perspective:
- Parked order: Still belongs to the original SA + actor context
- Resumed order: Still belongs to the original SA + actor context
- Context switch: If cashier switches SA while order is parked, the original SA context is preserved (the order was created under that SA)
This is a design decision: SA context is frozen at order creation time. It does not change if the cashier later switches SA.
Implications for Association Tables¶
Because SA context is frozen at creation:
ov.sa_pos_order.account_idnever changes after creationov.actor_sa_pos_order.actor_idnever changes after creation- If cashier switches SA and creates a new order, a new
ov.sa_pos_order+ov.actor_sa_pos_orderis created
6. Reporting Privileges — Three Tiers¶
Design Principle¶
Each actor can view history data within an SA context. Aggregation among members of an SA is the privilege of the SA Manager only. Aggregation along the SA tree is the sole privilege of the parent SA over its child-SAs.
Tier 1: Actor — View Own + Assigned¶
An actor (agent/cashier) can view:
- Their own transaction history within the SA
- Transactions explicitly assigned to them (if
scope_policy='assigned_only') - Unassigned transactions within the SA (if
scope_policy='assigned_plus_unassigned')
They cannot see other agents' transactions within the same SA (unless
scope_policy='sa_wide').
Tier 2: SA Manager — Aggregate Within SA¶
The SA Manager (ov.membership.is_sa_manager=True) can:
- View all transactions within their SA (regardless of which agent executed them)
- Generate aggregate reports (total sales, product mix, customer count)
- Export data for analysis
- Manage membership (add/remove agents, change roles)
They cannot see transactions in other SAs (unless those SAs are child-SAs — see Tier 3).
Tier 3: Parent SA — Aggregate Over Child-SAs¶
A parent SA (ov.serviced_account with parent_id set) can:
- View transactions across all child-SAs in the SA tree
- Generate roll-up reports ("total sales across all regional shops")
- Compare performance across child-SAs
This is configured via ov.serviced_account parent pointer, not via
membership. The parent SA "inherits" visibility over children for reporting
purposes.
Reporting Privilege Matrix¶
| Role | Own Transactions | All in SA | Child-SA Roll-Up |
|---|---|---|---|
Actor (staff/agent) |
✅ | ⚠️ Depends on scope_policy |
❌ |
| SA Manager | ✅ | ✅ | ❌ |
| Parent SA Manager | ✅ | ✅ | ✅ |
7. sa_pos vs. Odoo POS Configuration¶
Design Principle¶
sa_posis a different thing from basic Odoo POS configuration. Product availability, pricing, customer visibility etc. are scoped along the SA line.sa_posis NOT a POS machine. POS configurations in the underlying Odoo system must be compatible or subordinated to SA rule.
What sa_pos Is (and Is Not)¶
| Concept | sa_pos |
Odoo POS (pos.config) |
|---|---|---|
| Is | Governance layer for POS transactions | Infrastructure config for POS terminal |
| Scopes | Product availability, pricing, customer visibility | Payment methods, receipt format, hardware |
| Is a machine? | ❌ No | ❌ No (it's a config, not hardware) |
| Can exist without the other? | Yes (governance without terminal) | Yes (terminal without SA scoping) |
sa_pos Scopes These Dimensions¶
-
Product Availability: Which products are visible/orderable in this SA? →
ov.sa_product_template(association table) -
Pricing: Which pricelist applies to this SA? →
ov.sa_pricelist(association table) -
Customer Visibility: Which customers are visible to this SA? →
ov.sa_customer(association table) -
POS Terminal Binding: Which POS terminal(s) serve this SA? →
ov.sa_pos_config(association table)
Odoo POS Config Must Be Subordinated to SA Rule¶
If Odoo POS config (pos.config) contradicts SA scoping, SA rule wins:
| Scenario | Odoo POS Config Says | SA Rule Says | Outcome |
|---|---|---|---|
| Product not in SA | Product visible in POS | No ov.sa_product_template row |
❌ Product hidden in SA-POS UI |
| Pricelist mismatch | Default pricelist = P1 | SA uses pricelist P2 | ✅ SA pricelist P2 applied |
| Customer not in SA | Customer visible in search | No ov.sa_customer row |
⚠️ Customer visible but auto-admitted on transaction |
sa_pos Is NOT a POS Machine¶
This is a common misconception. sa_pos is a software governance layer,
not a hardware terminal. The physical POS machine is configured in Odoo via
pos.config. sa_pos sits on top and scopes what that terminal can do.
Physical POS Machine
→ Odoo pos.config (hardware settings, payment methods)
→ sa_pos governance (product scope, pricing scope, customer scope)
→ POS Applet UI (what the cashier actually sees)
8. Open Question: How to Configure a POS Machine to Be SA-Aware?¶
The Problem¶
A POS machine (physical hardware) may serve multiple SAs (e.g., a
supermarket with retail counter + swap counter). But pos.config in Odoo
is a single record per terminal. How does the machine "know" which SA it's
serving at any given time?
Current Design (from ADR 0005 original)¶
The cashier's membership determines the SA context, not the terminal:
- Cashier logs in → system reads
ov.membershipfor this partner - If one membership → auto-select that SA
- If multiple → cashier selects SA at shift start
- SA context frozen for the shift (or per-transaction, configurable)
Open Design Questions¶
-
Terminal vs. Cashier: Should the terminal itself be SA-bound (
ov.sa_pos_configlinks terminal to SA), or should the cashier's membership determine it? Current answer: cashier's membership. -
Multi-SA Terminal: If a terminal serves multiple SAs, should the cashier switch SA per transaction, or have multiple "modes" in the UI? Current answer: shift-level selection, but per-transaction is configurable.
-
Hardware Binding: Should a terminal be locked to a single SA (kiosk mode), or be flexible? This is a field decision.
-
Offline Mode: If the terminal goes offline, how is SA context preserved? (Odoo POS supports offline, but SA context must be cached.)
Recommendation (for future decision)¶
The SA awareness should be cashier-centric, not terminal-centric:
- Terminal is infrastructure (like a laptop)
- Cashier's membership defines governance scope
- Terminal may be pre-configured for certain SAs (via
ov.sa_pos_config), but cashier's membership is the enforcement point
This allows a single terminal to serve multiple SAs (just switch cashier login), and allows a single cashier to serve multiple SAs (switch context at shift start or per-transaction).
9. SA-POS Data Model (Summary)¶
Association Tables for POS¶
| Table | Purpose | Points To |
|---|---|---|
ov.sa_pos_config |
Which SA owns this POS terminal? | ov.serviced_account + pos.config |
ov.sa_pricelist |
Which pricelist belongs to this SA? | ov.serviced_account + product.pricelist |
ov.sa_pos_order |
Which SA governs this POS order? | ov.serviced_account + pos.order |
ov.actor_sa_pos_order |
Which actor executed this POS order? | ov.sa_pos_order + res.partner |
Key Constraint¶
ov.actor_sa_pos_order points to ov.sa_pos_order, not to pos.order
directly. This enforces the subset rule: an actor's governed reach cannot
exceed the SA that authorized the order.
Consequences¶
-
Customer identification is mandatory — no anonymous sales, for customer protection (product quality + agent independence)
-
Agent identity is two-level — system (Odoo user) + SA context (membership), action context stamped as
{sa, actor} -
Admin role is external to SAs — admins set up/maintain associations, they are not "super-managers" within SAs
-
SA scope = ACL — field team decides scope, but this directly defines access control; heavy responsibility
-
POS order is unit operation — SA context frozen at creation, survives park/resume
-
Reporting is three-tier — actor sees own, SA Manager sees all in SA, parent SA sees roll-up over children
-
sa_posis governance layer, not hardware — Odoo POS config must be subordinated to SA scoping rules -
POS machine SA-awareness is cashier-centric — open question for future decision
Open Items¶
- POS machine SA-awareness: Finalize whether terminal or cashier determines SA context (recommendation: cashier-centric)
- Offline mode: How SA context is preserved when terminal is offline
- Multi-SA terminal UI: Design the SA selector in POS UI for cashiers with multiple memberships
- Parent SA roll-up reporting: Implement the aggregate query pattern for SA tree reporting
scope_policyenforcement: Implementir.rulethat respectsscope_policyvalues (Track B in ADR 0006)