New Membership Page and other changes #4
4 changed files with 219 additions and 87 deletions
|
@ -86,8 +86,9 @@ h5 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.currency-col {
|
.currency-col {
|
||||||
width: 175px;
|
width: 200px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-transparency td:not(:first-child) {
|
.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 argparse import ArgumentParser
|
||||||
|
|
||||||
|
from helpers.finances import generate_transparency_table, get_transparency_data
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,9 +30,27 @@ def catch_all(path):
|
||||||
if app.development_mode:
|
if app.development_mode:
|
||||||
warning = render_template("prod-warning.html")
|
warning = render_template("prod-warning.html")
|
||||||
|
|
||||||
return render_template(
|
kwargs = {
|
||||||
f"{path}.html", services=services, warning=warning
|
"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:
|
except TemplateNotFound:
|
||||||
return "404 Not Found", 404
|
return "404 Not Found", 404
|
||||||
|
|
||||||
|
|
|
@ -84,89 +84,7 @@
|
||||||
income and expenses for the last month.
|
income and expenses for the last month.
|
||||||
</p>
|
</p>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-transparency">
|
{{ finances|safe }}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue