home · contact · privacy
Add auto-balancing per #balancer comment instructions.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 11 Feb 2026 00:47:26 +0000 (01:47 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 11 Feb 2026 00:47:26 +0000 (01:47 +0100)
src/ledgplom/ledger.py
src/templates/edit_structured.js
src/tests/full.dat
src/tests/full.edit_raw.0
src/tests/full.edit_structured.0
src/tests/full.edit_structured.4
src/tests/full.ledger_raw
src/tests/full.ledger_structured

index 7cc1abbc90659adcbb98a05ca90776082bfebb01..cae1ac163ce94db910b1694942df93762dd531fb 100644 (file)
@@ -10,7 +10,8 @@ from typing import Any, Callable, Iterator, Optional, Self, Union
 SPACE = ' '
 _INDENT_CHARS = {SPACE, '\t'}
 SEP_COMMENTS = ';'
-_PREFIX_DEF = '#def '
+_PREFIX_DEF = '#desc '
+_PREFIX_BAL = '#balancer '
 
 
 class _Wealth():
@@ -63,11 +64,12 @@ class _Wealth():
 
 class _Account:
     'Combine name, position in tree of owner, and wealth of self + children.'
+    desc = ''
+    balancer = ''
 
     def __init__(self, parent: Optional[Self], basename: str) -> None:
         self._wealth_diffs: dict[int, _Wealth] = {}
         self.basename = basename
-        self.desc = ''
         self.children: list[Self] = []
         self.parent = parent
         if self.parent:
@@ -161,16 +163,18 @@ class _DatLine:
         return self._raw[:]
 
     @property
-    def comment_instructions(self) -> dict[str, str]:
+    def comment_instructions(self) -> dict[str, dict[str, str]]:
         'Parse .comment into _Account modification instructions.'
-        instructions = {}
-        if self.comment.startswith(_PREFIX_DEF):
-            parts = [part.strip() for part
-                     in self.comment[len(_PREFIX_DEF):].split(SEP_COMMENTS)]
-            first_part_parts = parts[0].split(maxsplit=1)
-            account_name = first_part_parts[0]
-            desc = first_part_parts[1] if len(first_part_parts) > 1 else ''
-            instructions[account_name] = desc
+        instructions: dict[str, dict[str, str]] = {}
+        for prefix in (_PREFIX_DEF, _PREFIX_BAL):
+            attr_name = prefix[1:].rstrip()
+            instructions[attr_name] = {}
+            if self.comment.startswith(prefix):
+                parts = tuple(part.strip() for part
+                              in self.comment[len(prefix):].split(maxsplit=1))
+                if not parts:
+                    continue
+                instructions[attr_name][parts[0]] = ''.join(parts[1:])
         return instructions
 
 
@@ -649,10 +653,11 @@ class Ledger:
         for block in self.blocks:
             collect_more_than_names = id_ < 0 or block.id_ <= id_
             for line in block.lines:
-                for acc_name, desc in line.comment_instructions.items():
-                    ensure_accounts(acc_name)
-                    if collect_more_than_names:
-                        accounts[acc_name].desc = desc
+                for attr_name, defs in line.comment_instructions.items():
+                    for acc_name, definition in defs.items():
+                        ensure_accounts(acc_name)
+                        if collect_more_than_names and definition:
+                            setattr(accounts[acc_name], attr_name, definition)
             if block.booking:
                 for acc_name, wealth in block.booking.diffs_targeted.items():
                     ensure_accounts(acc_name)
@@ -729,12 +734,14 @@ class Ledger:
     def view_ctx_edit(self, id_: int, raw=True, lines=False) -> dict[str, Any]:
         'All context data relevant for rendering an edit view.'
         block = self.blocks[id_]
+        accounts = self._calc_accounts(id_)
         if lines:
             return {'raw_gap_lines': [dl.raw for dl in block.gap_lines],
+                    'account_balancers': {k: v.balancer
+                                          for k, v in accounts.items()},
                     'booking_lines': ([line.as_dict
                                        for line in block.booking.booking_lines]
                                       if block.booking else tuple())}
-        accounts = self._calc_accounts(id_)
         roots: list[dict[str, Any]] = []
         for full_path in sorted(block.booking.diffs_targeted.keys()
                                 if block.booking else []):
index 6fecc4843e3a1f11fb71c2537ce6af716e46b3cf..f607ea439432f986375469fe8dca616a80d06d2f 100644 (file)
@@ -21,11 +21,11 @@ eslint
 ],
 "max-lines": [
     "error",
-    {"max": 376, "skipBlankLines": true, "skipComments": true}
+    {"max": 394, "skipBlankLines": true, "skipComments": true}
 ],
 "max-lines-per-function": [
     "error",
-    250
+    259
 ],
 "max-params": [
     "error",
@@ -33,7 +33,7 @@ eslint
 ],
 "max-statements": [
     "error",
-    42
+    43
 ],
 "multiline-comment-style": [
     "error",
@@ -56,7 +56,7 @@ import {
 } from "/taint.js";
 
 const
-    DEFAULT_INPUT_LEN_INDENT = 2,
+    BALANCERS_DEFAULT = {"": "?"},
     IDX_LAST = -1,
     IDX_PAST_INTRO_LINE = 1,
     IDX_START = 0,
@@ -68,26 +68,33 @@ const
     LEN_CURRENCY = 3,
     LEN_DATE = 10,
     LEN_EMPTY = 0,
+    LEN_INDENT_INPUT_BALANCED = 4,
+    LEN_INDENT_INPUT_DEFAULT = 2,
     LEN_INTRO_LINE = 4,
     LEN_LEN_INDENT = 1,
     LEN_LINE_STEP = 1,
     LEN_STEP_INPUT_LEN_INDENT = 1,
     LEN_TARGET = 37,
     MINIMUM_INPUT_LEN_INDENT = 1,
+    accountBalancers = Object.assign(
+        BALANCERS_DEFAULT,
+        {{ account_balancers|tojson|safe }}
+    ),
     bookingLines = {{ booking_lines|tojson|safe }},
     rawGapLines = {{ raw_gap_lines|tojson|safe }};
 
 const newBookingLine = (
     account = "",
     amount = "None",
-    currency = ""
+    currency = "",
+    lenIndent = LEN_INDENT_INPUT_DEFAULT
 ) => ({
     account,
     amount,
     "comment": "",
     currency,
     "errors": [],
-    "len_indent": DEFAULT_INPUT_LEN_INDENT
+    "len_indent": lenIndent
 });
 
 const taintAndUpdateForm = () => {
@@ -95,6 +102,29 @@ const taintAndUpdateForm = () => {
     updateForm(); // eslint-disable-line no-use-before-define
 };
 
+const balance = (bookingLine) => {
+    let invertedAmount = "None";
+    if (bookingLine.amount !== "None") {
+        invertedAmount = `-${bookingLine.amount}`;
+        const doubleMinus = "--";
+        if (invertedAmount.startsWith(doubleMinus)) {
+            invertedAmount = invertedAmount.slice(doubleMinus.length);
+        }
+    }
+    let balancer = "";
+    const accNameSteps = bookingLine.account.split(":");
+    while (balancer === "") {
+        balancer = accountBalancers[accNameSteps.join(":")];
+        accNameSteps.pop();
+    }
+    return newBookingLine(
+        balancer,
+        invertedAmount,
+        bookingLine.currency,
+        LEN_INDENT_INPUT_BALANCED
+    );
+};
+
 const updateForm = () => {
     const
         table = document.getElementById("booking_lines"),
@@ -111,8 +141,8 @@ const updateForm = () => {
     const addButton = (
         parentTd,
         label,
-        disabled,
-        onclick
+        onclick,
+        disabled = false
     ) => {
         /* add button to td to run onclick (after updating bookingLines from
            from inputs, and followed by calling taint and updateForm) */
@@ -221,7 +251,6 @@ const updateForm = () => {
             ].forEach((kwargs) => addButton(
                 tdBtnsUpdown,
                 kwargs.label,
-                !kwargs.enabled,
                 () => {
                     const otherLine = bookingLines[kwargs.earlierIdx];
                     bookingLines.splice(
@@ -233,7 +262,8 @@ const updateForm = () => {
                         LEN_EMPTY,
                         otherLine
                     );
-                }
+                },
+                !kwargs.enabled
             ));
         }
 
@@ -309,7 +339,6 @@ const updateForm = () => {
         addButton(
             tdAddDel,
             "add new",
-            false,
             () => bookingLines.splice(
                 idx + LEN_LINE_STEP,
                 LEN_EMPTY,
@@ -317,10 +346,18 @@ const updateForm = () => {
             )
         );
         if (idx > IDX_START) {
+            addButton(
+                tdAddDel,
+                "balance",
+                () => bookingLines.splice(
+                    idx + LEN_LINE_STEP,
+                    LEN_EMPTY,
+                    balance(bookingLine)
+                );
+            );
             addButton(
                 tdAddDel,
                 "delete",
-                idx <= IDX_START,
                 () => bookingLines.splice(
                     idx,
                     LEN_LINE_STEP
@@ -367,23 +404,9 @@ const replace = () => {
 };
 
 const mirror = () => {
-    bookingLines.slice(IDX_PAST_INTRO_LINE).forEach((bookingLine) => {
-        let invertedAmount = "None";
-        if (bookingLine.amount !== "None") {
-            invertedAmount = `-${bookingLine.amount}`;
-            const doubleMinus = "--";
-            if (invertedAmount.startsWith(doubleMinus)) {
-                invertedAmount = invertedAmount.slice(doubleMinus.length);
-            }
-        }
-        bookingLines.push(
-            newBookingLine(
-                "?",
-                invertedAmount,
-                bookingLine.currency
-            )
-        );
-    });
+    bookingLines.slice(IDX_PAST_INTRO_LINE).forEach(
+        (line) => bookingLines.push(balance(line))
+    );
     taintAndUpdateForm();
 };
 
index 91c537dd575685cb804fb044163e556997797530..12f75ef91ecdb2d3150891d595c7c8859d16ac50 100644 (file)
@@ -1,4 +1,4 @@
-; #def bar:x bla bla bla
+; #desc bar:x bla bla bla
 
 2001-01-01 test  ; foo
     ; in-body comment 1
@@ -22,7 +22,7 @@
     bar:x:y   -10 €
     bar:z   -1 USD
 
-; #def bar:x bla foo bla
+; #desc bar:x bla foo bla
 
 2001-01-03 test
     foo:x    10 €
index af3835cc3c4e16e0e7f568c8ed7794051f5e2431..e63bf9da0d1afa28128fc3a9677a14fe0e274d25 100644 (file)
@@ -82,7 +82,7 @@ td.direct_target {
 </span>
 <hr>
 
-<textarea name="raw_lines" class="tainter" cols="100" rows="9">; #def bar:x bla bla bla
+<textarea name="raw_lines" class="tainter" cols="100" rows="9">; #desc bar:x bla bla bla
 
 2001-01-01 test  ; foo
     ; in-body comment 1
index 9ffb0a39dd878489c4ccbd435d9295eb5f89a02c..53ef8ac561300c30fe5c7c8eaf3e99c19dbeba94 100644 (file)
@@ -103,7 +103,7 @@ to
 </span>
 <hr>
 <div>Gap:</div>
-<textarea id="gap_lines" name="raw_lines" cols="100" rows="3" class="tainter">; #def bar:x bla bla bla
+<textarea id="gap_lines" name="raw_lines" cols="100" rows="3" class="tainter">; #desc bar:x bla bla bla
 
 </textarea>
 <EXPANDED>
index 6ad51b056517df80784576995dafbd614e63ce7c..1cedd9b34067782f661e5ef13c4afbfe922485c9 100644 (file)
@@ -110,7 +110,7 @@ to
 <hr>
 <div>Gap:</div>
 <textarea id="gap_lines" name="raw_lines" cols="100" rows="4" class="tainter">
-; #def bar:x bla foo bla
+; #desc bar:x bla foo bla
 
 </textarea>
 <EXPANDED>
index 49e154e17e7aec41c0c065b9c40fd220ce9d7e12..3765eb80eeb6266a7affd7a7926fa8003ee31659 100644 (file)
@@ -70,7 +70,7 @@ Detected redundant empty lines in gaps, <input type="submit" name="remove_redund
             [<a href="/edit_raw/0">e</a>]
         </td>
     </tr>
-    <tr><td>; #def bar:x bla bla bla&nbsp;</td></tr>
+    <tr><td>; #desc bar:x bla bla bla&nbsp;</td></tr>
     <tr><td>&nbsp;</td></tr>
     <tr><td>2001-01-01 test  ; foo&nbsp;</td></tr>
     <tr><td>    ; in-body comment 1&nbsp;</td></tr>
@@ -154,7 +154,7 @@ Detected redundant empty lines in gaps, <input type="submit" name="remove_redund
         </td>
     </tr>
     <tr><td>&nbsp;</td></tr>
-    <tr><td>; #def bar:x bla foo bla&nbsp;</td></tr>
+    <tr><td>; #desc bar:x bla foo bla&nbsp;</td></tr>
     <tr><td>&nbsp;</td></tr>
     <tr><td>2001-01-03 test&nbsp;</td></tr>
     <tr><td>    foo:x    10 €&nbsp;</td></tr>
index 2d73385c3301a0cd124d5476e1b05a3e494b376c..676eaff4b736c9374817a0ab97e55e8f36da9ed4 100644 (file)
@@ -76,7 +76,7 @@ Detected redundant empty lines in gaps, <input type="submit" name="remove_redund
         </td>
     </tr>
     <tr>
-        <td colspan="4">; #def bar:x bla bla bla&nbsp;</td>
+        <td colspan="4">; #desc bar:x bla bla bla&nbsp;</td>
     </tr>
     <tr>
         <td colspan="4">&nbsp;</td>
@@ -242,7 +242,7 @@ Detected redundant empty lines in gaps, <input type="submit" name="remove_redund
         <td colspan="4">&nbsp;</td>
     </tr>
     <tr>
-        <td colspan="4">; #def bar:x bla foo bla&nbsp;</td>
+        <td colspan="4">; #desc bar:x bla foo bla&nbsp;</td>
     </tr>
     <tr>
         <td colspan="4">&nbsp;</td>