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.
This commit is contained in:
Kumi 2024-05-29 14:50:52 +02:00
parent 7527675201
commit 6033a47b6a
Signed by: kumi
GPG key ID: ECBCC9082395383F
4 changed files with 219 additions and 87 deletions

View file

@ -86,8 +86,9 @@ h5 {
}
.currency-col {
width: 175px;
width: 200px;
white-space: nowrap;
text-align: right;
}
.table-transparency td:not(:first-child) {

193
helpers/finances.py Normal file
View file

@ -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 = """
<table class="table table-bordered table-transparency">
<thead class="table-light">
<tr>
<th scope="col">Category</th>
"""
if currencies is None:
currencies = extract_currencies(result)
# Add currency headers
for currency in currencies:
if currency == "EUR":
html += '<th class="currency-col" scope="col">Euros (€)</th>'
elif currency == "BTC":
html += '<th class="currency-col" scope="col">Bitcoin (BTC)</th>'
elif currency == "ETH":
html += '<th class="currency-col" scope="col">Ethereum (ETH)</th>'
elif currency == "XMR":
html += '<th class="currency-col" scope="col">Monero (XMR)</th>'
else:
html += f'<th class="currency-col" scope="col">{currency}</th>'
html += """
</tr>
</thead>
<tbody>
"""
# Add start balance row
html += "<tr><td>Account Balance (start of month)</td>"
for currency in currencies:
value = result["start_balance"].get(currency, Decimal(0))
html += f"<td>{format_value(value, currency)}</td>"
html += "</tr>"
# Add income rows
for category, transactions in result["incomes"].items():
html += f"<tr><td>{category}</td>"
for currency in currencies:
value = transactions.get(currency, "")
if value != "":
html += f"<td>{format_value(value, currency)}</td>"
else:
html += "<td></td>"
html += "</tr>"
# Add expense rows
for category, transactions in result["expenses"].items():
html += f"<tr><td>{category}</td>"
for currency in currencies:
value = transactions.get(currency, "")
if value != "":
html += f"<td>{format_value(value, currency)}</td>"
else:
html += "<td></td>"
html += "</tr>"
# Add total income row
html += '<tr class="table-secondary"><td><b>Total Income</b></td>'
for currency in currencies:
value = result["accumulated_incomes"].get(currency, Decimal(0))
html += f"<td><b>{format_value(value, currency)}</b></td>"
html += "</tr>"
# Add total expenses row
html += '<tr class="table-secondary"><td><b>Total Expenses</b></td>'
for currency in currencies:
value = result["accumulated_expenses"].get(currency, Decimal(0))
html += f"<td><b>{format_value(value, currency)}</b></td>"
html += "</tr>"
# Add end balance row
html += '<tr class="table-secondary"><td><b>Account Balance (end of month)</b></td>'
for currency in currencies:
value = result["end_balance"].get(currency, Decimal(0))
html += f"<td><b>{format_value(value, currency)}</b></td>"
html += "</tr>"
html += """
</tbody>
</table>
"""
return html

26
main.py
View file

@ -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

View file

@ -84,89 +84,7 @@
income and expenses for the last month.
</p>
<div class="table-responsive">
<table class="table table-bordered table-transparency">
<thead class="table-light">
<tr>
<th scope="col">Category</th>
<th class="currency-col" scope="col">Euros (€)</th>
<th class="currency-col" scope="col">Bitcoin (BTC)</th>
<th class="currency-col" scope="col">Ethereum (ETH)</th>
<th class="currency-col" scope="col">Monero (XMR)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Account Balance (start of month)</td>
<td>+ €112.33</td>
<td>0 BTC</td>
<td>0 ETH</td>
<td>0 XMR</td>
</tr>
<tr>
<td>Membership Fees</td>
<td>+ €390.00</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Donations</td>
<td></td>
<td>+ 0.00043400 BTC</td>
<td></td>
<td>+ 0.447661805527 XMR</td>
</tr>
<tr>
<td>Server Costs</td>
<td>- €430.04</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Domain Names</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Operating Expenses</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Conversions</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="table-secondary">
<td><b>Total Income</b></td>
<td><b>+ €390.00</b></td>
<td><b>+ 0.00043400 BTC</b></td>
<td><b>0 ETH</b></td>
<td><b>+ 0.447661805527 XMR</b></td>
</tr>
<tr class="table-secondary">
<td><b>Total Expenses</b></td>
<td><b>- €430.04</b></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="table-secondary">
<td><b>Account Balance (end of month)</b></td>
<td><b>+ €72.29</b></td>
<td><b>+ 0.00043400 BTC</b></td>
<td><b>0 ETH</b></td>
<td><b>+ 0.447661805527 XMR</b></td>
</tr>
</tbody>
</table>
{{ finances|safe }}
</div>
</div>
</div>