privatecoffee-website/helpers/finances.py
Kumi 1a10e8968b
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.
2024-06-03 16:12:21 +02:00

223 lines
7.7 KiB
Python

from decimal import Decimal
def get_latest_month(data):
years = sorted(data.keys())
latest_year = years[-1]
months = sorted(data[latest_year].keys())
latest_month = months[-1]
return int(latest_month), int(latest_year)
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 = {}
notes = {}
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() if k != "Notes"
}
for category in data[y][m]:
for currency, amount in data[y][m][category].items():
if currency == "Notes":
if int(y) == int(year) and int(m) == int(month):
notes[category] = amount
else:
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() if k != "Notes"
}
# 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
if currency != "Notes"
}
accumulated_expenses = {
currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses)
for currency in balances
if currency != "Notes"
}
return {
"start_balance": start_balance,
"end_balance": end_balance,
"incomes": incomes,
"expenses": expenses,
"accumulated_incomes": accumulated_incomes,
"accumulated_expenses": accumulated_expenses,
"notes": notes,
}
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", "Notes"}
)
)
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():
has_notes = result["notes"].get(category)
html += f"<tr><td>{category}{'*' if has_notes else ''}</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():
has_notes = result["notes"].get(category)
html += f"<tr><td>{category}{'*' if has_notes else ''}</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>
"""
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