New Membership Page and other changes #4
4 changed files with 219 additions and 87 deletions
|
@ -86,8 +86,9 @@ h5 {
|
|||
}
|
||||
|
||||
.currency-col {
|
||||
width: 175px;
|
||||
width: 200px;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.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 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
|
||||
|
||||
|
|
|
@ -84,89 +84,7 @@
|
|||
income and expenses for the last month.
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-transparency">
|
||||
<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>
|
||||
{{ finances|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue