OVES Odoo Extension Architecture — Base Reference¶
Date: 2026-06-01
Status: Living reference — all Odoo extension work must align with this document.
Core Principle¶
Never modify underlying Odoo object models. Never assume behavior or "semantics" of Odoo models.
Odoo base models are treated as read-only nodes. All extensions are built as a graph layered on top of the base models, using association tables as edges.
The Graph Model¶
Mental Model¶
Base Odoo models = NODES
Association models = EDGES
Every Odoo base model is a node. Every custom ov.sa_* model is a typed edge connecting two nodes, carrying attributes about the relationship.
Node A (ov.serviced_account) ──edge──▶ Node B (sale.order)
│
├── association_kind (type of relationship)
├── date_from / date_to (temporal validity)
├── assigned_by_id (audit trail)
└── state (active / inactive)
Why a Graph, Not Inheritance¶
Inheritance (_inherit) |
Graph (Association) |
|---|---|
| Mutates base model schema | Base models never touched |
| Odoo upgrades break custom fields | Fully isolated from Odoo core |
| Can only model one relationship | Edges can be multiple, typed, directional |
| No temporal validity on relationship | date_from / date_to on every edge |
| No audit trail on "why this link exists" | assigned_by_id on every edge |
| Monolithic permission model | Graph traversal for fine-grained access |
Nodes: Odoo Base Models (Read-Only)¶
Base models are never modified. We only read from them and link to them.
| Node Type | Odoo Model | Role in Graph |
|---|---|---|
| Organization | res.company |
Root node for SA tree |
| Identity | res.partner |
People, companies, customers |
| Product | product.product |
Goods, services |
| Transaction | sale.order |
Sales transactions |
| Transaction | account.move |
Invoices, journal entries |
| Transaction | stock.picking |
Inventory transfers |
| User | res.users |
System users (login) |
| Asset | ov.asset |
Physical assets (custom node) |
| Vehicle | fleet.vehicle |
Fleet vehicles |
| ... | ... | ... |
Edges: Association Models (ov.sa_*)¶
Every edge model follows a strict convention.
Canonical Field Structure¶
| Field | Type | Required | Purpose |
|---|---|---|---|
account_id |
M2O → ov.serviced_account |
✅ | Source node of the edge (the SA) |
[object]_id |
M2O → target Odoo model | ✅ | Target node of the edge (the business object) |
association_kind |
Selection | ✅ | Type of edge — what kind of relationship is this? |
state |
Selection | ✅ | active / inactive |
date_from |
Datetime | ❌ | Edge is valid from this time |
date_to |
Datetime | ❌ | Edge expires at this time |
assigned_by_id |
M2O → res.partner |
❌ | Who authorized this edge |
create_date / create_uid |
System | — | Audit trail (automatic) |
write_date / write_uid |
System | — | Audit trail (automatic) |
Naming Convention¶
ov.sa_{business_object}
| Association Model | Target Model | FK Field |
|---|---|---|
ov.sa_sale_order |
sale.order |
order_id |
ov.sa_customer |
res.partner |
partner_id |
ov.sa_invoice |
account.move |
move_id |
ov.sa_product |
product.product |
product_id |
ov.sa_delivery |
stock.picking |
picking_id |
ov.sa_asset |
ov.asset |
asset_id |
ov.sa_vehicle |
fleet.vehicle |
vehicle_id |
| ... | ... | ... |
association_kind — The Edge Type¶
This is the most important field. It defines what the edge means.
Possible values (to be confirmed with implementation):
| Value | Meaning | SA Member Can |
|---|---|---|
binding |
This object belongs to this SA | Full access |
visible |
This object is visible to this SA | Read-only |
responsible |
This SA is responsible for this object | Edit, but not own |
| (others TBD) |
The SA Tree as a Graph¶
ov.serviced_account is itself a node, and the SA hierarchy is a tree within the graph.
OV Global Root SA (node)
└── Company Root SA (node, company_id=X)
├── SA-A (node) ─── ov.sa_sale_order ───▶ sale.order #1001
│ ───▶ sale.order #1002
└── SA-B (node) ─── ov.sa_customer ───▶ res.partner #5001
Graph traversal query pattern ("What can SA-A access?"):
ov.sa_sale_order
WHERE account_id IN (SA-A, SA-A's children)
AND state = 'active'
AND (date_from IS NULL OR date_from <= now())
AND (date_to IS NULL OR date_to >= now())
→ returns: sale.order #1001, #1002
Permission Enforcement Pattern¶
How ir.rule Uses the Graph¶
Instead of modifying base models, permission is enforced via domain rules that traverse the graph:
# Pseudo-code for sale.order ir.rule
[('id', 'in',
search('ov.sa_sale_order',
[('account_id', 'in', current_user_sa_ids),
('state', '=', 'active')]
).mapped('order_id')
)]
This means:
- Base sale.order model is never modified
- Access control is centralized in association tables
- Adding a new SA → just add an edge, no schema change
What We Never Do¶
| ❌ Forbidden | ✅ Do Instead |
|---|---|
_inherit Odoo base model to add fields |
Create an ov.sa_* association model |
Modify ir.model.fields on base models |
Link via association table |
| Assume Odoo model behavior in custom code | Only use documented API / RPC calls |
| Hard-code SA IDs in business logic | Traverse the graph via domain rules |
| Delete base model records directly | Soft-delete via association state |
Conventions Summary¶
- All Odoo base models are read-only nodes
- All relationships are modeled as
ov.sa_*edge records - Every edge has a type (
association_kind) - Every edge has temporal validity (
date_from/date_to) - Every edge has an audit trail (
assigned_by_id) - No custom fields on base models, ever
- Graph traversal, not model inheritance, drives permissions
For AI Agents Working with This System¶
When asked to "add a new relationship between SA and X":
1. ✅ Create a new ov.sa_X model (edge)
2. ❌ Never add a field to sale.order, res.partner, etc.
3. ✅ The edge model must have: account_id, X_id, association_kind, state, date_from, date_to
4. ✅ Register the model in ir.model
5. ✅ Add ir.rule on the target model to enforce access via graph traversal
This document is the architectural constitution for all Odoo work in OVES. Any deviation must be explicitly justified and documented.