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:
parent
7527675201
commit
6033a47b6a
4 changed files with 219 additions and 87 deletions
|
@ -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
193
helpers/finances.py
Normal 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
26
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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue