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())