feat: Enhance financial transparency
Introduced detailed financial reporting on a per-category basis, including notes for specific expenses and administrative costs in the data model. Enhanced the financial transparency feature by integrating a monthly financial report generator in the backend, enabling dynamic generation and display of detailed financial reports on the website. This commit also includes a new transparency report page, enriching user engagement with detailed insights into financial allocations and expenditures. - Refactored finance-related data structures to include notes about specific financial activities. - Extended the financial reporting capabilities to generate and display detailed notes and categorizations of expenses and incomes, improving transparency. - Updated the UI to display these detailed reports, ensuring that users have access to comprehensive financial information. This change aligns with our commitment to transparency and accountability by providing clear, detailed financial information to our members and donors.
This commit is contained in:
parent
b3ae436679
commit
1a10e8968b
5 changed files with 103 additions and 32 deletions
|
@ -4,15 +4,14 @@
|
||||||
"Membership Fees": {
|
"Membership Fees": {
|
||||||
"EUR": 365
|
"EUR": 365
|
||||||
},
|
},
|
||||||
"Donations": {},
|
|
||||||
"Server Costs": {
|
"Server Costs": {
|
||||||
"EUR": -216.57
|
"EUR": -216.57
|
||||||
},
|
},
|
||||||
"Domain Names": {},
|
"Domain Names": {},
|
||||||
"Operating Expenses": {
|
"Administrative Expenses": {
|
||||||
"EUR": -36.10
|
"EUR": -36.10,
|
||||||
},
|
"Notes": "Administrative fee for the formation of the association"
|
||||||
"Conversions": {}
|
}
|
||||||
},
|
},
|
||||||
"5": {
|
"5": {
|
||||||
"Membership Fees": {
|
"Membership Fees": {
|
||||||
|
@ -23,11 +22,9 @@
|
||||||
"XMR": 0.447661805527
|
"XMR": 0.447661805527
|
||||||
},
|
},
|
||||||
"Server Costs": {
|
"Server Costs": {
|
||||||
"EUR": -430.04
|
"EUR": -430.04,
|
||||||
},
|
"Notes": "Includes setup costs and two monthly payments for new server"
|
||||||
"Domain Names": {},
|
}
|
||||||
"Operating Expenses": {},
|
|
||||||
"Conversions": {}
|
|
||||||
},
|
},
|
||||||
"6": {
|
"6": {
|
||||||
"Server Costs": {
|
"Server Costs": {
|
||||||
|
|
|
@ -9,6 +9,7 @@ def get_latest_month(data):
|
||||||
|
|
||||||
return int(latest_month), int(latest_year)
|
return int(latest_month), int(latest_year)
|
||||||
|
|
||||||
|
|
||||||
def get_transparency_data(data, year=None, month=None):
|
def get_transparency_data(data, year=None, month=None):
|
||||||
if year is None:
|
if year is None:
|
||||||
year = max(data.keys())
|
year = max(data.keys())
|
||||||
|
@ -23,6 +24,7 @@ def get_transparency_data(data, year=None, month=None):
|
||||||
balances = {}
|
balances = {}
|
||||||
incomes = {}
|
incomes = {}
|
||||||
expenses = {}
|
expenses = {}
|
||||||
|
notes = {}
|
||||||
|
|
||||||
start_balance = {}
|
start_balance = {}
|
||||||
end_balance = {}
|
end_balance = {}
|
||||||
|
@ -37,41 +39,51 @@ def get_transparency_data(data, year=None, month=None):
|
||||||
|
|
||||||
# If the month is the one we are interested in, capture the start balance
|
# If the month is the one we are interested in, capture the start balance
|
||||||
if int(y) == int(year) and int(m) == int(month):
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
start_balance = {k: Decimal(v) for k, v in balances.items()}
|
start_balance = {
|
||||||
|
k: Decimal(v) for k, v in balances.items() if k != "Notes"
|
||||||
|
}
|
||||||
|
|
||||||
for category in data[y][m]:
|
for category in data[y][m]:
|
||||||
for currency, amount in data[y][m][category].items():
|
for currency, amount in data[y][m][category].items():
|
||||||
if currency not in balances:
|
if currency == "Notes":
|
||||||
balances[currency] = Decimal(0)
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
balances[currency] += Decimal(str(amount))
|
notes[category] = amount
|
||||||
|
else:
|
||||||
|
if currency not in balances:
|
||||||
|
balances[currency] = Decimal(0)
|
||||||
|
balances[currency] += Decimal(str(amount))
|
||||||
|
|
||||||
# Track incomes and expenses
|
# Track incomes and expenses
|
||||||
if int(y) == int(year) and int(m) == int(month):
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
if Decimal(str(amount)) > 0:
|
if Decimal(str(amount)) > 0:
|
||||||
if category not in incomes:
|
if category not in incomes:
|
||||||
incomes[category] = {}
|
incomes[category] = {}
|
||||||
if currency not in incomes[category]:
|
if currency not in incomes[category]:
|
||||||
incomes[category][currency] = Decimal(0)
|
incomes[category][currency] = Decimal(0)
|
||||||
incomes[category][currency] += Decimal(str(amount))
|
incomes[category][currency] += Decimal(str(amount))
|
||||||
else:
|
else:
|
||||||
if category not in expenses:
|
if category not in expenses:
|
||||||
expenses[category] = {}
|
expenses[category] = {}
|
||||||
if currency not in expenses[category]:
|
if currency not in expenses[category]:
|
||||||
expenses[category][currency] = Decimal(0)
|
expenses[category][currency] = Decimal(0)
|
||||||
expenses[category][currency] += Decimal(str(amount))
|
expenses[category][currency] += Decimal(str(amount))
|
||||||
|
|
||||||
# If the month is the one we are interested in, capture the end balance
|
# If the month is the one we are interested in, capture the end balance
|
||||||
if int(y) == int(year) and int(m) == int(month):
|
if int(y) == int(year) and int(m) == int(month):
|
||||||
end_balance = {k: Decimal(v) for k, v in balances.items()}
|
end_balance = {
|
||||||
|
k: Decimal(v) for k, v in balances.items() if k != "Notes"
|
||||||
|
}
|
||||||
|
|
||||||
# Calculate accumulated sums of incomes and expenses
|
# Calculate accumulated sums of incomes and expenses
|
||||||
accumulated_incomes = {
|
accumulated_incomes = {
|
||||||
currency: sum(incomes[cat].get(currency, Decimal(0)) for cat in incomes)
|
currency: sum(incomes[cat].get(currency, Decimal(0)) for cat in incomes)
|
||||||
for currency in balances
|
for currency in balances
|
||||||
|
if currency != "Notes"
|
||||||
}
|
}
|
||||||
accumulated_expenses = {
|
accumulated_expenses = {
|
||||||
currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses)
|
currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses)
|
||||||
for currency in balances
|
for currency in balances
|
||||||
|
if currency != "Notes"
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -81,6 +93,7 @@ def get_transparency_data(data, year=None, month=None):
|
||||||
"expenses": expenses,
|
"expenses": expenses,
|
||||||
"accumulated_incomes": accumulated_incomes,
|
"accumulated_incomes": accumulated_incomes,
|
||||||
"accumulated_expenses": accumulated_expenses,
|
"accumulated_expenses": accumulated_expenses,
|
||||||
|
"notes": notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,7 +107,7 @@ def generate_transparency_table(result, currencies=None):
|
||||||
+ list(data["accumulated_incomes"].keys())
|
+ list(data["accumulated_incomes"].keys())
|
||||||
+ list(data["accumulated_expenses"].keys())
|
+ list(data["accumulated_expenses"].keys())
|
||||||
)
|
)
|
||||||
- {"EUR"}
|
- {"EUR", "Notes"}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -152,7 +165,8 @@ def generate_transparency_table(result, currencies=None):
|
||||||
|
|
||||||
# Add income rows
|
# Add income rows
|
||||||
for category, transactions in result["incomes"].items():
|
for category, transactions in result["incomes"].items():
|
||||||
html += f"<tr><td>{category}</td>"
|
has_notes = result["notes"].get(category)
|
||||||
|
html += f"<tr><td>{category}{'*' if has_notes else ''}</td>"
|
||||||
for currency in currencies:
|
for currency in currencies:
|
||||||
value = transactions.get(currency, "")
|
value = transactions.get(currency, "")
|
||||||
if value != "":
|
if value != "":
|
||||||
|
@ -163,7 +177,8 @@ def generate_transparency_table(result, currencies=None):
|
||||||
|
|
||||||
# Add expense rows
|
# Add expense rows
|
||||||
for category, transactions in result["expenses"].items():
|
for category, transactions in result["expenses"].items():
|
||||||
html += f"<tr><td>{category}</td>"
|
has_notes = result["notes"].get(category)
|
||||||
|
html += f"<tr><td>{category}{'*' if has_notes else ''}</td>"
|
||||||
for currency in currencies:
|
for currency in currencies:
|
||||||
value = transactions.get(currency, "")
|
value = transactions.get(currency, "")
|
||||||
if value != "":
|
if value != "":
|
||||||
|
@ -198,4 +213,11 @@ def generate_transparency_table(result, currencies=None):
|
||||||
</table>
|
</table>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if result["notes"]:
|
||||||
|
html += "<p><b>Notes:</b></p>"
|
||||||
|
html += "<ul>"
|
||||||
|
for category, footnote in result["notes"].items():
|
||||||
|
html += f"<li>{category}: {footnote}</li>"
|
||||||
|
html += "</ul>"
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
22
main.py
22
main.py
|
@ -69,6 +69,28 @@ def catch_all(path):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if path == "transparency":
|
||||||
|
finances = json.loads(
|
||||||
|
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
|
||||||
|
)
|
||||||
|
|
||||||
|
finance_data = {}
|
||||||
|
|
||||||
|
for year in sorted(finances.keys(), reverse=True):
|
||||||
|
for month in sorted(finances[year].keys(), reverse=True):
|
||||||
|
if year not in finance_data:
|
||||||
|
finance_data[year] = {}
|
||||||
|
print(get_transparency_data(finances, year, month))
|
||||||
|
finance_data[year][month] = generate_transparency_table(
|
||||||
|
get_transparency_data(finances, year, month)
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs.update(
|
||||||
|
{
|
||||||
|
"finances": finance_data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(f"{path}.html", **kwargs)
|
return render_template(f"{path}.html", **kwargs)
|
||||||
|
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
|
|
|
@ -85,6 +85,11 @@
|
||||||
donations are being used.
|
donations are being used.
|
||||||
</p>
|
</p>
|
||||||
<div class="table-responsive">{{ finances|safe }}</div>
|
<div class="table-responsive">{{ finances|safe }}</div>
|
||||||
|
<p class="card-text">
|
||||||
|
Want to know how we got here? Check out all of our
|
||||||
|
<a href="/transparency.html">transparency reports</a> for more
|
||||||
|
information.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
25
templates/transparency.html
Normal file
25
templates/transparency.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends "base.html" %} {% block title %}Membership / Donations{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h1 class="special-header fancy-text-primary">Transparency</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Private.coffee is funded by its members and donations. We believe in
|
||||||
|
transparency and accountability. Below you can find financial reports for
|
||||||
|
each month since our inception.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for year, year_data in finances.items() %}
|
||||||
|
{% for month, month_data in year_data.items() %}
|
||||||
|
<div class="card shadow-sm mt-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Transparency Report for {{ month }}/{{ year }}</h5>
|
||||||
|
<div class="table-responsive">{{ month_data|safe }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in a new issue