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

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CategoryEuros (€)Bitcoin (BTC)Ethereum (ETH)Monero (XMR)
Account Balance (start of month)+ €112.330 BTC0 ETH0 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 BTC0 ETH+ 0.447661805527 XMR
Total Expenses- €430.04
Account Balance (end of month)+ €72.29+ 0.00043400 BTC0 ETH+ 0.447661805527 XMR
+ {{ finances|safe }}