Skip to content

Async Timers

AsyncTimerNamespace for timer and timesheet management, accessed as client.timer on AsyncOdooClient.

timer

Async Odoo timer (timesheet) operations.

Mirrors :mod:vodoo.timer with async methods. Reuses all data classes and parsing logic from the sync module.

AsyncTimerBackend

Bases: ABC

Version-specific async timer behavior.

enrich_with_running_state abstractmethod async

enrich_with_running_state(timesheets: list[Timesheet], client: AsyncOdooClient, uid: int) -> list[Timesheet]

Enrich timesheets with running timer state.

Source code in src/vodoo/aio/timer.py
@abstractmethod
async def enrich_with_running_state(
    self,
    timesheets: list[Timesheet],
    client: AsyncOdooClient,
    uid: int,
) -> list[Timesheet]:
    """Enrich timesheets with running timer state."""

start_timer abstractmethod async

start_timer(timesheet: Timesheet, client: AsyncOdooClient) -> None

Start a timer on a timesheet.

Source code in src/vodoo/aio/timer.py
@abstractmethod
async def start_timer(self, timesheet: Timesheet, client: AsyncOdooClient) -> None:
    """Start a timer on a timesheet."""

stop_timer abstractmethod async

stop_timer(timesheet: Timesheet, client: AsyncOdooClient) -> Any

Stop a timer on a timesheet.

Source code in src/vodoo/aio/timer.py
@abstractmethod
async def stop_timer(self, timesheet: Timesheet, client: AsyncOdooClient) -> Any:
    """Stop a timer on a timesheet."""

AsyncOdoo19TimerBackend

Bases: AsyncTimerBackend

Async Odoo 19+: timers managed directly on account.analytic.line.

AsyncLegacyTimerBackend

Bases: AsyncTimerBackend

Async Odoo 14-18: running timer state lives in timer.timer model.

AsyncTimerHandle dataclass

AsyncTimerHandle(_namespace: AsyncTimerNamespace, _source_kind: str, _source_id: int)

Handle returned by start_*() — call :meth:stop to stop this specific timer.

stop async

stop() -> None

Stop the timer that was started with this handle.

Source code in src/vodoo/aio/timer.py
async def stop(self) -> None:
    """Stop the timer that was started with this handle."""
    active = await self._namespace.active()
    for ts in active:
        if ts.source.kind == self._source_kind and ts.source.id == self._source_id:
            await self._namespace._stop_one(ts)
            return
    if self._source_kind == "standalone":
        await self._namespace.stop_timesheet(self._source_id)
        return
    msg = f"No running timer found for {self._source_kind} {self._source_id}"
    raise ValueError(msg)

AsyncTimerNamespace

AsyncTimerNamespace(client: AsyncOdooClient)

Async namespace for timer (timesheet) operations.

Source code in src/vodoo/aio/timer.py
def __init__(self, client: AsyncOdooClient) -> None:
    self._client = client
    self._helpdesk_field: bool | None = None

list async

list(*, days: int = 0, limit: int | None = None) -> list[Timesheet]

Fetch timesheets for the current user.

PARAMETER DESCRIPTION
days

How many days back to include. 0 (default) means today only, 7 means the past week, etc. Pass -1 for all time.

TYPE: int DEFAULT: 0

limit

Maximum number of records to return (None = unlimited).

TYPE: int | None DEFAULT: None

RETURNS DESCRIPTION
list[Timesheet]

Timesheets sorted by date descending.

Source code in src/vodoo/aio/timer.py
async def list(self, *, days: int = 0, limit: int | None = None) -> list[Timesheet]:
    """Fetch timesheets for the current user.

    Args:
        days: How many days back to include.  ``0`` (default) means today
            only, ``7`` means the past week, etc.  Pass ``-1`` for all time.
        limit: Maximum number of records to return (``None`` = unlimited).

    Returns:
        Timesheets sorted by date descending.
    """
    uid = await self._client.get_uid()
    fields = await self._get_fields()
    domain: list[Any] = [["user_id", "=", uid]]
    if days >= 0:
        since = (datetime.now(tz=UTC) - timedelta(days=days)).strftime("%Y-%m-%d")
        domain.append(["date", ">=", since])
    records = await self._client.search_read(
        TIMESHEET_MODEL,
        domain=domain,
        fields=fields,
        order="date desc",
        limit=limit,
    )

    timesheets = [ts for r in records if (ts := _parse_timesheet(r)) is not None]
    backend = self._get_backend()
    return await backend.enrich_with_running_state(timesheets, self._client, uid)

active async

active() -> list[Timesheet]

Fetch currently running timesheets.

Source code in src/vodoo/aio/timer.py
async def active(self) -> builtins.list[Timesheet]:
    """Fetch currently running timesheets."""
    return [ts for ts in await self.list() if ts.timer_start is not None]

start_task async

start_task(task_id: int) -> AsyncTimerHandle

Start a timer on a project task.

Source code in src/vodoo/aio/timer.py
async def start_task(self, task_id: int) -> AsyncTimerHandle:
    """Start a timer on a project task."""
    await self._client.execute("project.task", "action_timer_start", [task_id])
    return AsyncTimerHandle(self, "task", task_id)

start_ticket async

start_ticket(ticket_id: int) -> AsyncTimerHandle

Start a timer on a helpdesk ticket.

Source code in src/vodoo/aio/timer.py
async def start_ticket(self, ticket_id: int) -> AsyncTimerHandle:
    """Start a timer on a helpdesk ticket."""
    await self._client.execute("helpdesk.ticket", "action_timer_start", [ticket_id])
    return AsyncTimerHandle(self, "ticket", ticket_id)

start_timesheet async

start_timesheet(timesheet_id: int) -> AsyncTimerHandle

Start a timer on an existing timesheet.

Source code in src/vodoo/aio/timer.py
async def start_timesheet(self, timesheet_id: int) -> AsyncTimerHandle:
    """Start a timer on an existing timesheet."""
    fields = await self._get_fields()
    records = await self._client.search_read(
        TIMESHEET_MODEL,
        domain=[["id", "=", timesheet_id]],
        fields=fields,
        limit=1,
    )
    if not records:
        msg = f"Timesheet {timesheet_id} not found"
        raise ValueError(msg)
    ts = _parse_timesheet(records[0])
    if ts is None:
        msg = f"Failed to parse timesheet {timesheet_id}"
        raise ValueError(msg)
    backend = self._get_backend()
    await backend.start_timer(ts, self._client)
    source_id = ts.source.id if ts.source.kind != "standalone" else timesheet_id
    return AsyncTimerHandle(self, ts.source.kind, source_id)

stop_timesheet async

stop_timesheet(timesheet_id: int) -> None

Stop a timer on an existing timesheet.

Source code in src/vodoo/aio/timer.py
async def stop_timesheet(self, timesheet_id: int) -> None:
    """Stop a timer on an existing timesheet."""
    fields = await self._get_fields()
    records = await self._client.search_read(
        TIMESHEET_MODEL,
        domain=[["id", "=", timesheet_id]],
        fields=fields,
        limit=1,
    )
    if not records:
        msg = f"Timesheet {timesheet_id} not found"
        raise ValueError(msg)

    ts = _parse_timesheet(records[0])
    if ts is None:
        msg = f"Failed to parse timesheet {timesheet_id}"
        raise ValueError(msg)

    backend = self._get_backend()
    result = await backend.stop_timer(ts, self._client)
    await self._handle_stop_wizard(result)

stop async

stop() -> list[Timesheet]

Stop all currently running timers.

Source code in src/vodoo/aio/timer.py
async def stop(self) -> builtins.list[Timesheet]:
    """Stop all currently running timers."""
    active = await self.active()
    backend = self._get_backend()

    for ts in active:
        result = await backend.stop_timer(ts, self._client)
        await self._handle_stop_wizard(result)

    return active