Skip to content

OdooClient

The main entry point for interacting with Odoo. Domain operations are available as namespace properties on the client instance.

Domain Namespaces

Property Class Description
client.helpdesk HelpdeskNamespace Helpdesk ticket operations
client.crm CRMNamespace CRM lead/opportunity operations
client.tasks TaskNamespace Project task operations
client.projects ProjectNamespace Project operations
client.knowledge KnowledgeNamespace Knowledge article operations
client.timer TimerNamespace Timer and timesheet management
client.security SecurityNamespace Security group management
client.generic GenericNamespace Generic model operations
from vodoo import OdooClient, OdooConfig

config = OdooConfig(url="https://my.odoo.com", database="mydb",
                    username="bot@example.com", password="api-key")

with OdooClient(config) as client:
    tickets = client.helpdesk.list(limit=10)
    client.helpdesk.comment(42, "fixed")

    leads = client.crm.list(limit=5)
    tasks = client.tasks.list(limit=5)

OdooClient

OdooClient(config: OdooConfig, *, transport: OdooTransport | None = None, auto_detect: bool = True)

Odoo client for external API access.

Wraps an OdooTransport to provide a convenient interface for Odoo operations. Supports both legacy JSON-RPC (Odoo 14-18) and JSON-2 API (Odoo 19+).

Initialize Odoo client.

PARAMETER DESCRIPTION
config

Odoo configuration

TYPE: OdooConfig

transport

Explicit transport instance (skips auto-detection)

TYPE: OdooTransport | None DEFAULT: None

auto_detect

If True and no transport given, probe JSON-2 first then fall back to legacy. If False, use legacy directly.

TYPE: bool DEFAULT: True

Source code in src/vodoo/client.py
def __init__(
    self,
    config: OdooConfig,
    *,
    transport: OdooTransport | None = None,
    auto_detect: bool = True,
) -> None:
    """Initialize Odoo client.

    Args:
        config: Odoo configuration
        transport: Explicit transport instance (skips auto-detection)
        auto_detect: If True and no transport given, probe JSON-2 first then
                     fall back to legacy. If False, use legacy directly.
    """
    self.config = config
    self.url = config.url.rstrip("/")
    self.db = config.database
    self.username = config.username
    self.password = config.password
    self._retry = config.retry_config
    self._extra_headers = config.http_headers

    if transport is not None:
        self._transport = transport
    elif auto_detect:
        self._transport = self._detect_transport()
    else:
        self._transport = LegacyTransport(
            url=self.url,
            database=self.db,
            username=self.username,
            password=self.password,
            retry=self._retry,
            extra_headers=self._extra_headers,
        )

    # Domain namespaces
    self.helpdesk = _make_helpdesk(self)
    self.crm = _make_crm(self)
    self.tasks = _make_tasks(self)
    self.projects = _make_projects(self)
    self.account_moves = _make_account_moves(self)
    self.knowledge = _make_knowledge(self)
    self.timer = _make_timer(self)
    self.security = _make_security(self)
    self.generic = _make_generic(self)

transport property

transport: OdooTransport

The underlying transport.

is_json2 property

is_json2: bool

Whether the client is using the JSON-2 API (Odoo 19+).

uid property

uid: int

Get authenticated user ID.

close

close() -> None

Close the underlying HTTP client.

Source code in src/vodoo/client.py
def close(self) -> None:
    """Close the underlying HTTP client."""
    self._transport.close()

execute

execute(model: str, method: str, *args: Any, **kwargs: Any) -> Any

Execute a method on an Odoo model.

PARAMETER DESCRIPTION
model

Odoo model name (e.g., 'helpdesk.ticket')

TYPE: str

method

Method name (e.g., 'search', 'read')

TYPE: str

*args

Positional arguments for the method

TYPE: Any DEFAULT: ()

**kwargs

Keyword arguments for the method

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Any

Method result

Source code in src/vodoo/client.py
def execute(
    self,
    model: str,
    method: str,
    *args: Any,
    **kwargs: Any,
) -> Any:
    """Execute a method on an Odoo model.

    Args:
        model: Odoo model name (e.g., 'helpdesk.ticket')
        method: Method name (e.g., 'search', 'read')
        *args: Positional arguments for the method
        **kwargs: Keyword arguments for the method

    Returns:
        Method result
    """
    return self._transport.execute_kw(model, method, list(args), kwargs or None)

execute_sudo

execute_sudo(model: str, method: str, user_id: int, *args: Any, **kwargs: Any) -> Any

Execute a method as another user using sudo.

PARAMETER DESCRIPTION
model

Odoo model name

TYPE: str

method

Method name

TYPE: str

user_id

User ID to execute as

TYPE: int

*args

Positional arguments

TYPE: Any DEFAULT: ()

**kwargs

Keyword arguments

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Any

Method result

Source code in src/vodoo/client.py
def execute_sudo(
    self,
    model: str,
    method: str,
    user_id: int,
    *args: Any,
    **kwargs: Any,
) -> Any:
    """Execute a method as another user using sudo.

    Args:
        model: Odoo model name
        method: Method name
        user_id: User ID to execute as
        *args: Positional arguments
        **kwargs: Keyword arguments

    Returns:
        Method result
    """
    if "context" not in kwargs:
        kwargs["context"] = {}
    kwargs["context"]["sudo_user_id"] = user_id
    return self.execute(model, method, *args, **kwargs)

search

search(model: str, domain: list[Any] | None = None, limit: int | None = None, offset: int = 0, order: str | None = None) -> list[int]

Search for records.

Source code in src/vodoo/client.py
def search(
    self,
    model: str,
    domain: list[Any] | None = None,
    limit: int | None = None,
    offset: int = 0,
    order: str | None = None,
) -> list[int]:
    """Search for records."""
    return self._transport.search(model, domain, limit, offset, order)

read

read(model: str, ids: list[int], fields: list[str] | None = None) -> list[dict[str, Any]]

Read records by IDs.

Source code in src/vodoo/client.py
def read(
    self,
    model: str,
    ids: list[int],
    fields: list[str] | None = None,
) -> list[dict[str, Any]]:
    """Read records by IDs."""
    return _normalize_false(self._transport.read(model, ids, fields))

search_read

search_read(model: str, domain: list[Any] | None = None, fields: list[str] | None = None, limit: int | None = None, offset: int = 0, order: str | None = None) -> list[dict[str, Any]]

Search and read records in one call.

Source code in src/vodoo/client.py
def search_read(
    self,
    model: str,
    domain: list[Any] | None = None,
    fields: list[str] | None = None,
    limit: int | None = None,
    offset: int = 0,
    order: str | None = None,
) -> list[dict[str, Any]]:
    """Search and read records in one call."""
    return _normalize_false(
        self._transport.search_read(model, domain, fields, limit, offset, order)
    )

create

create(model: str, values: dict[str, Any], context: dict[str, Any] | None = None) -> int

Create a new record.

Source code in src/vodoo/client.py
def create(
    self,
    model: str,
    values: dict[str, Any],
    context: dict[str, Any] | None = None,
) -> int:
    """Create a new record."""
    return self._transport.create(model, process_values(values), context)

write

write(model: str, ids: list[int], values: dict[str, Any]) -> bool

Update records.

Source code in src/vodoo/client.py
def write(
    self,
    model: str,
    ids: list[int],
    values: dict[str, Any],
) -> bool:
    """Update records."""
    return self._transport.write(model, ids, process_values(values))
unlink(model: str, ids: list[int]) -> bool

Delete records.

Source code in src/vodoo/client.py
def unlink(
    self,
    model: str,
    ids: list[int],
) -> bool:
    """Delete records."""
    return self._transport.unlink(model, ids)

fields_get

fields_get(model: str, fields: list[str] | None = None, attributes: list[str] | None = None) -> dict[str, Any]

Return field definitions for a model.

PARAMETER DESCRIPTION
model

Odoo model name (e.g., 'res.partner')

TYPE: str

fields

Optional list of field names to inspect. None returns all fields.

TYPE: list[str] | None DEFAULT: None

attributes

Optional list of field attributes to return (e.g., ['string', 'type', 'required']). None returns all attributes.

TYPE: list[str] | None DEFAULT: None

RETURNS DESCRIPTION
dict[str, Any]

Dictionary mapping field names to their attribute dicts.

Source code in src/vodoo/client.py
def fields_get(
    self,
    model: str,
    fields: list[str] | None = None,
    attributes: list[str] | None = None,
) -> dict[str, Any]:
    """Return field definitions for a model.

    Args:
        model: Odoo model name (e.g., ``'res.partner'``)
        fields: Optional list of field names to inspect.
                ``None`` returns all fields.
        attributes: Optional list of field attributes to return
                    (e.g., ``['string', 'type', 'required']``).
                    ``None`` returns all attributes.

    Returns:
        Dictionary mapping field names to their attribute dicts.
    """
    args: list[Any] = [fields or []]
    kwargs: dict[str, Any] = {}
    if attributes is not None:
        kwargs["attributes"] = attributes
    result: dict[str, Any] = self._transport.execute_kw(
        model, "fields_get", args, kwargs or None
    )
    return result
name_search(model: str, name: str, domain: list[Any] | None = None, limit: int = 7) -> list[tuple[int, str]]

Autocomplete search returning (id, display_name) pairs.

Source code in src/vodoo/client.py
def name_search(
    self,
    model: str,
    name: str,
    domain: list[Any] | None = None,
    limit: int = 7,
) -> list[tuple[int, str]]:
    """Autocomplete search returning (id, display_name) pairs."""
    return self._transport.name_search(model, name, domain, limit)