Skip to content

Auth

Authentication utilities and requested-author message operations. Cross-user attribution requires an internal authenticated user. Odoo may reject or override it for share users, which can reliably attribute only to their own partner. Selecting an author does not change authenticated identity, access checks, or auditing.

auth

Authentication utilities for Vodoo.

get_default_user_id

get_default_user_id(client: OdooClient, username: str | None = None) -> int

Get the default requested-author user ID for message operations.

PARAMETER DESCRIPTION
client

Odoo client

TYPE: OdooClient

username

Username to search for (defaults to configured username)

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
int

User ID

RAISES DESCRIPTION
RecordNotFoundError

If user not found

Source code in src/vodoo/auth.py
def get_default_user_id(client: OdooClient, username: str | None = None) -> int:
    """Get the default requested-author user ID for message operations.

    Args:
        client: Odoo client
        username: Username to search for (defaults to configured username)

    Returns:
        User ID

    Raises:
        RecordNotFoundError: If user not found

    """
    search_username = username or client.username
    user_ids = client.search("res.users", domain=[("login", "=", search_username)], limit=1)

    if not user_ids:
        raise RecordNotFoundError("res.users", 0)

    return user_ids[0]

get_partner_id_from_user

get_partner_id_from_user(client: OdooClient, user_id: int) -> int

Get the partner ID associated with a user.

In Odoo, res.users has a partner_id field that links to res.partner. The mail.message.author_id field references res.partner, not res.users.

PARAMETER DESCRIPTION
client

Odoo client

TYPE: OdooClient

user_id

User ID (res.users)

TYPE: int

RETURNS DESCRIPTION
int

Partner ID (res.partner)

RAISES DESCRIPTION
RecordNotFoundError

If user not found or has no partner

Source code in src/vodoo/auth.py
def get_partner_id_from_user(client: OdooClient, user_id: int) -> int:
    """Get the partner ID associated with a user.

    In Odoo, res.users has a partner_id field that links to res.partner.
    The mail.message.author_id field references res.partner, not res.users.

    Args:
        client: Odoo client
        user_id: User ID (res.users)

    Returns:
        Partner ID (res.partner)

    Raises:
        RecordNotFoundError: If user not found or has no partner

    """
    users = client.read("res.users", [user_id], ["partner_id"])
    if not users:
        raise RecordNotFoundError("res.users", user_id)

    partner_id = users[0].get("partner_id")
    if not partner_id:
        raise RecordNotFoundError("res.partner", 0)

    # partner_id is returned as [id, name] tuple
    if isinstance(partner_id, list):
        result: int = partner_id[0]
        return result
    return int(partner_id)

message_post_sudo

message_post_sudo(client: OdooClient, model: str, res_id: int, body: str, user_id: int | None = None, message_type: str = 'comment', is_note: bool = False, **kwargs: Any) -> bool

Post a message or note requesting a specific displayed author.

Cross-user attribution requires an internal authenticated user. Odoo may reject or override it for share users, which can reliably attribute only to their own partner. This does not change the authenticated identity, access checks, or auditing.

RETURNS DESCRIPTION
bool

True if successful

Source code in src/vodoo/auth.py
def message_post_sudo(
    client: OdooClient,
    model: str,
    res_id: int,
    body: str,
    user_id: int | None = None,
    message_type: str = "comment",
    is_note: bool = False,
    **kwargs: Any,
) -> bool:
    """Post a message or note requesting a specific displayed author.

    Cross-user attribution requires an internal authenticated user. Odoo may reject or
    override it for share users, which can reliably attribute only to their own partner.
    This does not change the authenticated identity, access checks, or auditing.

    Returns:
        True if successful
    """
    return bool(
        message_post_sudo_with_id(
            client,
            model,
            res_id,
            body,
            user_id=user_id,
            message_type=message_type,
            is_note=is_note,
            **kwargs,
        )
    )

message_post_sudo_with_id

message_post_sudo_with_id(client: OdooClient, model: str, res_id: int, body: str, user_id: int | None = None, message_type: str = 'comment', is_note: bool = False, **kwargs: Any) -> int

Post requesting a specific displayed author and return the message ID.

Cross-user attribution requires an internal authenticated user. Odoo may reject or override it for share users, which can reliably attribute only to their own partner. This does not change the authenticated identity, access checks, or auditing.

PARAMETER DESCRIPTION
client

Odoo client

TYPE: OdooClient

model

Model name (e.g., 'helpdesk.ticket')

TYPE: str

res_id

Record ID

TYPE: int

body

Message body (HTML)

TYPE: str

user_id

User whose partner is requested as author (uses default if None)

TYPE: int | None DEFAULT: None

message_type

Type of message ('comment' or 'notification')

TYPE: str DEFAULT: 'comment'

is_note

If True, creates an internal note (not visible to customers)

TYPE: bool DEFAULT: False

**kwargs

Additional arguments for message_post

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
int

ID of the created mail.message record

RAISES DESCRIPTION
ConfigurationError

If no default user configured

Source code in src/vodoo/auth.py
def message_post_sudo_with_id(
    client: OdooClient,
    model: str,
    res_id: int,
    body: str,
    user_id: int | None = None,
    message_type: str = "comment",
    is_note: bool = False,
    **kwargs: Any,
) -> int:
    """Post requesting a specific displayed author and return the message ID.

    Cross-user attribution requires an internal authenticated user. Odoo may reject or
    override it for share users, which can reliably attribute only to their own partner.
    This does not change the authenticated identity, access checks, or auditing.

    Args:
        client: Odoo client
        model: Model name (e.g., 'helpdesk.ticket')
        res_id: Record ID
        body: Message body (HTML)
        user_id: User whose partner is requested as author (uses default if None)
        message_type: Type of message ('comment' or 'notification')
        is_note: If True, creates an internal note (not visible to customers)
        **kwargs: Additional arguments for message_post

    Returns:
        ID of the created ``mail.message`` record

    Raises:
        ConfigurationError: If no default user configured

    """
    if user_id is None:
        if client.config.default_user_id is None:
            msg = "No default user ID configured"
            raise ConfigurationError(msg)
        user_id = client.config.default_user_id

    # Convert user_id (res.users) to partner_id (res.partner)
    # mail.message.author_id references res.partner, not res.users
    partner_id = get_partner_id_from_user(client, user_id)

    # Create the message directly in mail.message model
    # This avoids the RPC marshalling issue with message_post

    # Resolve translated subtype labels through their stable external IDs.
    subtype_xmlid = "mt_note" if is_note else "mt_comment"
    subtype_rows = client.search_read(
        "ir.model.data",
        domain=[("module", "=", "mail"), ("name", "=", subtype_xmlid)],
        fields=["res_id"],
        limit=1,
    )
    subtype_id = subtype_rows[0].get("res_id") if subtype_rows else None
    if not isinstance(subtype_id, int) or isinstance(subtype_id, bool) or subtype_id <= 0:
        raise RecordNotFoundError("mail.message.subtype", 0)

    # For non-internal users (share users), internal notes require message_type='notification'
    # because Odoo's mail.message._get_forbidden_access() blocks message_type='comment'
    # with internal subtypes for non-internal users
    effective_message_type = "notification" if is_note else message_type

    message_vals = {
        "model": model,
        "res_id": res_id,
        "body": body,
        "message_type": effective_message_type,
        "subtype_id": subtype_id,
        "author_id": partner_id,  # Use partner_id, not user_id
        **kwargs,
    }

    # Create the message
    return client.create("mail.message", message_vals)