183 lines
6.6 KiB
Python
183 lines
6.6 KiB
Python
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import re
|
|
from flask import Flask, render_template
|
|
|
|
# Add scripts directory to path to allow importing from it
|
|
scripts_dir = Path(__file__).parent / "scripts"
|
|
sys.path.append(str(scripts_dir))
|
|
|
|
from attendance import get_members_with_fees, SHEET_ID as ATTENDANCE_SHEET_ID
|
|
from match_payments import reconcile, fetch_sheet_data, fetch_exceptions, normalize, DEFAULT_SPREADSHEET_ID as PAYMENTS_SHEET_ID
|
|
|
|
app = Flask(__name__)
|
|
|
|
@app.route("/")
|
|
def index():
|
|
# Redirect root to /fees for convenience while there are no other apps
|
|
return '<meta http-equiv="refresh" content="0; url=/fees" />'
|
|
|
|
@app.route("/fees")
|
|
def fees():
|
|
attendance_url = f"https://docs.google.com/spreadsheets/d/{ATTENDANCE_SHEET_ID}/edit"
|
|
payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
|
|
|
|
members, sorted_months = get_members_with_fees()
|
|
if not members:
|
|
return "No data."
|
|
|
|
# Filter to adults only for display
|
|
results = [(name, fees) for name, tier, fees in members if tier == "A"]
|
|
|
|
# Format month labels
|
|
month_labels = {
|
|
m: datetime.strptime(m, "%Y-%m").strftime("%b %Y") for m in sorted_months
|
|
}
|
|
|
|
monthly_totals = {m: 0 for m in sorted_months}
|
|
|
|
# Get exceptions for formatting
|
|
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
|
exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path)
|
|
|
|
formatted_results = []
|
|
for name, month_fees in results:
|
|
row = {"name": name, "months": []}
|
|
norm_name = normalize(name)
|
|
for m in sorted_months:
|
|
fee, count = month_fees.get(m, (0, 0))
|
|
monthly_totals[m] += fee
|
|
|
|
# Check for exception
|
|
norm_period = normalize(m)
|
|
override = exceptions.get((norm_name, norm_period))
|
|
|
|
if override is not None and override != fee:
|
|
cell = f"{override} ({fee}) CZK ({count})" if count > 0 else f"{override} ({fee}) CZK"
|
|
is_overridden = True
|
|
else:
|
|
cell = f"{fee} CZK ({count})" if count > 0 else "-"
|
|
is_overridden = False
|
|
row["months"].append({"cell": cell, "overridden": is_overridden})
|
|
formatted_results.append(row)
|
|
|
|
return render_template(
|
|
"fees.html",
|
|
months=[month_labels[m] for m in sorted_months],
|
|
results=formatted_results,
|
|
totals=[f"{monthly_totals[m]} CZK" for m in sorted_months],
|
|
attendance_url=attendance_url,
|
|
payments_url=payments_url
|
|
)
|
|
|
|
@app.route("/reconcile")
|
|
def reconcile_view():
|
|
attendance_url = f"https://docs.google.com/spreadsheets/d/{ATTENDANCE_SHEET_ID}/edit"
|
|
payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
|
|
|
|
# Use hardcoded credentials path for now, consistent with other scripts
|
|
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
|
|
|
members, sorted_months = get_members_with_fees()
|
|
if not members:
|
|
return "No data."
|
|
|
|
transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path)
|
|
exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path)
|
|
result = reconcile(members, sorted_months, transactions, exceptions)
|
|
|
|
# Format month labels
|
|
month_labels = {
|
|
m: datetime.strptime(m, "%Y-%m").strftime("%b %Y") for m in sorted_months
|
|
}
|
|
|
|
# Filter to adults for the main table
|
|
adult_names = sorted([name for name, tier, _ in members if tier == "A"])
|
|
|
|
formatted_results = []
|
|
for name in adult_names:
|
|
data = result["members"][name]
|
|
row = {"name": name, "months": [], "balance": data["total_balance"]}
|
|
for m in sorted_months:
|
|
mdata = data["months"].get(m, {"expected": 0, "original_expected": 0, "paid": 0})
|
|
expected = mdata["expected"]
|
|
original = mdata["original_expected"]
|
|
paid = int(mdata["paid"])
|
|
|
|
cell_status = ""
|
|
if expected == 0 and paid == 0:
|
|
cell = "-"
|
|
elif paid >= expected and expected > 0:
|
|
cell = "OK"
|
|
elif paid > 0:
|
|
cell = f"{paid}/{expected}"
|
|
else:
|
|
cell = f"UNPAID {expected}"
|
|
|
|
row["months"].append(cell)
|
|
|
|
row["balance"] = data["total_balance"] # Updated to use total_balance
|
|
formatted_results.append(row)
|
|
|
|
# Format credits and debts
|
|
credits = sorted([{"name": n, "amount": a["total_balance"]} for n, a in result["members"].items() if a["total_balance"] > 0], key=lambda x: x["name"])
|
|
debts = sorted([{"name": n, "amount": abs(a["total_balance"])} for n, a in result["members"].items() if a["total_balance"] < 0], key=lambda x: x["name"])
|
|
# Format unmatched
|
|
unmatched = result["unmatched"]
|
|
import json
|
|
|
|
return render_template(
|
|
"reconcile.html",
|
|
months=[month_labels[m] for m in sorted_months],
|
|
raw_months=sorted_months,
|
|
results=formatted_results,
|
|
member_data=json.dumps(result["members"]),
|
|
credits=credits,
|
|
debts=debts,
|
|
unmatched=unmatched,
|
|
attendance_url=attendance_url,
|
|
payments_url=payments_url
|
|
)
|
|
|
|
@app.route("/payments")
|
|
def payments():
|
|
attendance_url = f"https://docs.google.com/spreadsheets/d/{ATTENDANCE_SHEET_ID}/edit"
|
|
payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
|
|
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
|
|
|
transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path)
|
|
|
|
# Group transactions by person
|
|
grouped = {}
|
|
for tx in transactions:
|
|
person = str(tx.get("person", "")).strip()
|
|
if not person:
|
|
person = "Unmatched / Unknown"
|
|
|
|
# Handle multiple people (comma separated)
|
|
people = [p.strip() for p in person.split(",") if p.strip()]
|
|
for p in people:
|
|
# Strip markers
|
|
clean_p = re.sub(r"\[\?\]\s*", "", p)
|
|
if clean_p not in grouped:
|
|
grouped[clean_p] = []
|
|
grouped[clean_p].append(tx)
|
|
|
|
# Sort people and their transactions
|
|
sorted_people = sorted(grouped.keys())
|
|
for p in sorted_people:
|
|
# Sort by date descending
|
|
grouped[p].sort(key=lambda x: str(x.get("date", "")), reverse=True)
|
|
|
|
return render_template(
|
|
"payments.html",
|
|
grouped_payments=grouped,
|
|
sorted_people=sorted_people,
|
|
attendance_url=attendance_url,
|
|
payments_url=payments_url
|
|
)
|
|
|
|
if __name__ == "__main__":
|
|
app.run(debug=True, host='0.0.0.0', port=5001)
|