Initial chiffon
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user