Files
callista-odoo-demo/scripts/odoo_connect.py
T
SMEtoolsandClaude Opus 4.8 618bdcb444 odoo-demo-architect: discovery -> live Odoo demo + sales docs, AI-built
Public skill for Claude Code. Mines a discovery call, researches native-vs-custom,
stands up a working Odoo demo over XML-RPC (modules + data + tested hero features),
and generates run-of-show, one-pager, and a 1-page demo script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBytQq5dcASi4ETm7n6THb
2026-06-28 20:13:22 +03:00

77 lines
3.0 KiB
Python

"""Generic Odoo XML-RPC connector for odoo-demo-architect.
Reads credentials from environment variables (or a creds.json next to this file):
ODOO_URL e.g. https://my-instance.odoo.com (no trailing slash)
ODOO_DB database name
ODOO_LOGIN user login (often an email; "admin" on Odoo.sh trials)
ODOO_KEY an API key (Settings > My Profile > Account Security > New API Key)
Usage:
from odoo_connect import x, common, UID, URL, DB
UID # authenticated user id
common.version() # confirm server_version BEFORE building (field names vary by version)
x("res.partner", "search_read", [["customer_rank", ">", 0]], fields=["name"])
The x() helper passes keyword args straight into execute_kw's kwargs, so pass
Odoo kwargs natively: x(model, "search", domain, limit=5, context={"active_test": False})
Do NOT pass a context dict positionally — that becomes `offset` and Postgres errors.
"""
import os, json, ssl, xmlrpc.client
_here = os.path.dirname(os.path.abspath(__file__))
def _cfg(key, default=None):
v = os.environ.get(key)
if v:
return v
path = os.path.join(_here, "creds.json")
if os.path.exists(path):
with open(path) as f:
return json.load(f).get(key, default)
return default
URL = (_cfg("ODOO_URL") or "").rstrip("/")
DB = _cfg("ODOO_DB")
LOGIN = _cfg("ODOO_LOGIN") or "admin"
KEY = _cfg("ODOO_KEY")
if not (URL and DB and KEY):
raise SystemExit("Set ODOO_URL, ODOO_DB, ODOO_KEY (and optionally ODOO_LOGIN) "
"as env vars or in creds.json next to this script.")
_ctx = ssl.create_default_context()
common = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/common", context=_ctx, allow_none=True)
UID = common.authenticate(DB, LOGIN, KEY, {})
if not UID:
raise SystemExit("Authentication failed — check DB / LOGIN / API key.")
models = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/object", context=_ctx, allow_none=True)
def x(model, method, *args, **kwargs):
"""execute_kw wrapper. Positional args -> Odoo positional params;
keyword args (limit, fields, context, ...) -> Odoo kwargs."""
return models.execute_kw(DB, UID, KEY, model, method, list(args), kwargs)
def get_or_create(model, domain, vals, label=None):
"""Idempotent create: update if a record matches `domain`, else create."""
ids = x(model, "search", domain, limit=1)
if ids:
x(model, "write", ids, vals)
return ids[0]
return x(model, "create", vals)
def fields_named(model, *needles):
"""Return field technical names on `model` whose name/label contains any needle.
NOTE: on some Odoo 19 builds fields_get attribute VALUES return null over XML-RPC,
so match on the technical KEYS, which are always reliable."""
keys = list(x(model, "fields_get", [], {}).keys())
nl = [n.lower() for n in needles]
return sorted(k for k in keys if any(n in k.lower() for n in nl))
if __name__ == "__main__":
print("server_version:", common.version().get("server_version"))
print("uid:", UID, "url:", URL, "db:", DB)