return d
-class Wealth:
+class Wealth():
"""Collects amounts mapped to currencies."""
def __init__(self, moneys: Optional[dict[str, Decimal]] = None) -> None:
self.moneys = moneys if moneys else {}
- self._move_euro_up()
+ self._sort_with_euro_up()
- def _move_euro_up(self) -> None:
+ def _sort_with_euro_up(self) -> None:
if '€' in self.moneys:
temp = {'€': self.moneys['€']}
- for curr in [c for c in self.moneys if c != '€']:
+ for curr in sorted([c for c in self.moneys if c != '€']):
temp[curr] = self.moneys[curr]
self.moneys = temp
- def _inc_by(self, other: Self, add=True) -> Self:
- for currency, amount in other.moneys.items():
+ def ensure_currencies(self, currencies: set[str]) -> None:
+ """Ensure all of currencies have at least a Decimal(0) entry."""
+ for currency in currencies:
if currency not in self.moneys:
self.moneys[currency] = Decimal(0)
- self.moneys[currency] += amount if add else -amount
- self._move_euro_up()
- return self
+ self._sort_with_euro_up()
+
+ def purge_currencies_except(self, currencies: set[str]) -> None:
+ """From .moneys remove currencies except those listed."""
+ self.moneys = {curr: amt for curr, amt in self.moneys.items()
+ if curr in currencies}
- def __iadd__(self, other: Self) -> Self:
- return self._inc_by(other, True)
+ def _add(self, other: Self, add=True) -> Self:
+ result = self.__class__(self.moneys.copy())
+ result.ensure_currencies(set(other.moneys.keys()))
+ for currency, amount in other.moneys.items():
+ result.moneys[currency] += amount if add else -amount
+ return result
- def __isub__(self, other: Self) -> Self:
- return self._inc_by(other, False)
+ def __add__(self, other: Self) -> Self:
+ return self._add(other)
+
+ def __sub__(self, other: Self) -> Self:
+ return self._add(other, add=False)
@property
def sink_empty(self) -> bool:
def __init__(self, parent: Optional['Account'], basename: str) -> None:
self.local_wealth = Wealth()
self.basename = basename
- self.children: list[Account] = []
+ self.children: list[Self] = []
self.parent = parent
if self.parent:
self.parent.children += [self]
total += child.wealth
return total
+ @staticmethod
+ def by_paths(acc_names: list[str]) -> dict[str, 'Account']:
+ """From bookings generate dict of all refered Accounts by paths."""
+ paths_to_accs: dict[str, Account] = {}
+ for full_name in acc_names:
+ path = ''
+ for step_name in full_name.split(':'):
+ parent_name = path[:]
+ path = ':'.join([path, step_name]) if path else step_name
+ if path not in paths_to_accs:
+ paths_to_accs[path] = Account(
+ paths_to_accs[parent_name] if parent_name else None,
+ step_name)
+ return paths_to_accs
+
@staticmethod
def names_over_bookings(bookings: list['Booking']) -> list[str]:
"""Sorted list of all account names refered to in bookings."""
return True
return False
+ def apply_to_account_dict(self, acc_dict: dict[str, Account]) -> None:
+ """To account dictionary of expected keys, apply .account_changes."""
+ for acc_name, wealth in self.account_changes.items():
+ acc_dict[acc_name].local_wealth += wealth
+
class Handler(PlomHttpHandler):
""""Handles HTTP requests."""
to_balance = (self.server.bookings[:id_ + 1] if id_ >= 0
else self.server.bookings)
valid = 0 == len([b for b in to_balance if b.is_questionable])
- full_names_to_accounts: dict[str, Account] = {}
- for full_name in Account.names_over_bookings(to_balance):
- step_names = full_name.split(':')
- path = ''
- for step_name in step_names:
- parent_name = path[:]
- path = ':'.join([path, step_name]) if path else step_name
- if path not in full_names_to_accounts:
- full_names_to_accounts[path] = Account(
- full_names_to_accounts[parent_name] if parent_name
- else None,
- step_name)
+ acc_dict = Account.by_paths(Account.names_over_bookings(to_balance))
for booking in to_balance:
- for account_name in booking.account_changes:
- full_names_to_accounts[account_name].local_wealth +=\
- booking.account_changes[account_name]
- ctx['roots'] = [ac for ac in full_names_to_accounts.values()
- if not ac.parent]
+ booking.apply_to_account_dict(acc_dict)
+ ctx['roots'] = [ac for ac in acc_dict.values() if not ac.parent]
ctx['valid'] = valid
ctx['booking'] = self.server.bookings[id_]
self._send_rendered('balance', ctx)
def get_edit(self, ctx, raw: bool) -> None:
"""Display edit form for individual Booking."""
id_ = int(self.path_toks[2])
+ booking = self.server.bookings[id_]
+ acc_names = Account.names_over_bookings(self.server.bookings)
+ to_balance = self.server.bookings[:id_ + 1]
+ accounts_after = Account.by_paths(acc_names)
+ accounts_before = Account.by_paths(acc_names)
+ for b in to_balance:
+ if b != booking:
+ b.apply_to_account_dict(accounts_before)
+ b.apply_to_account_dict(accounts_after)
+ observed_tree: list[dict[str, Any]] = []
+ for full_name in sorted(booking.account_changes.keys()):
+ parent_children: list[dict[str, Any]] = observed_tree
+ path = ''
+ for step_name in full_name.split(':'):
+ path = ':'.join([path, step_name]) if path else step_name
+ for child in [n for n in parent_children if path == n['name']]:
+ parent_children = child['children']
+ continue
+ wealth_before = accounts_before[path].wealth
+ wealth_after = accounts_after[path].wealth
+ diff = {c: a for c, a in (wealth_after - wealth_before
+ ).moneys.items()
+ if a != 0}
+ if diff:
+ displayed_currencies = set(diff.keys())
+ for wealth in wealth_before, wealth_after:
+ wealth.ensure_currencies(displayed_currencies)
+ wealth.purge_currencies_except(displayed_currencies)
+ node: dict[str, Any] = {
+ 'name': path,
+ 'wealth_before': wealth_before.moneys,
+ 'wealth_diff': diff,
+ 'wealth_after': wealth_after.moneys,
+ 'children': []}
+ parent_children += [node]
+ parent_children = node['children']
ctx['id'] = id_
- ctx['dat_lines'] = [dl if raw else dl.as_dict for dl
- in self.server.bookings[id_].booked_lines]
+ ctx['dat_lines'] = [dl if raw else dl.as_dict
+ for dl in booking.booked_lines]
+ ctx['valid'] = 0 == len([b for b in to_balance if b.is_questionable])
+ ctx['roots'] = observed_tree
if not raw:
- ctx['accounts'] = Account.names_over_bookings(self.server.bookings)
+ ctx['all_accounts'] = acc_names
self._send_rendered(EDIT_RAW if raw else EDIT_STRUCT, ctx)
def get_ledger(self, ctx: dict[str, Any], raw: bool) -> None:
td.amt, td.curr { font-family: monospace; font-size: 1.3em; }
{% endmacro %}
+
+{% macro css_td_money_balance() %}
+td.balance.amt { width: 10em; }
+td.balance.curr { width: 3em; }
+{% endmacro %}
+
+
{% macro css_errors() %}
td.invalid, tr.warning td.invalid { background-color: #ff0000; }
{% endmacro %}
+
{% macro css_ledger_index_col() %}
table.ledger tr > td:first-child { background-color: white; }
{% endmacro %}
+
+{% macro tr_money_balance(amt, curr) %}
+<tr>
+<td class="balance amt">{{amt}}</td>
+<td class="balance curr">{{curr|truncate(4,true,"…")}}</td>
+</tr>
+{% endmacro %}
+
+
{% macro table_dat_lines(dat_lines, raw) %}
<form action="/ledger_{% if raw %}raw{% else %}structured{% endif %}" method="POST">
<table class="ledger">
</form>
{% endmacro %}
+
{% macro taint_js() %}
function taint() {
// activate buttons "apply", "revert"
}
{% endmacro %}
+
{% macro edit_bar(target, id) %}
<span class="disable_on_change">
<a href="/bookings/{{id-1}}">prev</a> · <a href="/bookings/{{id+1}}">next</a>
</span>
<hr />
{% endmacro %}
+
+
+{% macro booking_balance_account_with_children(account) %}
+<tr class="alternating">
+<td>{{account.name}}{% if account.children %}:{% endif %}</td>
+<td class="money">
+<table>
+{% for curr, amt in account.wealth_before.items() %}
+ {{ tr_money_balance(amt, curr) }}
+{% endfor %}
+</table>
+</td>
+<td class="money">
+<table>
+{% for curr, amt in account.wealth_diff.items() %}
+ {{ tr_money_balance(amt, curr) }}
+{% endfor %}
+</table>
+</td>
+<td class="money">
+<table>
+{% for curr, amt in account.wealth_after.items() %}
+ {{ tr_money_balance(amt, curr) }}
+{% endfor %}
+</table>
+</td>
+</tr>
+{% for child in account.children %}
+ {{ booking_balance_account_with_children(child) }}
+{% endfor %}
+{% endmacro %}
+
+
+{% macro booking_balance(valid, roots) %}
+<hr />
+<table{% if not valid %} class="warning"{% endif %}>
+<tr class="alternating"><th>account</th><th>before</th><th>diff</th><th>after</th></tr>
+{% for root in roots %}
+{{ booking_balance_account_with_children(root) }}
+{% endfor %}
+</table>
+{% endmacro %}
{% extends '_base.tmpl' %}
-{% macro tr_money(amt, curr) %}
-<tr>
-<td class="amt">{{amt}}</td>
-<td class="curr">{{curr|truncate(4,true,"…")}}</td>
-</tr>
-{% endmacro %}
-
-
{% macro account_with_children(account, indent) %}
<tr class="alternating">
<td class="money">
{% if account.wealth.moneys|length == 1 %}
<table>
{% for curr, amt in account.wealth.moneys.items() %}
- {{ tr_money(amt, curr) }}
+ {{ macros.tr_money_balance(amt, curr) }}
{% endfor %}
</table>
{% else %}
<table>
{% for curr, amt in account.wealth.moneys.items() %}
{% if 1 == loop.index %}
- {{ tr_money(amt, curr) }}
+ {{ macros.tr_money_balance(amt, curr) }}
{% endif %}
{% endfor %}
</table>
<table>
{% for curr, amt in account.wealth.moneys.items() %}
{% if 1 < loop.index %}
- {{ tr_money(amt, curr) }}
+ {{ macros.tr_money_balance(amt, curr) }}
{% endif %}
{% endfor %}
</table>
{% block css %}
{{ macros.css_td_money() }}
-
+{{ macros.css_td_money_balance() }}
td.money table { float: left; }
-td.amt { width: 10em; }
-td.curr { width: 3em; }
summary::marker { font-family: monospace; font-size: 1.2em; }
summary { list-style-type: "[…]"; }
details[open] > summary { list-style-type: "[^]"; }
-
span.indent { letter-spacing: 3em; }
-
{% endblock css %}
{% block content %}