"""
Attendance API – Zoho People check-in/out and attendance reports.
Endpoints covered
-----------------
- ``POST attendance`` – single check-in / check-out
- ``POST attendance/bulkImport`` – bulk attendance import
- ``GET attendance/getUserReport`` – attendance report for a period
- ``GET attendance/getAttendanceEntries`` – daily clock entries
- ``GET attendance/getShiftConfiguration`` – shift configuration
Scope required: ``ZOHOPEOPLE.attendance.ALL``
.. note::
The check-in / check-out write endpoints require the authenticated user to
have the *"Check-in/Check-out"* or *"Regularize Attendance"* permission
enabled in Zoho People → Settings → Attendance → Permissions.
Read endpoints (``getUserReport``) work for all roles.
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from ..client import ZohoPeopleClient
#: Date-time format expected by the check-in / check-out API.
CHECKIN_DATE_FORMAT: str = "dd/MM/yyyy HH:mm:ss"
[docs]
class AttendanceAPI:
"""
Read and write attendance records in Zoho People.
Obtain an instance via :attr:`ZohoPeopleClient.attendance`.
Examples
--------
Record a single check-in and check-out::
client.attendance.checkin(
email_id="mario.rossi@company.com",
checkin="25/04/2026 09:00:00",
checkout="25/04/2026 18:00:00",
)
Fetch the monthly attendance report::
report = client.attendance.get_user_report(
start_date="01/04/2026",
end_date="30/04/2026",
date_format="dd/MM/yyyy",
)
"""
def __init__(self, client: "ZohoPeopleClient") -> None:
self._client = client
# ------------------------------------------------------------------
# Check-in / Check-out
# ------------------------------------------------------------------
[docs]
def checkin(
self,
checkin: str,
checkout: Optional[str] = None,
*,
emp_id: Optional[str] = None,
email_id: Optional[str] = None,
map_id: Optional[str] = None,
date_format: str = CHECKIN_DATE_FORMAT,
) -> dict[str, Any]:
"""
Record a check-in (and optionally a check-out) for an employee.
Parameters
----------
checkin:
Check-in datetime string, e.g. ``"25/04/2026 09:00:00"``.
checkout:
Check-out datetime string. Omit to record only the entry.
emp_id:
Employee ID (e.g. ``"EMP001"`` or ``"IMP085"``).
email_id:
Employee e-mail address.
map_id:
Biometric device mapper ID.
date_format:
Datetime format (default ``"dd/MM/yyyy HH:mm:ss"``).
Returns
-------
dict
Zoho API response.
Raises
------
ValueError
If none of *emp_id*, *email_id*, *map_id* is provided.
"""
if not any([emp_id, email_id, map_id]):
raise ValueError("At least one of emp_id, email_id, or map_id is required.")
data: dict[str, Any] = {"dateFormat": date_format, "checkIn": checkin}
if checkout: data["checkOut"] = checkout
if emp_id: data["empId"] = emp_id
if email_id: data["emailId"] = email_id
if map_id: data["mapId"] = map_id
return self._client.post("attendance", data=data)
[docs]
def checkout(
self,
checkout: str,
*,
emp_id: Optional[str] = None,
email_id: Optional[str] = None,
map_id: Optional[str] = None,
date_format: str = CHECKIN_DATE_FORMAT,
) -> dict[str, Any]:
"""
Record a standalone check-out (employee already checked in).
Raises
------
ValueError
If none of *emp_id*, *email_id*, *map_id* is provided.
"""
if not any([emp_id, email_id, map_id]):
raise ValueError("At least one of emp_id, email_id, or map_id is required.")
data: dict[str, Any] = {"dateFormat": date_format, "checkOut": checkout}
if emp_id: data["empId"] = emp_id
if email_id: data["emailId"] = email_id
if map_id: data["mapId"] = map_id
return self._client.post("attendance", data=data)
# ------------------------------------------------------------------
# Bulk import
# ------------------------------------------------------------------
[docs]
def bulk_import(
self,
records: list[dict[str, Any]],
date_format: str = "yyyy-MM-dd HH:mm:ss",
) -> dict[str, Any]:
"""
Import multiple attendance records in a single request.
Zoho processes check-in and check-out as **separate entries**, so each
record should contain either ``checkIn`` or ``checkOut`` (not both)::
records = [
{"checkIn": "2026-04-01 09:00:00"},
{"checkOut": "2026-04-01 18:00:00"},
]
client.attendance.bulk_import(records)
Parameters
----------
records:
List of check-in / check-out dicts.
date_format:
Datetime format for all records (default ``"yyyy-MM-dd HH:mm:ss"``).
Returns
-------
dict
Import result; may contain an ``errorDates`` key on partial failure.
"""
data = {"data": json.dumps(records), "dateFormat": date_format}
result = self._client.post("attendance/bulkImport", data=data)
return result if isinstance(result, dict) else {}
# ------------------------------------------------------------------
# Reports
# ------------------------------------------------------------------
[docs]
def get_user_report(
self,
start_date: str,
end_date: str,
*,
emp_id: Optional[str] = None,
email_id: Optional[str] = None,
map_id: Optional[str] = None,
date_format: Optional[str] = None,
start_index: int = 0,
) -> list[dict[str, Any]]:
"""
Retrieve the attendance report for a date range.
Without an employee identifier the response covers the authenticated user.
Paginate with *start_index* in steps of 100.
Parameters
----------
start_date:
Period start (e.g. ``"01/04/2026"``).
end_date:
Period end.
date_format:
Date format if different from the organisation default.
start_index:
Pagination offset (0, 100, 200, …).
Returns
-------
list[dict]
Each item contains ``employeeDetails`` and ``attendanceDetails``.
"""
params: dict[str, Any] = {
"sdate": start_date,
"edate": end_date,
"startIndex": start_index,
}
if emp_id: params["empId"] = emp_id
if email_id: params["emailId"] = email_id
if map_id: params["mapId"] = map_id
if date_format: params["dateFormat"] = date_format
result = self._client.get("attendance/getUserReport", params=params)
if isinstance(result, list):
return result
return result.get("result", []) if isinstance(result, dict) else []
[docs]
def get_entries(
self,
date: str,
*,
emp_id: Optional[str] = None,
email_id: Optional[str] = None,
erecno: Optional[str] = None,
map_id: Optional[str] = None,
date_format: Optional[str] = None,
) -> list[dict[str, Any]]:
"""
Retrieve individual clock events (in/out timestamps) for a single day.
Parameters
----------
date:
Day string in the organisation's format or *date_format*.
Returns
-------
list[dict]
Clock entries with timestamps and metadata.
"""
params: dict[str, Any] = {"date": date}
if emp_id: params["empId"] = emp_id
if email_id: params["emailId"] = email_id
if erecno: params["erecno"] = erecno
if map_id: params["mapId"] = map_id
if date_format: params["dateFormat"] = date_format
result = self._client.get("attendance/getAttendanceEntries", params=params)
if isinstance(result, list):
return result
return result.get("result", []) if isinstance(result, dict) else []
# ------------------------------------------------------------------
# Shifts
# ------------------------------------------------------------------
[docs]
def get_shift_configuration(
self,
start_date: str,
end_date: str,
*,
emp_id: Optional[str] = None,
email_id: Optional[str] = None,
map_id: Optional[str] = None,
) -> dict[str, Any]:
"""
Retrieve the shift configuration for an employee over a period.
Parameters
----------
start_date:
Period start in ``yyyy-MM-dd`` format.
end_date:
Period end in ``yyyy-MM-dd`` format.
Returns
-------
dict
Shift details: name, start/end times, weekends, public holidays.
"""
params: dict[str, Any] = {"sdate": start_date, "edate": end_date}
if emp_id: params["empId"] = emp_id
if email_id: params["emailId"] = email_id
if map_id: params["mapId"] = map_id
result = self._client.get("attendance/getShiftConfiguration", params=params)
return result if isinstance(result, dict) else {}