Files
odoo-employee-schedule/src/odoo_employee_schedule/cli.py
T
2026-09-16 15:43:14 +02:00

186 lines
5.4 KiB
Python

from __future__ import annotations
import argparse
import os
import sys
from datetime import date
from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from rich.console import Console
from .client import OdooClient, OdooError
from .render import render_gantt
from .schedule import DAY_NAMES, get_schedules, resolve_employees
BASE_URL = "https://www.odoo.com"
def parse_date(value: str) -> date:
return date.fromisoformat(value)
def _valid_tz(name: str | None) -> str | None:
if not name:
return None
try:
ZoneInfo(name)
except (ZoneInfoNotFoundError, ValueError):
return None
return name
def detect_local_tz() -> str | None:
"""Best-effort IANA zone name for the machine this script runs on (stdlib only, POSIX).
Checks $TZ first, then resolves the /etc/localtime symlink most Linux/macOS
systems point at their zoneinfo file. Returns None if neither works (e.g.
Windows, or a system that copies the zoneinfo file instead of symlinking).
"""
tz = _valid_tz(os.environ.get("TZ"))
if tz:
return tz
localtime = Path("/etc/localtime")
try:
target = localtime.resolve()
except OSError:
return None
parts = target.parts
if "zoneinfo" in parts:
name = "/".join(parts[parts.index("zoneinfo") + 1 :])
return _valid_tz(name)
return None
def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Show when one or more employees are working in a date range, including "
"days off with no leave taken."
)
)
parser.add_argument(
"employees",
nargs="+",
help=(
"Employee ids and/or name tags (the parenthesized suffix on the real "
"server's employee names, e.g. 'mibr' for 'Michel Bram (mibr)'), space-separated"
),
)
parser.add_argument(
"--date-from",
dest="date_from",
type=parse_date,
default=None,
help="Start date, YYYY-MM-DD (default: today)",
)
parser.add_argument(
"--date-to",
dest="date_to",
type=parse_date,
default=None,
help="End date, YYYY-MM-DD, inclusive (default: same as --date-from)",
)
parser.add_argument(
"--tz",
default=None,
help="Timezone for day boundaries (default: detected from this machine's environment, "
"falling back to your Odoo session tz)",
)
parser.add_argument(
"--no-explain",
action="store_true",
help="Don't label why a day is off (leave / public holiday / weekly day off); skips the public-holiday lookup only, hr.leave is still read for correctness",
)
parser.add_argument(
"--include-weekends",
action="store_true",
help="Also print Saturdays and Sundays (skipped by default)",
)
parser.add_argument(
"--debug",
action="store_true",
help="Log each outgoing request (model/method/args, not the response) to stderr",
)
parser.add_argument(
"--gantt",
action="store_true",
help="Show a compact colored gantt-like grid (one row per employee) instead of the per-day text list",
)
parser.add_argument(
"--details",
action="store_true",
help="With --gantt, also print the Details section (specific leave/holiday names, pending "
"requests) below the grid; off by default",
)
args = parser.parse_args()
if args.date_from is None:
args.date_from = date.today()
if args.date_to is None:
args.date_to = args.date_from
if args.date_from > args.date_to:
parser.error("date_from must be before or equal to date_to")
try:
client = OdooClient.from_actions(BASE_URL, debug=args.debug)
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
tz_name = args.tz
if tz_name is None:
tz_name = detect_local_tz()
if tz_name is None:
try:
info = client.session_info()
tz_name = info.get("user_context", {}).get("tz") or "UTC"
except OdooError:
tz_name = "UTC"
try:
employees = resolve_employees(client, args.employees)
except (ValueError, OdooError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
try:
schedules = get_schedules(
client,
employees,
args.date_from,
args.date_to,
tz_name,
explain=not args.no_explain,
)
except OdooError as exc:
print(f"error calling Odoo: {exc}", file=sys.stderr)
return 1
if args.gantt:
console = Console()
console.print(f"{args.date_from} to {args.date_to} [{tz_name}]\n")
render_gantt(console, schedules, employees, args.include_weekends, show_details=args.details)
return 0
for i, emp in enumerate(employees):
if i:
print()
print(f"{emp.name} (id={emp.id}) -- {args.date_from} to {args.date_to} [{tz_name}]\n")
for d in schedules[emp.id]:
if not args.include_weekends and d.day.weekday() in (5, 6):
continue
weekday = DAY_NAMES[d.day.weekday()]
line = f"{d.day} {weekday:<9} {d.status}"
if d.notes:
line += f" ({'; '.join(d.notes)})"
print(line)
return 0
if __name__ == "__main__":
sys.exit(main())