Initial commit: bookkeeping skill

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>
This commit is contained in:
Knowit
2026-04-20 15:03:06 +08:00
commit 6110ef4bc2
8 changed files with 292 additions and 0 deletions

80
scripts/append_row.py Executable file
View File

@@ -0,0 +1,80 @@
#!/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())

46
scripts/fx_convert.py Executable file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
fx_convert.py <amount> <currency> [--date yyyy-mm-dd]
Convert a given amount in <currency> to HKD.
Prints one tab-separated line: <hkd_amount>\t<fx_rate>\t<fx_date>
Non-zero exit on failure.
Uses frankfurter.app by default: free, no API key, ECB reference rates,
historical dates supported via /{date} path.
"""
from __future__ import annotations
import argparse
import json
import sys
import urllib.request
from datetime import date
def fetch_rate(currency: str, on_date: str) -> tuple[float, str]:
currency = currency.upper()
if currency == "HKD":
return 1.0, on_date
url = f"https://api.frankfurter.app/{on_date}?from={currency}&to=HKD"
with urllib.request.urlopen(url, timeout=10) as resp:
data = json.loads(resp.read())
rate = data["rates"]["HKD"]
return float(rate), data["date"]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("amount", type=float)
ap.add_argument("currency")
ap.add_argument("--date", default=date.today().isoformat())
args = ap.parse_args()
rate, fx_date = fetch_rate(args.currency, args.date)
hkd = round(args.amount * rate, 2)
print(f"{hkd}\t{rate}\t{fx_date}")
return 0
if __name__ == "__main__":
sys.exit(main())

39
scripts/setup.md Normal file
View File

@@ -0,0 +1,39 @@
# One-time setup
## 1. Python deps
```
pip install google-api-python-client google-auth
```
## 2. Google Cloud service account
1. Create (or reuse) a GCP project.
2. Enable the **Google Sheets API** for the project.
3. Create a **service account**; skip the optional IAM steps.
4. In the service account, create a **JSON key** and download it.
5. Move the key to a safe path, e.g. `~/.config/gcp/bookkeeping-sa.json`, then:
```
chmod 600 ~/.config/gcp/bookkeeping-sa.json
```
## 3. Prepare the Google Sheet
1. Create a new Google Sheet (or open an existing one).
2. Rename the first tab to `Expenses` (or update `worksheet` in config).
3. In row 1 add headers matching `schema.md` columns AN:
`Date | Merchant | Category | Amount | Currency | Amount (HKD) | FX Rate | FX Date | Payment Method | Line Items | Raw OCR | Note | Receipt | Logged At`
4. Open the service account JSON and copy the `client_email` value (looks like `...@...iam.gserviceaccount.com`).
5. Click **Share** on the sheet and add that email as **Editor**.
6. Copy the sheet ID from the URL: `https://docs.google.com/spreadsheets/d/<SHEET_ID>/edit`.
## 4. Skill config
```
cd ~/.claude/skills/bookkeeping
cp config.example.json config.json
# edit config.json: sheet_id, service_account_path
```
## 5. Sanity check
```
echo '{"date":"2026-04-20","merchant":"TEST","category":"Other","amount":1,"currency":"HKD","amount_hkd":1,"fx_rate":1,"fx_date":"2026-04-20"}' \
| python ~/.claude/skills/bookkeeping/scripts/append_row.py
```
You should see `OK Expenses!A2:N2` (or similar) and a new row in the sheet. Delete the TEST row when done.