Receipt-image to Google Sheets expense logger with HKD conversion. Includes SKILL.md, categories/schema reference, config template, and Python scripts for FX conversion (frankfurter.app) and Sheets append. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
2.2 KiB
Python
Executable File
81 lines
2.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
append_row.py — append one expense row to the configured Google Sheet.
|
|
|
|
Reads config from ../config.json.
|
|
Reads a single JSON object from stdin. Keys (all optional; missing -> ""):
|
|
date, merchant, category, amount, currency,
|
|
amount_hkd, fx_rate, fx_date, payment_method,
|
|
line_items, raw_ocr, note, receipt
|
|
|
|
`logged_at` is set automatically to now (UTC, ISO-8601).
|
|
|
|
Prints the updated range on success; exits non-zero on failure.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from google.oauth2.service_account import Credentials
|
|
from googleapiclient.discovery import build
|
|
|
|
|
|
CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.json"
|
|
|
|
COLUMNS = [
|
|
"date", "merchant", "category", "amount", "currency",
|
|
"amount_hkd", "fx_rate", "fx_date", "payment_method",
|
|
"line_items", "raw_ocr", "note", "receipt", "logged_at",
|
|
]
|
|
|
|
|
|
def load_config() -> dict:
|
|
if not CONFIG_PATH.exists():
|
|
sys.exit(
|
|
f"config.json not found at {CONFIG_PATH}. "
|
|
f"Copy config.example.json to config.json and fill it in."
|
|
)
|
|
cfg = json.loads(CONFIG_PATH.read_text())
|
|
cfg["service_account_path"] = os.path.expanduser(cfg["service_account_path"])
|
|
return cfg
|
|
|
|
|
|
def main() -> int:
|
|
cfg = load_config()
|
|
row = json.loads(sys.stdin.read())
|
|
row.setdefault(
|
|
"logged_at",
|
|
datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
)
|
|
|
|
values = [str(row.get(col, "")) for col in COLUMNS]
|
|
|
|
creds = Credentials.from_service_account_file(
|
|
cfg["service_account_path"],
|
|
scopes=["https://www.googleapis.com/auth/spreadsheets"],
|
|
)
|
|
svc = build("sheets", "v4", credentials=creds, cache_discovery=False)
|
|
resp = (
|
|
svc.spreadsheets()
|
|
.values()
|
|
.append(
|
|
spreadsheetId=cfg["sheet_id"],
|
|
range=f"{cfg['worksheet']}!A1",
|
|
valueInputOption="USER_ENTERED",
|
|
insertDataOption="INSERT_ROWS",
|
|
body={"values": [values]},
|
|
)
|
|
.execute()
|
|
)
|
|
updated = resp.get("updates", {}).get("updatedRange", "?")
|
|
print(f"OK {updated}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|