feat: add member details popup with attendance and fee exceptions
This commit is contained in:
@@ -233,10 +233,49 @@ def fetch_sheet_data(spreadsheet_id: str, credentials_path: str) -> list[dict]:
|
||||
return transactions
|
||||
|
||||
|
||||
def fetch_exceptions(spreadsheet_id: str, credentials_path: str) -> dict[tuple[str, str], dict]:
|
||||
"""Fetch manual fee overrides from the 'exceptions' sheet.
|
||||
|
||||
Returns a dict mapping (member_name, period_YYYYMM) to {'amount': int, 'note': str}.
|
||||
"""
|
||||
service = get_sheets_service(credentials_path)
|
||||
try:
|
||||
result = service.spreadsheets().values().get(
|
||||
spreadsheetId=spreadsheet_id,
|
||||
range="'exceptions'!A2:D",
|
||||
valueRenderOption="UNFORMATTED_VALUE"
|
||||
).execute()
|
||||
rows = result.get("values", [])
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not fetch exceptions: {e}")
|
||||
return {}
|
||||
|
||||
exceptions = {}
|
||||
for row in rows:
|
||||
if len(row) < 3 or str(row[0]).lower().startswith("name"):
|
||||
continue
|
||||
|
||||
name = str(row[0]).strip()
|
||||
period = str(row[1]).strip()
|
||||
# Robust normalization using czech_utils.normalize
|
||||
norm_name = normalize(name)
|
||||
norm_period = normalize(period)
|
||||
|
||||
try:
|
||||
amount = int(row[2])
|
||||
note = str(row[3]).strip() if len(row) > 3 else ""
|
||||
exceptions[(norm_name, norm_period)] = {"amount": amount, "note": note}
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
return exceptions
|
||||
|
||||
|
||||
def reconcile(
|
||||
members: list[tuple[str, str, dict[str, int]]],
|
||||
sorted_months: list[str],
|
||||
transactions: list[dict],
|
||||
exceptions: dict[tuple[str, str], dict] = None,
|
||||
) -> dict:
|
||||
"""Match transactions to members and months.
|
||||
|
||||
@@ -251,11 +290,30 @@ def reconcile(
|
||||
|
||||
# Initialize ledger
|
||||
ledger: dict[str, dict[str, dict]] = {}
|
||||
exceptions = exceptions or {}
|
||||
for name in member_names:
|
||||
ledger[name] = {}
|
||||
for m in sorted_months:
|
||||
# Robust normalization for lookup
|
||||
norm_name = normalize(name)
|
||||
norm_period = normalize(m)
|
||||
fee_data = member_fees[name].get(m, (0, 0))
|
||||
original_expected = fee_data[0] if isinstance(fee_data, tuple) else fee_data
|
||||
attendance_count = fee_data[1] if isinstance(fee_data, tuple) else 0
|
||||
|
||||
ex_data = exceptions.get((norm_name, norm_period))
|
||||
if ex_data is not None:
|
||||
expected = ex_data["amount"]
|
||||
exception_info = ex_data
|
||||
else:
|
||||
expected = original_expected
|
||||
exception_info = None
|
||||
|
||||
ledger[name][m] = {
|
||||
"expected": member_fees[name].get(m, 0),
|
||||
"expected": expected,
|
||||
"original_expected": original_expected,
|
||||
"attendance_count": attendance_count,
|
||||
"exception": exception_info,
|
||||
"paid": 0,
|
||||
"transactions": [],
|
||||
}
|
||||
@@ -392,10 +450,12 @@ def print_report(result: dict, sorted_months: list[str]):
|
||||
for m in sorted_months:
|
||||
mdata = data["months"].get(m, {"expected": 0, "paid": 0})
|
||||
expected = mdata["expected"]
|
||||
original = mdata["original_expected"]
|
||||
paid = int(mdata["paid"])
|
||||
total_expected += expected
|
||||
total_paid += paid
|
||||
|
||||
|
||||
cell_status = ""
|
||||
if expected == 0 and paid == 0:
|
||||
cell = "-"
|
||||
elif paid >= expected and expected > 0:
|
||||
@@ -404,6 +464,7 @@ def print_report(result: dict, sorted_months: list[str]):
|
||||
cell = f"{paid}/{expected}"
|
||||
else:
|
||||
cell = f"UNPAID {expected}"
|
||||
|
||||
member_balance += paid - expected
|
||||
line += f" | {cell:>10}"
|
||||
balance_str = f"{member_balance:+d}" if member_balance != 0 else "0"
|
||||
@@ -509,7 +570,11 @@ def main():
|
||||
|
||||
print(f"Processing {len(transactions)} transactions.\n")
|
||||
|
||||
result = reconcile(members, sorted_months, transactions)
|
||||
exceptions = fetch_exceptions(args.sheet_id, args.credentials)
|
||||
if exceptions:
|
||||
print(f"Loaded {len(exceptions)} fee exceptions.")
|
||||
|
||||
result = reconcile(members, sorted_months, transactions, exceptions)
|
||||
print_report(result, sorted_months)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user