Skip to content

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

  1. All Odoo base models are read-only nodes
  2. All relationships are modeled as ov.sa_* edge records
  3. Every edge has a type (association_kind)
  4. Every edge has temporal validity (date_from / date_to)
  5. Every edge has an audit trail (assigned_by_id)
  6. No custom fields on base models, ever
  7. 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.