From ced238385ee1193827eaa16e6f22a4fd520f9f47 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Thu, 9 Apr 2026 13:51:37 +0200 Subject: [PATCH] feat: Exclude current month from Pay buttons and balance Hide Pay/Pay All buttons for months still in progress, exclude current month debt from balance column, and show in-progress month debt in a muted red color. Co-Authored-By: Claude Opus 4.6 --- app.py | 102 +++++++++++++++++++++++++++++++---------- templates/adults.html | 14 ++++-- templates/juniors.html | 14 ++++-- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/app.py b/app.py index 1b1ff29..9625b09 100644 --- a/app.py +++ b/app.py @@ -183,7 +183,8 @@ def adults_view(): month_labels = get_month_labels(sorted_months, ADULT_MERGED_MONTHS) adult_names = sorted([name for name, tier, _ in members if tier == "A"]) - + current_month = datetime.now().strftime("%Y-%m") + monthly_totals = {m: {"expected": 0, "paid": 0} for m in sorted_months} formatted_results = [] for name in adult_names: @@ -191,6 +192,7 @@ def adults_view(): row = {"name": name, "months": [], "balance": data["total_balance"], "unpaid_periods": "", "raw_unpaid_periods": ""} unpaid_months = [] raw_unpaid_months = [] + payable_amount = 0 for m in sorted_months: mdata = data["months"].get(m, {"expected": 0, "original_expected": 0, "attendance_count": 0, "paid": 0, "exception": None}) expected = mdata.get("expected", 0) @@ -223,13 +225,17 @@ def adults_view(): elif paid > 0: status = "partial" cell_text = f"{paid}/{fee_display}" - unpaid_months.append(month_labels[m]) - raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + if m < current_month: + unpaid_months.append(month_labels[m]) + raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + payable_amount += amount_to_pay else: status = "unpaid" cell_text = f"0/{fee_display}" - unpaid_months.append(month_labels[m]) - raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + if m < current_month: + unpaid_months.append(month_labels[m]) + raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + payable_amount += amount_to_pay elif paid > 0: status = "surplus" cell_text = f"PAID {paid}" @@ -252,11 +258,22 @@ def adults_view(): "tooltip": tooltip }) - row["unpaid_periods"] = ", ".join(unpaid_months) if unpaid_months else ("Older debt" if data["total_balance"] < 0 else "") + # Compute balance excluding current/future months + current_month_debt = 0 + for m in sorted_months: + if m >= current_month: + mdata = data["months"].get(m, {"expected": 0, "paid": 0}) + exp = mdata.get("expected", 0) + pd = int(mdata.get("paid", 0)) + current_month_debt += max(0, exp - pd) + settled_balance = data["total_balance"] + current_month_debt + + row["unpaid_periods"] = ", ".join(unpaid_months) if unpaid_months else ("Older debt" if settled_balance < 0 and payable_amount == 0 else "") row["raw_unpaid_periods"] = "+".join(raw_unpaid_months) - row["balance"] = data["total_balance"] + row["balance"] = settled_balance + row["payable_amount"] = payable_amount formatted_results.append(row) - + formatted_totals = [] for m in sorted_months: t = monthly_totals[m] @@ -268,14 +285,19 @@ def adults_view(): status = "unpaid" else: status = "surplus" - + formatted_totals.append({ "text": f"{t['paid']} / {t['expected']} CZK", "status": status }) - credits = sorted([{"name": n, "amount": a["total_balance"]} for n, a in result["members"].items() if a["total_balance"] > 0 and n in adult_names], 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 and n in adult_names], key=lambda x: x["name"]) + def settled_balance(name): + data = result["members"][name] + debt = sum(max(0, data["months"].get(m, {"expected": 0, "paid": 0}).get("expected", 0) - int(data["months"].get(m, {"expected": 0, "paid": 0}).get("paid", 0))) for m in sorted_months if m >= current_month) + return data["total_balance"] + debt + + credits = sorted([{"name": n, "amount": settled_balance(n)} for n in adult_names if settled_balance(n) > 0], key=lambda x: x["name"]) + debts = sorted([{"name": n, "amount": abs(settled_balance(n))} for n in adult_names if settled_balance(n) < 0], key=lambda x: x["name"]) unmatched = result["unmatched"] import json @@ -294,7 +316,8 @@ def adults_view(): unmatched=unmatched, attendance_url=attendance_url, payments_url=payments_url, - bank_account=BANK_ACCOUNT + bank_account=BANK_ACCOUNT, + current_month=current_month ) @app.route("/juniors") @@ -341,7 +364,8 @@ def juniors_view(): month_labels = get_month_labels(sorted_months, JUNIOR_MERGED_MONTHS) junior_names = sorted([name for name, tier, _ in adapted_members]) junior_members_dict = {name: fees_dict for name, _, fees_dict in junior_members} - + current_month = datetime.now().strftime("%Y-%m") + monthly_totals = {m: {"expected": 0, "paid": 0} for m in sorted_months} formatted_results = [] for name in junior_names: @@ -349,6 +373,7 @@ def juniors_view(): row = {"name": name, "months": [], "balance": data["total_balance"], "unpaid_periods": "", "raw_unpaid_periods": ""} unpaid_months = [] raw_unpaid_months = [] + payable_amount = 0 for m in sorted_months: mdata = data["months"].get(m, {"expected": 0, "original_expected": 0, "attendance_count": 0, "paid": 0, "exception": None}) expected = mdata.get("expected", 0) @@ -401,14 +426,18 @@ def juniors_view(): status = "partial" cell_text = f"{paid}/{fee_display}" amount_to_pay = expected - paid - unpaid_months.append(month_labels[m]) - raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + if m < current_month: + unpaid_months.append(month_labels[m]) + raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + payable_amount += amount_to_pay else: status = "unpaid" cell_text = f"0/{fee_display}" amount_to_pay = expected - unpaid_months.append(month_labels[m]) - raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + if m < current_month: + unpaid_months.append(month_labels[m]) + raw_unpaid_months.append(datetime.strptime(m, "%Y-%m").strftime("%m/%Y")) + payable_amount += amount_to_pay elif paid > 0: status = "surplus" cell_text = f"PAID {paid}" @@ -428,11 +457,23 @@ def juniors_view(): "tooltip": tooltip }) - row["unpaid_periods"] = ", ".join(unpaid_months) if unpaid_months else ("Older debt" if data["total_balance"] < 0 else "") + # Compute balance excluding current/future months + current_month_debt = 0 + for m in sorted_months: + if m >= current_month: + mdata = data["months"].get(m, {"expected": 0, "paid": 0}) + exp = mdata.get("expected", 0) + if isinstance(exp, int): + pd = int(mdata.get("paid", 0)) + current_month_debt += max(0, exp - pd) + settled_balance = data["total_balance"] + current_month_debt + + row["unpaid_periods"] = ", ".join(unpaid_months) if unpaid_months else ("Older debt" if settled_balance < 0 and payable_amount == 0 else "") row["raw_unpaid_periods"] = "+".join(raw_unpaid_months) - row["balance"] = data["total_balance"] + row["balance"] = settled_balance + row["payable_amount"] = payable_amount formatted_results.append(row) - + formatted_totals = [] for m in sorted_months: t = monthly_totals[m] @@ -444,15 +485,27 @@ def juniors_view(): status = "unpaid" else: status = "surplus" - + formatted_totals.append({ "text": f"{t['paid']} / {t['expected']} CZK", "status": status }) # 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"]) + def junior_settled_balance(name): + data = result["members"][name] + debt = 0 + for m in sorted_months: + if m >= current_month: + mdata = data["months"].get(m, {"expected": 0, "paid": 0}) + exp = mdata.get("expected", 0) + if isinstance(exp, int): + debt += max(0, exp - int(mdata.get("paid", 0))) + return data["total_balance"] + debt + + junior_all_names = [name for name, _, _ in adapted_members] + credits = sorted([{"name": n, "amount": junior_settled_balance(n)} for n in junior_all_names if junior_settled_balance(n) > 0], key=lambda x: x["name"]) + debts = sorted([{"name": n, "amount": abs(junior_settled_balance(n))} for n in junior_all_names if junior_settled_balance(n) < 0], key=lambda x: x["name"]) unmatched = result["unmatched"] import json @@ -471,7 +524,8 @@ def juniors_view(): unmatched=unmatched, attendance_url=attendance_url, payments_url=payments_url, - bank_account=BANK_ACCOUNT + bank_account=BANK_ACCOUNT, + current_month=current_month ) @app.route("/payments") diff --git a/templates/adults.html b/templates/adults.html index 47b0394..9597cb6 100644 --- a/templates/adults.html +++ b/templates/adults.html @@ -167,6 +167,12 @@ position: relative; } + .cell-unpaid-current { + color: #994444; + background-color: rgba(153, 68, 68, 0.05); + position: relative; + } + .cell-overridden { color: #ffa500 !important; } @@ -532,9 +538,9 @@ {% for cell in row.months %} + class="{% if cell.status == 'empty' %}cell-empty{% elif (cell.status == 'unpaid' or cell.status == 'partial') and cell.raw_month >= current_month %}cell-unpaid-current{% elif cell.status == 'unpaid' or cell.status == 'partial' %}cell-unpaid{% elif cell.status == 'ok' %}cell-ok{% endif %}{% if cell.overridden %} cell-overridden{% endif %}"> {{ cell.text }} - {% if cell.status == 'unpaid' or cell.status == 'partial' %} + {% if (cell.status == 'unpaid' or cell.status == 'partial') and cell.raw_month < current_month %} {% endif %} @@ -542,9 +548,9 @@ {% endfor %} {{ "%+d"|format(row.balance) if row.balance != 0 else "0" }} - {% if row.balance < 0 %} + {% if row.payable_amount > 0 %} + onclick="showPayQR('{{ row.name|e }}', {{ row.payable_amount }}, '{{ row.unpaid_periods|e }}', '{{ row.raw_unpaid_periods|e }}')">Pay All {% endif %} diff --git a/templates/juniors.html b/templates/juniors.html index 055700f..e90ce10 100644 --- a/templates/juniors.html +++ b/templates/juniors.html @@ -167,6 +167,12 @@ position: relative; } + .cell-unpaid-current { + color: #994444; + background-color: rgba(153, 68, 68, 0.05); + position: relative; + } + .cell-overridden { color: #ffa500 !important; } @@ -532,9 +538,9 @@ {% for cell in row.months %} + class="{% if cell.status == 'empty' %}cell-empty{% elif (cell.status == 'unpaid' or cell.status == 'partial') and cell.raw_month >= current_month %}cell-unpaid-current{% elif cell.status == 'unpaid' or cell.status == 'partial' %}cell-unpaid{% elif cell.status == 'ok' %}cell-ok{% endif %}{% if cell.overridden %} cell-overridden{% endif %}"> {{ cell.text }} - {% if cell.status == 'unpaid' or cell.status == 'partial' %} + {% if (cell.status == 'unpaid' or cell.status == 'partial') and cell.raw_month < current_month %} {% endif %} @@ -542,9 +548,9 @@ {% endfor %} {{ "%+d"|format(row.balance) if row.balance != 0 else "0" }} - {% if row.balance < 0 %} + {% if row.payable_amount > 0 %} + onclick="showPayQR('{{ row.name|e }}', {{ row.payable_amount }}, '{{ row.unpaid_periods|e }}', '{{ row.raw_unpaid_periods|e }}')">Pay All {% endif %}