Back to Skills
    🦞

    odoo-manager

    Manage Odoo (contacts, any business objects, and metadata)

    By @willykinfoussia
    View on GitHub
    SKILL.md
    ---
    name: odoo-manager
    description: Manage Odoo (contacts, any business objects, and metadata) via the official External XML-RPC API. Supports generic CRUD operations on any model using execute_kw, with ready-made flows for res.partner and model introspection. Features dynamic instance and database switching with context-aware URL, database, and credential resolution.
    homepage: https://www.odoo.com/documentation/
    metadata: {"openclaw":{"emoji":"🏢","requires":{"env":["ODOO_URL","ODOO_DB","ODOO_USERNAME","ODOO_PASSWORD"]},"primaryEnv":"ODOO_PASSWORD"}}
    ---
    
    # Odoo Manager Skill
    
    ## 🔐 URL, Database & Credential Resolution
    
    ### URL Resolution
    
    Odoo server URL precedence (highest to lowest):
    
    1. `temporary_url` — one-time URL for a specific operation
    2. `user_url` — user-defined URL for the current session
    3. `ODOO_URL` — environment default URL
    
    This allows you to:
    
    - Switch between multiple Odoo instances (production, staging, client-specific)
    - Test against demo databases
    - Work with different client environments without changing global config
    
    **Examples (conceptual):**
    
    ```text
    // Default: uses ODOO_URL from environment
    {{resolved_url}}/xmlrpc/2/common
    
    // Override for one operation:
    temporary_url = "https://staging.mycompany.odoo.com"
    {{resolved_url}}/xmlrpc/2/common
    
    // Override for session:
    user_url = "https://client-xyz.odoo.com"
    {{resolved_url}}/xmlrpc/2/common
    ```
    
    ### Database Resolution
    
    Database name (`db`) precedence:
    
    1. `temporary_db`
    2. `user_db`
    3. `ODOO_DB`
    
    Use this to:
    
    - Work with multiple databases on the same Odoo server
    - Switch between test and production databases
    
    ### Username & Secret Resolution
    
    Username precedence:
    
    1. `temporary_username`
    2. `user_username`
    3. `ODOO_USERNAME`
    
    Secret (password or API key) precedence:
    
    1. `temporary_api_key` or `temporary_password`
    2. `user_api_key` or `user_password`
    3. `ODOO_API_KEY` (if set) or `ODOO_PASSWORD`
    
    **Important:**
    
    - Odoo API keys are used **in place of** the password, with the usual login.
    - Store passwords / API keys like real passwords; never log or expose them.
    
    Environment variables are handled via standard OpenClaw metadata: `requires.env` declares **required** variables (`ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_PASSWORD`). `ODOO_API_KEY` is an **optional** environment variable used instead of the password when present; it is not listed in metadata and should simply be set in the environment when needed.
    
    ### Resolved Values
    
    At runtime the skill always works with:
    
    - `{{resolved_url}}` — final URL
    - `{{resolved_db}}` — final database name
    - `{{resolved_username}}` — final login
    - `{{resolved_secret}}` — password **or** API key actually used to authenticate
    
    These are computed using the precedence rules above.
    
    ---
    
    ## 🔄 Context Management
    
    > The `temporary_*` and `user_*` names are **runtime context variables used by the skill logic**, not OpenClaw metadata fields. OpenClaw does **not** have an `optional.context` metadata key; context is resolved dynamically at runtime as described below.
    
    ### Temporary Context (One-Time Use)
    
    **User examples:**
    
    - "Pour cette requête, utilise l’instance staging Odoo"
    - "Utilise la base `odoo_demo` juste pour cette opération"
    - "Connecte-toi avec cet utilisateur uniquement pour cette action"
    
    **Behavior:**
    
    - Set `temporary_*` (url, db, username, api_key/password)
    - Use them for **a single logical operation**
    - Automatically clear after use
    
    This is ideal for:
    
    - Comparing data between two environments
    - Running a single check on a different database
    
    ### Session Context (Current Session)
    
    **User examples:**
    
    - "Travaille sur l’instance Odoo du client XYZ"
    - "Utilise la base `clientx_prod` pour cette session"
    - "Connecte-toi avec mon compte administrateur pour les prochaines opérations"
    
    **Behavior:**
    
    - Set `user_*` (url, db, username, api_key/password)
    - Persist for the whole current session
    - Overridden only by `temporary_*` or by clearing `user_*`
    
    ### Resetting Context
    
    **User examples:**
    
    - "Reviens à la configuration Odoo par défaut"
    - "Efface mon contexte utilisateur Odoo"
    
    **Action:**
    
    - Clear `user_url`, `user_db`, `user_username`, `user_password`, `user_api_key`
    - Skill falls back to environment variables (`ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_PASSWORD` / `ODOO_API_KEY`)
    
    ### Viewing Current Context
    
    **User examples:**
    
    - "Sur quelle instance Odoo es-tu connecté ?"
    - "Montre la configuration Odoo actuelle"
    
    **Response should show (never full secrets):**
    
    ```text
    Current Odoo Context:
    - URL: https://client-xyz.odoo.com (user_url)
    - DB: clientxyz_prod (user_db)
    - Username: api_integration (user_username)
    - Secret: using API key (user_api_key)
    - Fallback URL: https://default.odoo.com (ODOO_URL)
    - Fallback DB: default_db (ODOO_DB)
    ```
    
    ---
    
    ## ⚙️ Odoo XML-RPC Basics
    
    Odoo exposes part of its server framework over **XML-RPC** (not REST).
    The External API is documented here: https://www.odoo.com/documentation/18.0/fr/developer/reference/external_api.html
    
    Two main endpoints:
    
    - `{{resolved_url}}/xmlrpc/2/common` — authentication and meta calls
    - `{{resolved_url}}/xmlrpc/2/object` — model methods via `execute_kw`
    
    ### 1. Checking Server Version
    
    Call `version()` on the `common` endpoint to verify URL and connectivity:
    
    ```python
    common = xmlrpc.client.ServerProxy(f"{resolved_url}/xmlrpc/2/common")
    version_info = common.version()
    ```
    
    Example result:
    
    ```json
    {
      "server_version": "18.0",
      "server_version_info": [18, 0, 0, "final", 0],
      "server_serie": "18.0",
      "protocol_version": 1
    }
    ```
    
    ### 2. Authenticating
    
    Use `authenticate(db, username, password_or_api_key, {})` on the `common` endpoint:
    
    ```python
    uid = common.authenticate(resolved_db, resolved_username, resolved_secret, {})
    ```
    
    `uid` is an integer user ID and will be used in all subsequent calls.
    
    If authentication fails, `uid` is `False` / `0` — the skill should:
    
    - Inform the user that credentials or database are invalid
    - Suggest checking `ODOO_URL`, `ODOO_DB`, username, and secret
    
    ### 3. Calling Model Methods with execute_kw
    
    Build an XML-RPC client for the `object` endpoint:
    
    ```python
    models = xmlrpc.client.ServerProxy(f"{resolved_url}/xmlrpc/2/object")
    ```
    
    Then use `execute_kw` with the following signature:
    
    ```python
    models.execute_kw(
        resolved_db,
        uid,
        resolved_secret,
        "model.name",     # e.g. "res.partner"
        "method_name",    # e.g. "search_read"
        [positional_args],
        {keyword_args}
    )
    ```
    
    All ORM operations in this skill are expressed in terms of `execute_kw`.
    
    ---
    
    ## 🔍 Domains & Data Types (Odoo ORM)
    
    ### Domain Filters
    
    Domains are lists of conditions:
    
    ```python
    domain = [["field_name", "operator", value], ...]
    ```
    
    Examples:
    
    - All companies: `[['is_company', '=', True]]`
    - Partners in France: `[['country_id', '=', france_id]]`
    - Leads with probability > 50%: `[['probability', '>', 50]]`
    
    Common operators:
    
    - `"="`, `"!="`, `">"`, `">="`, `"<"`, `"<="`
    - `"like"`, `"ilike"` (case-insensitive)
    - `"in"`, `"not in"`
    - `"child_of"` (hierarchical relations)
    
    ### Field Value Conventions
    
    - **Integer / Float / Char / Text**: use native types.
    - **Date / Datetime**: strings in `YYYY-MM-DD` or ISO 8601 format.
    - **Many2one**: usually send the **record ID** (`int`) when writing; reads often return `[id, display_name]`.
    - **One2many / Many2many**: use the Odoo **command list** protocol for writes (not fully detailed here; see Odoo docs if needed).
    
    ---
    
    ## 🧩 Generic ORM Operations (execute_kw)
    
    Each subsection below shows typical user queries and the corresponding
    `execute_kw` usage. They are applicable to **any** model (not only `res.partner`).
    
    ### List / Search Records (search)
    
    **User queries:**
    
    - "Liste tous les partenaires société"
    - "Cherche les commandes de vente confirmées"
    
    **Action (generic):**
    
    ```python
    ids = models.execute_kw(
        resolved_db, uid, resolved_secret,
        "model.name", "search",
        [domain],
        {"offset": 0, "limit": 80}
    )
    ```
    
    Notes:
    
    - `domain` is a list (can be empty `[]` to match all records).
    - Use `offset` and `limit` for pagination.
    
    ### Count Records (search_count)
    
    **User queries:**
    
    - "Combien de partenaires sont des sociétés ?"
    - "Compte les tâches en cours"
    
    **Action:**
    
    ```python
    count = models.execute_kw(
        resolved_db, uid, resolved_secret,
        "model.name", "search_count",
        [domain]
    )
    ```
    
    ### Read Records by ID (read)
    
    **User queries:**
    
    - "Affiche les détails du partenaire 7"
    - "Donne-moi les champs name et country_id pour ces IDs"
    
    **Action:**
    
    ```python
    records = models.execute_kw(
        resolved_db, uid, resolved_secret,
        "model.name", "read",
        [ids],
        {"fields": ["name", "country_id", "comment"]}
    )
    ```
    
    If `fields` is omitted, Odoo returns all readable fields (often a lot).
    
    ### Search and Read in One Step (search_read)
    
    Shortcut for `search()` + `read()` in a single call.
    
    **User queries:**
    
    - "Liste les sociétés (nom, pays, commentaire)"
    - "Montre les 5 premiers partenaires avec leurs pays"
    
    **Action:**
    
    ```python
    records = models.execute_kw(
        resolved_db, uid, resolved_secret,
        "model.name", "search_read",
        [domain],
        {
            "fields": ["name", "country_id", "comment"],
            "limit": 5,
            "offset": 0,
            # Optional: "order": "name asc"
        }
    )
    ```
    
    ### Create Records (create)
    
    **User queries:**
    
    - "Crée un nouveau partenaire 'New Partner'"
    - "Crée une nouvelle tâche dans le projet X"
    
    **Action:**
    
    ```python
    new_id = models.execute_kw(
        resolved_db, uid, resolved_secret,
        "model.name", "create",
        [{
            "name": "New Partner"
            # other fields...
        }]
    )
    ```
    
    Returns the newly created record ID.
    
    ### Updat
    
    ... (truncated)