Initial chiffon

This commit is contained in:
Hubert Van De Walle
2026-08-21 11:17:21 +02:00
commit 415871ba6f
6 changed files with 594 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
from __future__ import annotations
import argparse
import os
import sys
from datetime import date
from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from .client import OdooClient, OdooError
from .schedule import DAY_NAMES, get_schedules, resolve_employees
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", type=parse_date, help="Start date, YYYY-MM-DD")
parser.add_argument("date_to", type=parse_date, help="End date, YYYY-MM-DD (inclusive)")
parser.add_argument("--url", default="http://localhost:1930", help="Odoo base URL")
parser.add_argument(
"--session-file",
default=Path(__file__).resolve().parents[2] / "session_id.txt",
type=Path,
help="Path to a file containing the session_id cookie value",
)
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",
)
args = parser.parse_args()
if args.date_from > args.date_to:
parser.error("date_from must be before or equal to date_to")
try:
client = OdooClient.from_session_file(args.url, args.session_file, debug=args.debug)
except (FileNotFoundError, 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
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())