From: Christian Heller Date: Wed, 5 Feb 2025 00:53:26 +0000 (+0100) Subject: Extend Booking edit views with per-booking balance. X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/%7B%7B%20web_path%20%7D%7D/static/%7B%7Bdb.prefix%7D%7D/todo?a=commitdiff_plain;p=plomledger Extend Booking edit views with per-booking balance. --- diff --git a/ledger.py b/ledger.py index 614fef5..b9df71d 100755 --- a/ledger.py +++ b/ledger.py @@ -43,33 +43,44 @@ class Dictable: 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: @@ -91,7 +102,7 @@ class Account: 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] @@ -105,6 +116,21 @@ class Account: 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.""" @@ -322,6 +348,11 @@ class Booking: 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.""" @@ -422,24 +453,10 @@ class Handler(PlomHttpHandler): 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) @@ -447,11 +464,49 @@ class Handler(PlomHttpHandler): 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: diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index a7ed619..5da4096 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -3,14 +3,31 @@ td.amt { text-align: right } 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) %} + +{{amt}} +{{curr|truncate(4,true,"…")}} + +{% endmacro %} + + {% macro table_dat_lines(dat_lines, raw) %}
@@ -62,6 +79,7 @@ table.ledger tr > td:first-child { background-color: white; } {% endmacro %} + {% macro taint_js() %} function taint() { // activate buttons "apply", "revert" @@ -88,6 +106,7 @@ function taint() { } {% endmacro %} + {% macro edit_bar(target, id) %} prev · next @@ -99,3 +118,45 @@ function taint() {
{% endmacro %} + + +{% macro booking_balance_account_with_children(account) %} + + + + + + +{% for child in account.children %} + {{ booking_balance_account_with_children(child) }} +{% endfor %} +{% endmacro %} + + +{% macro booking_balance(valid, roots) %} +
+ + +{% for root in roots %} +{{ booking_balance_account_with_children(root) }} +{% endfor %} +
{{account.name}}{% if account.children %}:{% endif %} + +{% for curr, amt in account.wealth_before.items() %} + {{ tr_money_balance(amt, curr) }} +{% endfor %} +
+
+ +{% for curr, amt in account.wealth_diff.items() %} + {{ tr_money_balance(amt, curr) }} +{% endfor %} +
+
+ +{% for curr, amt in account.wealth_after.items() %} + {{ tr_money_balance(amt, curr) }} +{% endfor %} +
+
accountbeforediffafter
+{% endmacro %} diff --git a/templates/balance.tmpl b/templates/balance.tmpl index 4cde274..6bbb648 100644 --- a/templates/balance.tmpl +++ b/templates/balance.tmpl @@ -1,21 +1,13 @@ {% extends '_base.tmpl' %} -{% macro tr_money(amt, curr) %} - -{{amt}} -{{curr|truncate(4,true,"…")}} - -{% endmacro %} - - {% macro account_with_children(account, indent) %} {% if account.wealth.moneys|length == 1 %} {% for curr, amt in account.wealth.moneys.items() %} - {{ tr_money(amt, curr) }} + {{ macros.tr_money_balance(amt, curr) }} {% endfor %}
{% else %} @@ -24,7 +16,7 @@ {% for curr, amt in account.wealth.moneys.items() %} {% if 1 == loop.index %} - {{ tr_money(amt, curr) }} + {{ macros.tr_money_balance(amt, curr) }} {% endif %} {% endfor %}
@@ -32,7 +24,7 @@ {% for curr, amt in account.wealth.moneys.items() %} {% if 1 < loop.index %} - {{ tr_money(amt, curr) }} + {{ macros.tr_money_balance(amt, curr) }} {% endif %} {% endfor %}
@@ -49,16 +41,12 @@ {% 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 %} diff --git a/templates/edit_raw.tmpl b/templates/edit_raw.tmpl index b58e7b3..adfccbc 100644 --- a/templates/edit_raw.tmpl +++ b/templates/edit_raw.tmpl @@ -3,6 +3,7 @@ {% block css %} {{ macros.css_td_money() }} +{{ macros.css_td_money_balance() }} {{ macros.css_errors() }} {% endblock %} @@ -19,4 +20,5 @@ {% for dat_line in dat_lines %}{{ dat_line.raw }} {% endfor %} +{{ macros.booking_balance(valid, roots) }} {% endblock %} diff --git a/templates/edit_structured.tmpl b/templates/edit_structured.tmpl index 8f2bcfa..7ffa642 100644 --- a/templates/edit_structured.tmpl +++ b/templates/edit_structured.tmpl @@ -3,6 +3,7 @@ {% block css %} {{ macros.css_td_money() }} +{{ macros.css_td_money_balance() }} {{ macros.css_errors() }} input.date_input, input.number_input { font-family: monospace; } input.number_input { text-align: right; } @@ -60,7 +61,7 @@ function update_form() { add_input(td, 'target', dat_line.booking_line.target, 35) } else if (!dat_line.error) { // i.e. valid TransferLine const acc_input = add_td_input('account', dat_line.booking_line.account, 30); - acc_input.setAttribute ('list', 'accounts'); + acc_input.setAttribute ('list', 'all_accounts'); acc_input.autocomplete = 'off'; // not using input[type=number] cuz no minimal step size, therefore regex test instead const amt_input = add_td_input('amount', dat_line.booking_line.amount == 'None' ? '' : dat_line.booking_line.amount, 12); @@ -123,9 +124,10 @@ window.onload = update_form;
- + {% for acc in accounts %} +{{ macros.booking_balance(valid, roots) }} {% endblock %}