#!/usr/bin/env python3 """ Upload timesheet dari file pending yang dibuat daily_timesheet.py. Jalankan ini setelah menerima notifikasi macOS jam 16:55. """ import json import urllib.request import urllib.error import argparse import glob import os from datetime import date BASE_URL = "https://odoo.aplikasi.web.id" SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) def build_headers(session_id: str) -> dict: return { "accept": "*/*", "content-type": "application/json", "origin": BASE_URL, "referer": f"{BASE_URL}/web", "user-agent": "Mozilla/5.0", "cookie": f"frontend_lang=en_US; cids=1; session_id={session_id}; tz=Asia/Jakarta", } def check_session(session_id: str) -> bool: payload = {"jsonrpc": "2.0", "method": "call", "id": 1, "params": {}} url = f"{BASE_URL}/web/session/get_session_info" body = json.dumps(payload).encode("utf-8") req = urllib.request.Request(url, data=body, headers=build_headers(session_id), method="POST") try: with urllib.request.urlopen(req) as resp: response = json.loads(resp.read().decode("utf-8")) uid = response.get("result", {}).get("uid") return bool(uid) except Exception: return False def upload_timesheet(session_id: str, entry: dict) -> int: payload = { "id": 1, "jsonrpc": "2.0", "method": "call", "params": { "model": "account.analytic.line", "method": "create", "args": [entry], "kwargs": { "context": { "lang": "en_US", "tz": "Asia/Jakarta", "uid": entry["user_id"], "allowed_company_ids": [1], "is_timesheet": 1, } }, }, } url = f"{BASE_URL}/web/dataset/call_kw/account.analytic.line/create" body = json.dumps(payload).encode("utf-8") req = urllib.request.Request(url, data=body, headers=build_headers(session_id), method="POST") try: with urllib.request.urlopen(req) as resp: response = json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as e: raise RuntimeError(f"HTTP {e.code}: {e.read().decode()}") from e if "error" in response: err = response["error"] raise RuntimeError(f"Odoo error: {err.get('message')}") return response.get("result") def find_pending(target_date: str) -> str | None: path = os.path.join(SCRIPT_DIR, f"pending_{target_date}.json") if os.path.exists(path): return path # fallback: cari pending terbaru files = sorted(glob.glob(os.path.join(SCRIPT_DIR, "pending_*.json")), reverse=True) return files[0] if files else None def main(): today = date.today().isoformat() parser = argparse.ArgumentParser(description="Upload timesheet dari pending file") parser.add_argument("--session-id", required=True, help="session_id cookie Odoo") parser.add_argument("--date", default=today, help=f"Tanggal pending (default: {today})") args = parser.parse_args() pending_path = find_pending(args.date) if not pending_path: print("Tidak ada file pending ditemukan.") return if not check_session(args.session_id): print("SESSION EXPIRED — login ulang ke Odoo, ambil session_id baru dari browser,") print("lalu jalankan ulang dengan --session-id ") return with open(pending_path) as f: pending = json.load(f) entries = pending["entries"] print(f"File : {os.path.basename(pending_path)}") print(f"Tanggal : {pending['date']}") print(f"Entries : {len(entries)}") print() print(f" {'PROJECT_ID':<12} {'TASK_ID':<10} {'JAM':>5} DESKRIPSI") print(f" {'─' * 60}") for e in entries: print(f" {str(e['project_id']):<12} {str(e['task_id']):<10} {e['unit_amount']:>5.2f} {e['name'][:40]}") print(f" {'─' * 60}") print(f" {'TOTAL':<23} {sum(e['unit_amount'] for e in entries):>5.2f}h") confirm = input(f"\nUpload {len(entries)} entry ke Odoo? (y/N): ").strip().lower() if confirm != "y": print("Dibatalkan.") return print() ok, fail = 0, 0 for e in entries: try: new_id = upload_timesheet(args.session_id, e) print(f" OK ID {new_id:<8} task {e['task_id']} {e['unit_amount']}h {e['name'][:40]}") ok += 1 except RuntimeError as ex: print(f" ERR task {e['task_id']} {e['name'][:40]} → {ex}") fail += 1 if ok == len(entries): os.rename(pending_path, pending_path.replace("pending_", "done_")) print(f"\nSelesai: {ok} sukses. File dipindah ke done_*.") else: print(f"\nSelesai: {ok} sukses, {fail} gagal. File pending tidak dihapus.") if __name__ == "__main__": main()