home · contact · privacy
Add balance view.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 22 Jan 2025 13:55:20 +0000 (14:55 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 22 Jan 2025 13:55:20 +0000 (14:55 +0100)
ledger.py
templates/_base.tmpl
templates/_macros.tmpl
templates/balance.tmpl [new file with mode: 0644]

index 0687377b895fdf29c8862ad14794e72d81e2fea8..c7f3116a4748be4ba261a2434cd9ccec71c2ce57 100755 (executable)
--- a/ledger.py
+++ b/ledger.py
@@ -48,6 +48,27 @@ class Wealth:
         return sink
 
 
+class Account:
+    """Combine name, position in tree of own, and wealth of self + children."""
+
+    def __init__(self, parent: Optional['Account'], basename: str) -> None:
+        self.local_wealth = Wealth()
+        self.basename = basename
+        self.children: list[Account] = []
+        self.parent = parent
+        if self.parent:
+            self.parent.children += [self]
+
+    @property
+    def wealth(self) -> Wealth:
+        """Total of .local_wealth with that of .children."""
+        total = Wealth()
+        total += self.local_wealth
+        for child in self.children:
+            total += child.wealth
+        return total
+
+
 class DatLine:
     """Line of .dat file parsed into comments and machine-readable data."""
 
@@ -164,22 +185,22 @@ class Booking:
             dat_line.booking_line = TransferLine(self, dat_line.code)
             self._transfer_lines += [dat_line.booking_line]
         changes = Wealth()
+        sink_account = None
         self.account_changes: dict[str, Wealth] = {}
-        self.sink_account = None
         for transfer_line in [tl for tl in self._transfer_lines
                               if not tl.errors]:
             if transfer_line.account not in self.account_changes:
                 self.account_changes[transfer_line.account] = Wealth()
             if transfer_line.amount is None:
-                if self.sink_account:
+                if sink_account:
                     transfer_line.errors += ['too many sinks']
-                self.sink_account = transfer_line.account
+                sink_account = transfer_line.account
                 continue
             change = Wealth({transfer_line.currency: transfer_line.amount})
             self.account_changes[transfer_line.account] += change
             changes += change
-        if self.sink_account:
-            self.account_changes[self.sink_account] += changes.as_sink
+        if sink_account:
+            self.account_changes[sink_account] += changes.as_sink
         elif not changes.sink_empty:
             self._transfer_lines[-1].errors += ['needed sink missing']
         self.id_ = id_
@@ -204,7 +225,10 @@ class Handler(PlomHttpHandler):
 
     def do_GET(self) -> None:
         # pylint: disable=invalid-name,missing-function-docstring
-        if self.pagename == 'booking':
+        if self.pagename == 'balance':
+            self.send_rendered(Path('balance.tmpl'),
+                               {'roots': self.server.balance_roots})
+        elif self.pagename == 'booking':
             self.send_rendered(
                     Path('booking.tmpl'),
                     {'dat_lines':
@@ -245,6 +269,31 @@ class Server(PlomHttpServer):
         """Return only those .data_lines with .code or .comment."""
         return [dl for dl in self.dat_lines if not dl.is_empty]
 
+    @property
+    def balance_roots(self) -> list[Account]:
+        """Return tree of calculated Accounts over all .bookings."""
+        account_names = set()
+        for booking in self.bookings:
+            for account_name in booking.account_changes:
+                account_names.add(account_name)
+        full_names_to_accounts: dict[str, Account] = {}
+        for full_name in sorted(list(account_names)):
+            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)
+        for booking in self.bookings:
+            for account_name in booking.account_changes:
+                full_names_to_accounts[account_name].local_wealth +=\
+                        booking.account_changes[account_name]
+        return [ac for ac in full_names_to_accounts.values() if not ac.parent]
+
 
 if __name__ == "__main__":
     if not LEDGER_DAT:
index 74ed6b11924446b093e2e0d799b96740fa19d771..75f33afd8ab3aebe73e8599cf1d60ab8ffa7f71e 100644 (file)
@@ -12,7 +12,7 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; }
 </style>
 </head>
 <body>
-<a href="/">home</a> · <a href="/raw">raw</a>
+<a href="/">home</a> · <a href="/raw">raw</a> · <a href="/balance">balance</a>
 <hr />
 {% block content %}{% endblock %}
 </body>
index 2a349b5c8c386cd4725ebe10d4b124412a2a1b19..106e443ab0bb1daafee88063af175c67977fee64 100644 (file)
@@ -1,6 +1,4 @@
 {% macro css_value_line() %}
-tr.warning td { background-color: #ff8888; }
-td.invalid, tr.warning td.invalid { background-color: #ff0000; }
 td.amt { text-align: right }
 td.amt, td.curr { font-family: monospace; font-size: 1.3em; }
 td.curr { text-align: center; }
diff --git a/templates/balance.tmpl b/templates/balance.tmpl
new file mode 100644 (file)
index 0000000..da13b34
--- /dev/null
@@ -0,0 +1,25 @@
+{% extends '_base.tmpl' %}
+
+
+{% macro account_with_children(account, indent) %}
+<tr>
+<td>{% for _ in range(indent) %}&nbsp; &nbsp; &nbsp;{% endfor %}{{account.basename}}</td>
+<td>
+{% for curr, amt in account.wealth.moneys.items() %}
+{{amt}}{{curr}} 
+{% endfor %}
+</td>
+</tr>
+{% for child in account.children %}
+{{ account_with_children(child, indent=indent+1) }}
+{% endfor %}
+{% endmacro %}
+
+
+{% block content %}
+<table>
+{% for root in roots %}
+{{ account_with_children(root, indent=0) }}
+{% endfor %}
+</table>
+{% endblock %}