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 = """
+
+
+
+ Category |
+ """
+
+ if currencies is None:
+ currencies = extract_currencies(result)
+
+ # Add currency headers
+ for currency in currencies:
+ if currency == "EUR":
+ html += 'Euros (€) | '
+ elif currency == "BTC":
+ html += 'Bitcoin (BTC) | '
+ elif currency == "ETH":
+ html += 'Ethereum (ETH) | '
+ elif currency == "XMR":
+ html += 'Monero (XMR) | '
+ else:
+ html += f'{currency} | '
+
+ html += """
+
+
+
+ """
+
+ # Add start balance row
+ html += "Account Balance (start of month) | "
+ for currency in currencies:
+ value = result["start_balance"].get(currency, Decimal(0))
+ html += f"{format_value(value, currency)} | "
+ html += "
"
+
+ # Add income rows
+ for category, transactions in result["incomes"].items():
+ html += f"{category} | "
+ for currency in currencies:
+ value = transactions.get(currency, "")
+ if value != "":
+ html += f"{format_value(value, currency)} | "
+ else:
+ html += " | "
+ html += "
"
+
+ # Add expense rows
+ for category, transactions in result["expenses"].items():
+ html += f"{category} | "
+ for currency in currencies:
+ value = transactions.get(currency, "")
+ if value != "":
+ html += f"{format_value(value, currency)} | "
+ else:
+ html += " | "
+ html += "
"
+
+ # Add total income row
+ html += 'Total Income | '
+ for currency in currencies:
+ value = result["accumulated_incomes"].get(currency, Decimal(0))
+ html += f"{format_value(value, currency)} | "
+ html += "
"
+
+ # Add total expenses row
+ html += 'Total Expenses | '
+ for currency in currencies:
+ value = result["accumulated_expenses"].get(currency, Decimal(0))
+ html += f"{format_value(value, currency)} | "
+ html += "
"
+
+ # Add end balance row
+ html += 'Account Balance (end of month) | '
+ for currency in currencies:
+ value = result["end_balance"].get(currency, Decimal(0))
+ html += f"{format_value(value, currency)} | "
+ html += "
"
+
+ html += """
+
+
+ """
+
+ 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.
-
-
-
- Category |
- Euros (€) |
- Bitcoin (BTC) |
- Ethereum (ETH) |
- Monero (XMR) |
-
-
-
-
- Account Balance (start of month) |
- + €112.33 |
- 0 BTC |
- 0 ETH |
- 0 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 BTC |
- 0 ETH |
- + 0.447661805527 XMR |
-
-
- Total Expenses |
- - €430.04 |
- |
- |
- |
-
-
- Account Balance (end of month) |
- + €72.29 |
- + 0.00043400 BTC |
- 0 ETH |
- + 0.447661805527 XMR |
-
-
-
+ {{ finances|safe }}