From 6033a47b6ad0d24cd35794327c41a83f4316d72a Mon Sep 17 00:00:00 2001 From: Kumi Date: Wed, 29 May 2024 14:50:52 +0200 Subject: [PATCH] feat: enhance financial transparency with dynamic tables Implemented dynamic financial transparency tables to automatically generate and display monthly financial data for memberships, including incomes, expenses, and balances across multiple currencies. This change involved adding a new `finances.py` helper to process financial data and integrate it with the existing Flask application. Additionally, adjusted the CSS for better alignment and readability of currency columns in the transparency tables. - Introduced a `generate_transparency_table` function to generate HTML tables dynamically based on the latest financial data. - Expanded the `main.py` Flask route for the membership page to include financial data rendering, ensuring up-to-date information is presented to users. - Removed static HTML table from the membership template in favor of dynamically generated content, offering real-time insight into finances. - Adjusted the width and text alignment of currency columns in `base.css` for enhanced table aesthetics and readability. This update significantly improves the transparency of financial information, making it easier for members to understand the flow of funds within the organization. --- assets/css/base.css | 3 +- helpers/finances.py | 193 ++++++++++++++++++++++++++++++++++++++ main.py | 26 ++++- templates/membership.html | 84 +---------------- 4 files changed, 219 insertions(+), 87 deletions(-) create mode 100644 helpers/finances.py diff --git a/assets/css/base.css b/assets/css/base.css index 6bb4bc4..60880c9 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -86,8 +86,9 @@ h5 { } .currency-col { - width: 175px; + width: 200px; white-space: nowrap; + text-align: right; } .table-transparency td:not(:first-child) { diff --git a/helpers/finances.py b/helpers/finances.py new file mode 100644 index 0000000..32b5647 --- /dev/null +++ b/helpers/finances.py @@ -0,0 +1,193 @@ +from decimal import Decimal + + +def get_transparency_data(data, year=None, month=None): + if year is None: + year = max(data.keys()) + + if month is None: + month = max(data[year].keys()) + + assert year in data, f"Year {year} not found in data" + assert month in data[year], f"Month {month}-{year} not found in data" + + # Initialize balances + balances = {} + incomes = {} + expenses = {} + + start_balance = {} + end_balance = {} + + for y in sorted(data.keys()): + if int(y) > int(year): + break + + for m in sorted(data[y].keys()): + if int(y) == int(year) and int(m) > int(month): + break + + # If the month is the one we are interested in, capture the start balance + if int(y) == int(year) and int(m) == int(month): + start_balance = {k: Decimal(v) for k, v in balances.items()} + + for category in data[y][m]: + for currency, amount in data[y][m][category].items(): + if currency not in balances: + balances[currency] = Decimal(0) + balances[currency] += Decimal(str(amount)) + + # Track incomes and expenses + if int(y) == int(year) and int(m) == int(month): + if Decimal(str(amount)) > 0: + if category not in incomes: + incomes[category] = {} + if currency not in incomes[category]: + incomes[category][currency] = Decimal(0) + incomes[category][currency] += Decimal(str(amount)) + else: + if category not in expenses: + expenses[category] = {} + if currency not in expenses[category]: + expenses[category][currency] = Decimal(0) + expenses[category][currency] += Decimal(str(amount)) + + # If the month is the one we are interested in, capture the end balance + if int(y) == int(year) and int(m) == int(month): + end_balance = {k: Decimal(v) for k, v in balances.items()} + + # Calculate accumulated sums of incomes and expenses + accumulated_incomes = { + currency: sum(incomes[cat].get(currency, Decimal(0)) for cat in incomes) + for currency in balances + } + accumulated_expenses = { + currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses) + for currency in balances + } + + return { + "start_balance": start_balance, + "end_balance": end_balance, + "incomes": incomes, + "expenses": expenses, + "accumulated_incomes": accumulated_incomes, + "accumulated_expenses": accumulated_expenses, + } + + +def generate_transparency_table(result, currencies=None): + def extract_currencies(data): + return ["EUR"] + ( + list( + set( + list(data["start_balance"].keys()) + + list(data["end_balance"].keys()) + + list(data["accumulated_incomes"].keys()) + + list(data["accumulated_expenses"].keys()) + ) + - {"EUR"} + ) + ) + + def format_currency(value, currency): + if currency == "EUR": + return f"€{value:,.2f}" + elif currency in ["BTC", "ETH", "XMR"]: + return f"{value:,.9f} {currency}" + else: + return f"{value} {currency}" + + def format_value(value, currency): + if value == 0: + return f"{format_currency(value, currency)}" + elif value > 0: + return f"+ {format_currency(value, currency)}" + else: + return f"- {format_currency(abs(value), currency)}" + + html = """ + + + + + """ + + if currencies is None: + currencies = extract_currencies(result) + + # Add currency headers + for currency in currencies: + if currency == "EUR": + html += '' + elif currency == "BTC": + html += '' + elif currency == "ETH": + html += '' + elif currency == "XMR": + html += '' + else: + html += f'' + + html += """ + + + + """ + + # Add start balance row + html += "" + for currency in currencies: + value = result["start_balance"].get(currency, Decimal(0)) + html += f"" + html += "" + + # Add income rows + for category, transactions in result["incomes"].items(): + html += f"" + for currency in currencies: + value = transactions.get(currency, "") + if value != "": + html += f"" + else: + html += "" + html += "" + + # Add expense rows + for category, transactions in result["expenses"].items(): + html += f"" + for currency in currencies: + value = transactions.get(currency, "") + if value != "": + html += f"" + else: + html += "" + html += "" + + # Add total income row + html += '' + for currency in currencies: + value = result["accumulated_incomes"].get(currency, Decimal(0)) + html += f"" + html += "" + + # Add total expenses row + html += '' + for currency in currencies: + value = result["accumulated_expenses"].get(currency, Decimal(0)) + html += f"" + html += "" + + # Add end balance row + html += '' + for currency in currencies: + value = result["end_balance"].get(currency, Decimal(0)) + html += f"" + html += "" + + html += """ + +
CategoryEuros (€)Bitcoin (BTC)Ethereum (ETH)Monero (XMR){currency}
Account Balance (start of month){format_value(value, currency)}
{category}{format_value(value, currency)}
{category}{format_value(value, currency)}
Total Income{format_value(value, currency)}
Total Expenses{format_value(value, currency)}
Account Balance (end of month){format_value(value, currency)}
+ """ + + return html diff --git a/main.py b/main.py index 221227b..f871dfe 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,8 @@ import os from argparse import ArgumentParser +from helpers.finances import generate_transparency_table, get_transparency_data + app = Flask(__name__) @@ -28,9 +30,27 @@ def catch_all(path): if app.development_mode: warning = render_template("prod-warning.html") - return render_template( - f"{path}.html", services=services, warning=warning - ) + kwargs = { + "services": services, + "warning": warning, + } + + if path == "membership": + finances = json.loads( + (pathlib.Path(__file__).parent / "finances.json").read_text() + ) + + finances_table = generate_transparency_table( + get_transparency_data(finances) + ) + + kwargs.update( + { + "finances": finances_table, + } + ) + + return render_template(f"{path}.html", **kwargs) except TemplateNotFound: return "404 Not Found", 404 diff --git a/templates/membership.html b/templates/membership.html index 3a260dd..9e25a14 100644 --- a/templates/membership.html +++ b/templates/membership.html @@ -84,89 +84,7 @@ income and expenses for the last month.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CategoryEuros (€)Bitcoin (BTC)Ethereum (ETH)Monero (XMR)
Account Balance (start of month)+ €112.330 BTC0 ETH0 XMR
Membership Fees+ €390.00
Donations+ 0.00043400 BTC+ 0.447661805527 XMR
Server Costs- €430.04
Domain Names
Operating Expenses
Conversions
Total Income+ €390.00+ 0.00043400 BTC0 ETH+ 0.447661805527 XMR
Total Expenses- €430.04
Account Balance (end of month)+ €72.29+ 0.00043400 BTC0 ETH+ 0.447661805527 XMR
+ {{ finances|safe }}