From 3e0d8acb51fc2131f978410d71908f781ea97b51 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 13:23:59 +0100 Subject: [PATCH 01/16] Redirect /booking/{id} to /edit_structured/{id} for now. --- ledger.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ledger.py b/ledger.py index a553fc2..d254628 100755 --- a/ledger.py +++ b/ledger.py @@ -300,13 +300,18 @@ class Handler(PlomHttpHandler): # pylint: disable=invalid-name,missing-function-docstring ctx = {'tainted': self.server.tainted, 'path': self.path} if self.pagename == 'booking' or self.pagename.startswith('edit_'): - ctx['id'] = int(self.path_toks[2]) + id_ = int(self.path_toks[2]) + if self.pagename.startswith('edit_'): + ctx['id'] = id_ if self.pagename == 'balance': valid, balance_roots = self.server.balance_roots( int(self.params.first('cutoff') or '0')) self.send_rendered(Path('balance.tmpl'), ctx | {'roots': balance_roots, 'valid': valid}) - elif self.pagename in {'booking', 'edit_structured'}: + elif self.pagename == 'booking': + self.redirect( + Path('/').joinpath('edit_structured').joinpath(str(id_))) + elif self.pagename == 'edit_structured': ctx['dat_lines'] = [dl.as_dict for dl in self.server.bookings[ctx['id']].dat_lines] self.send_rendered(Path('edit_structured.tmpl'), ctx) -- 2.30.2 From b561a011ad4e316155aeb2fb311ccca98ac82370 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 13:39:20 +0100 Subject: [PATCH 02/16] Calculate /balance up-and-incl. rather than up-and-excl, name last included Booking in view. --- ledger.py | 15 +++++++++------ templates/_macros.tmpl | 2 +- templates/balance.tmpl | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ledger.py b/ledger.py index d254628..a4a94bd 100755 --- a/ledger.py +++ b/ledger.py @@ -304,10 +304,12 @@ class Handler(PlomHttpHandler): if self.pagename.startswith('edit_'): ctx['id'] = id_ if self.pagename == 'balance': - valid, balance_roots = self.server.balance_roots( - int(self.params.first('cutoff') or '0')) + id_ = int(self.params.first('up_incl') or '-1') + valid, balance_roots = self.server.balance_roots(id_) self.send_rendered(Path('balance.tmpl'), - ctx | {'roots': balance_roots, 'valid': valid}) + ctx | {'roots': balance_roots, + 'valid': valid, + 'booking': self.server.bookings[id_]}) elif self.pagename == 'booking': self.redirect( Path('/').joinpath('edit_structured').joinpath(str(id_))) @@ -390,11 +392,12 @@ 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] - def balance_roots(self, cutoff: int) -> tuple[bool, list[Account]]: - """Return tree of calculated Accounts over .bookings[:cutoff].""" + def balance_roots(self, up_incl: int) -> tuple[bool, list[Account]]: + """Return tree of calculated Accounts over .bookings[:up_incl+1].""" account_names = set() valid = True - to_balance = self.bookings[:cutoff] if cutoff else self.bookings + to_balance = (self.bookings[:up_incl + 1] if up_incl >= 0 + else self.bookings) for booking in to_balance: valid = valid if not booking.is_questionable else False for account_name in booking.account_changes: diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index 0dfd2fc..7ae7845 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -15,7 +15,7 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; } {% endif %} {% if dat_line.is_intro %} - #/b + #/b {% else %} {% endif %} diff --git a/templates/balance.tmpl b/templates/balance.tmpl index 14878c9..15ebf83 100644 --- a/templates/balance.tmpl +++ b/templates/balance.tmpl @@ -62,6 +62,7 @@ span.indent { letter-spacing: 3em; } {% endblock css %} {% block content %} +

balance after booking {{booking.id_}} ({{booking.intro_line.date}}: {{booking.intro_line.target}})

{% for root in roots %} {{ account_with_children(root, indent=0) }} -- 2.30.2 From b32a0cca577ecaff39c053dd9ce35de467870231 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 13:46:32 +0100 Subject: [PATCH 03/16] Add /balance?up_incl= link to Booking edit views. --- templates/_macros.tmpl | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index 7ae7845..55243af 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -54,23 +54,29 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; } {% macro taint_js() %} function taint() { - const els = document.getElementsByClassName("enable_on_change"); + let els = document.getElementsByClassName("enable_on_change"); for (let i = 0; i < els.length; i++) { els[i].disabled = false; } - const a = document.getElementById("switch_link"); - const link_text = a.textContent; - const span = a.parentNode; - span.innerHTML = ""; + let links_text = ''; + els = document.getElementsByClassName("disable_on_change"); + for (let i = 0; i < els.length; i++) { + links_text += els[i].textContent + ' '; + } + const span = document.getElementById('booking_links'); + span.innerHTML = ''; const del = document.createElement("del"); span.appendChild(del); - del.textContent = link_text; + del.textContent = links_text; } {% endmacro %} {% macro edit_bar(target, id) %} -switch to {{target}} + +switch to {{target}} +balance after +
{% endmacro %} -- 2.30.2 From 6e35785536710bb7044cbaee2b2838529eb1cfcd Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 13:50:09 +0100 Subject: [PATCH 04/16] Add prev/next links to Booking edit views. --- templates/_macros.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index 55243af..acf74d9 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -77,6 +77,8 @@ function taint() { switch to {{target}} balance after +prev +next
{% endmacro %} -- 2.30.2 From c9ce17cf9f3654ceac5bdbc8b6c7a6c32f412bad Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 13:51:34 +0100 Subject: [PATCH 05/16] Rename /booking/{id} to /bookings/{id}. --- ledger.py | 4 ++-- templates/_macros.tmpl | 8 ++++---- templates/balance.tmpl | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ledger.py b/ledger.py index a4a94bd..d7ebad6 100755 --- a/ledger.py +++ b/ledger.py @@ -299,7 +299,7 @@ class Handler(PlomHttpHandler): def do_GET(self) -> None: # pylint: disable=invalid-name,missing-function-docstring ctx = {'tainted': self.server.tainted, 'path': self.path} - if self.pagename == 'booking' or self.pagename.startswith('edit_'): + if self.pagename == 'bookings' or self.pagename.startswith('edit_'): id_ = int(self.path_toks[2]) if self.pagename.startswith('edit_'): ctx['id'] = id_ @@ -310,7 +310,7 @@ class Handler(PlomHttpHandler): ctx | {'roots': balance_roots, 'valid': valid, 'booking': self.server.bookings[id_]}) - elif self.pagename == 'booking': + elif self.pagename == 'bookings': self.redirect( Path('/').joinpath('edit_structured').joinpath(str(id_))) elif self.pagename == 'edit_structured': diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index acf74d9..bf9bff5 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -22,14 +22,14 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; } {% if raw %} {% if dat_line.is_intro %} - {{dat_line.raw_nbsp|safe}} + {{dat_line.raw_nbsp|safe}} {% else %} {{dat_line.raw_nbsp|safe}} {% endif %} {% else %} {% if dat_line.is_intro %} - {{dat_line.booking_line.date}} + {{dat_line.booking_line.date}} {{dat_line.booking_line.target}} {% elif not dat_line.error %} {{dat_line.booking_line.amount_short}} @@ -77,8 +77,8 @@ function taint() { switch to {{target}} balance after -prev -next +prev +next
{% endmacro %} diff --git a/templates/balance.tmpl b/templates/balance.tmpl index 15ebf83..a8a3b7f 100644 --- a/templates/balance.tmpl +++ b/templates/balance.tmpl @@ -62,7 +62,7 @@ span.indent { letter-spacing: 3em; } {% endblock css %} {% block content %} -

balance after booking {{booking.id_}} ({{booking.intro_line.date}}: {{booking.intro_line.target}})

+

balance after booking {{booking.id_}} ({{booking.intro_line.date}}: {{booking.intro_line.target}})

{% for root in roots %} {{ account_with_children(root, indent=0) }} -- 2.30.2 From f6cecc4267d62a514208af83432f6b9efd881c5f Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 14:05:38 +0100 Subject: [PATCH 06/16] Replace Booking edit taint handlers from .onchange to .oninput, but remove once used. --- templates/_macros.tmpl | 24 +++++++++++++++--------- templates/edit_raw.tmpl | 2 +- templates/edit_structured.tmpl | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index bf9bff5..5afc960 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -54,20 +54,26 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; } {% macro taint_js() %} function taint() { - let els = document.getElementsByClassName("enable_on_change"); - for (let i = 0; i < els.length; i++) { - els[i].disabled = false; - } - let links_text = ''; - els = document.getElementsByClassName("disable_on_change"); - for (let i = 0; i < els.length; i++) { - links_text += els[i].textContent + ' '; - } + // activate buttons "apply", "revert" + Array.from(document.getElementsByClassName("enable_on_change")).forEach((el) => { + el.disabled = false; + }); + // deactivate Booking links + Array.from(document.getElementsByClassName("disable_on_change")).forEach((el) => { + links_text += el.textContent + ' '; + }); const span = document.getElementById('booking_links'); + let links_text = ''; span.innerHTML = ''; const del = document.createElement("del"); span.appendChild(del); del.textContent = links_text; + // remove oninput handlers no longer needed (since we only ever go one way) + ['input', 'textarea'].forEach((tag_name) => { + Array.from(document.getElementsByTagName(tag_name)).forEach((el) => { + el.oninput = null; + }); + }); } {% endmacro %} diff --git a/templates/edit_raw.tmpl b/templates/edit_raw.tmpl index 6e3702d..b58e7b3 100644 --- a/templates/edit_raw.tmpl +++ b/templates/edit_raw.tmpl @@ -15,7 +15,7 @@ {% block content %}
{{ macros.edit_bar("structured", id) }} -
diff --git a/templates/edit_structured.tmpl b/templates/edit_structured.tmpl index 6e31d91..0e510ae 100644 --- a/templates/edit_structured.tmpl +++ b/templates/edit_structured.tmpl @@ -40,7 +40,7 @@ function update_form() { input.name = `line_${i}_${name}` input.value = value.trim(); input.placeholder = placeholder; - input.onchange = taint; + input.oninput = taint; if (dat_line.error) { td.classList.add("invalid"); } -- 2.30.2 From 347c3e769ae35c136f5e91f8133176fe7a9665f0 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 14:13:35 +0100 Subject: [PATCH 07/16] Fix variable used before creation. --- templates/_macros.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index 5afc960..3619472 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -59,11 +59,11 @@ function taint() { el.disabled = false; }); // deactivate Booking links + let links_text = ''; Array.from(document.getElementsByClassName("disable_on_change")).forEach((el) => { links_text += el.textContent + ' '; }); const span = document.getElementById('booking_links'); - let links_text = ''; span.innerHTML = ''; const del = document.createElement("del"); span.appendChild(del); -- 2.30.2 From 454445ee9fd03c96e8b43074434cf5422ecd09e9 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 15:29:36 +0100 Subject: [PATCH 08/16] Improve structured-editing view. --- templates/_base.tmpl | 3 +- templates/edit_structured.tmpl | 64 +++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/templates/_base.tmpl b/templates/_base.tmpl index af5070c..bc15e6b 100644 --- a/templates/_base.tmpl +++ b/templates/_base.tmpl @@ -10,7 +10,8 @@ body { background-color: white; font-family: sans-serif; } tr.alternating:nth-child(odd) { background-color: #dcdcdc; } tr.alternating:nth-child(even) { background-color: #ffffff; } -td { text-align: left; vertical-align: top; } +td { margin: 0; padding: 0; text-align: left; vertical-align: top; } +input { background-color: transparent; } span.warning, table.warning tbody tr td, tr.warning td { background-color: #ff8888; } {% block css %}{% endblock %} diff --git a/templates/edit_structured.tmpl b/templates/edit_structured.tmpl index 0e510ae..f85a59d 100644 --- a/templates/edit_structured.tmpl +++ b/templates/edit_structured.tmpl @@ -4,6 +4,9 @@ {% block css %} {{ macros.css_td_money() }} {{ macros.css_errors() }} +input.date_input, input.number_input { font-family: monospace; } +input.number_input { text-align: right; } +input.date_input { margin-right: 0.1em; } {% endblock %} @@ -33,59 +36,78 @@ function update_form() { const dat_line = dat_lines[i]; const tr = document.createElement("tr"); table.appendChild(tr); - function add_input(name, value, colspan=1, placeholder='') { + function setup_input_td(tr, colspan) { const td = add_td(tr, colspan); + if (dat_line.error) { td.classList.add("invalid"); }; + return td; + } + function add_input(td, name, value, size) { const input = document.createElement("input"); td.appendChild(input); input.name = `line_${i}_${name}` input.value = value.trim(); - input.placeholder = placeholder; + input.size = size; input.oninput = taint; - if (dat_line.error) { - td.classList.add("invalid"); - } + return input; + } + function add_td_input(name, value, size=20, colspan=1) { + return add_input(setup_input_td(tr, colspan), name, value, size); } if (dat_line.is_intro) { - add_input('date', dat_line.booking_line.date) - add_input('target', dat_line.booking_line.target, 2) - } else if (!dat_line.error) { - add_input('account', dat_line.booking_line.account); - add_input('amount', dat_line.booking_line.amount == 'None' ? '' : dat_line.booking_line.amount); - add_input('currency', dat_line.booking_line.currency, 1, '€'); + const td = setup_input_td(tr, 3); + const date_input = add_input(td, 'date', dat_line.booking_line.date, 10) + date_input.classList.add('date_input'); + add_input(td, 'target', dat_line.booking_line.target, 35) + } else if (!dat_line.error) { // i.e. valid TransferLine + add_td_input('account', dat_line.booking_line.account, 30); + // 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); + amt_input.pattern = '^[0-9]+(\.[0-9]+)?$'; + amt_input.classList.add("number_input"); + // ensure integer amounts at least line up with double-digit decimals + if (amt_input.value.match(/^[0-9]+$/)) { amt_input.value += '.00'; } + // imply that POST handler will set '€' currency if unset, but amount set + const curr_input = add_td_input('currency', dat_line.booking_line.currency, 3); + curr_input.placeholder = '€'; } else { - add_input('error', dat_line.code, 3) + add_td_input('error', dat_line.code, 20, 3) } - add_input('comment', dat_line.comment); + add_td_input('comment', dat_line.comment, 40); + // add action buttons, with "delete" after some safety distance const td_btns = add_td(tr); - add_button(td_btns, 'delete', false, function() { - dat_lines.splice(i, 1); - }); - add_button(td_btns, 'move up', i > 1 ? false : true, function() { + add_button(td_btns, '^', i > 1 ? false : true, function() { const prev_line = dat_lines[i-1]; dat_lines.splice(i-1, 1); dat_lines.splice(i, 0, prev_line); }); - add_button(td_btns, 'move down', i+1 < dat_lines.length ? false : true, function() { + add_button(td_btns, 'v', i+1 < dat_lines.length ? false : true, function() { const next_line = dat_lines[i]; dat_lines.splice(i, 1); dat_lines.splice(i+1, 0, next_line); }); + td_btns.appendChild(document.createTextNode(' · · · ')) + add_button(td_btns, 'delete', false, function() { dat_lines.splice(i, 1); }); + // add error explanation row if necessary if (dat_line.error) { const tr = document.createElement("tr"); table.appendChild(tr); const td = add_td(tr, 3); tr.appendChild(document.createElement("td")); td.textContent = dat_line.error; - td.classList.add("invalid"); tr.classList.add("warning"); } } + // add "add line" row const tr = document.createElement("tr"); table.appendChild(tr); const td = add_td(tr, 5); add_button(td, 'add line', false, function() { - new_line = {error: '', comment: '', booking_line: {account: '', amount: '', currency: ''}}; - dat_lines.push(new_line); + new_line = {error: '', comment: '', booking_line: {account: '', amount: '', currency: ''}}; + dat_lines.push(new_line); + }); + // make all rows alternate background color for better readability + Array.from(table.rows).forEach((tr) => { + tr.classList.add('alternating'); }); } -- 2.30.2 From 826f88de017f5ed29162692a20bf83d10384d4fc Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 19:17:36 +0100 Subject: [PATCH 09/16] Improve Booking editing links bar layout. --- templates/_macros.tmpl | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index 3619472..a899699 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -59,15 +59,16 @@ function taint() { el.disabled = false; }); // deactivate Booking links - let links_text = ''; - Array.from(document.getElementsByClassName("disable_on_change")).forEach((el) => { - links_text += el.textContent + ' '; + Array.from(document.getElementsByClassName("disable_on_change")).forEach((span) => { + let links_text = ''; + Array.from(span.childNodes).forEach((node) => { + links_text += node.textContent + ' '; + }); + span.innerHTML = ''; + const del = document.createElement("del"); + span.appendChild(del); + del.textContent = links_text; }); - const span = document.getElementById('booking_links'); - span.innerHTML = ''; - const del = document.createElement("del"); - span.appendChild(del); - del.textContent = links_text; // remove oninput handlers no longer needed (since we only ever go one way) ['input', 'textarea'].forEach((tag_name) => { Array.from(document.getElementsByTagName(tag_name)).forEach((el) => { @@ -78,13 +79,13 @@ function taint() { {% endmacro %} {% macro edit_bar(target, id) %} + +prev · next + - -switch to {{target}} -balance after -prev -next + +switch to {{target}} · balance after
{% endmacro %} -- 2.30.2 From ae09b6a8aae561c75b12f0189618c711e94d4e0b Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Jan 2025 19:51:44 +0100 Subject: [PATCH 10/16] Improve ledger layout. --- ledger.py | 8 +++++--- templates/_macros.tmpl | 16 +++++++++++----- templates/ledger_raw.tmpl | 1 + templates/ledger_structured.tmpl | 3 +++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ledger.py b/ledger.py index d7ebad6..098c6f6 100755 --- a/ledger.py +++ b/ledger.py @@ -136,6 +136,7 @@ class BookingLine: def __init__(self, booking: 'Booking') -> None: self.errors: list[str] = [] self.booking = booking + self.idx = 0 class IntroLine(BookingLine): @@ -170,8 +171,9 @@ class IntroLine(BookingLine): class TransferLine(BookingLine): """Non-first Booking line, expected to carry value movement.""" - def __init__(self, booking: 'Booking', code: str) -> None: + def __init__(self, booking: 'Booking', code: str, idx: int) -> None: super().__init__(booking) + self.idx = idx self.currency = '' self.amount: Optional[Decimal] = None if not code[0].isspace(): @@ -211,8 +213,8 @@ class Booking: self.intro_line = IntroLine(self, dat_lines[0].code) dat_lines[0].booking_line = self.intro_line self._transfer_lines = [] - for dat_line in dat_lines[1:]: - dat_line.booking_line = TransferLine(self, dat_line.code) + for i, dat_line in enumerate(dat_lines[1:]): + dat_line.booking_line = TransferLine(self, dat_line.code, i + 1) self._transfer_lines += [dat_line.booking_line] changes = Wealth() sink_account = None diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index a899699..c88bce6 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -7,15 +7,21 @@ td.amt, td.curr { font-family: monospace; font-size: 1.3em; } 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 table_dat_lines(dat_lines, raw) %} - +
{% for dat_line in dat_lines %} {% if (not raw) and dat_line.is_intro and loop.index > 1 %} - + {% endif %} {% if dat_line.is_intro %} - + + {% elif dat_line.booking_line.idx == 1 %} + {% else %} {% endif %} @@ -29,8 +35,8 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; } {% else %} {% if dat_line.is_intro %} - {{dat_line.booking_line.date}} - {{dat_line.booking_line.target}} + + {{dat_line.booking_line.target}} {% elif not dat_line.error %} diff --git a/templates/ledger_raw.tmpl b/templates/ledger_raw.tmpl index d09e722..d11e192 100644 --- a/templates/ledger_raw.tmpl +++ b/templates/ledger_raw.tmpl @@ -4,6 +4,7 @@ {% block css %} table { font-family: monospace; } {{ macros.css_errors() }} +{{ macros.css_ledger_index_col() }} {% endblock %} {% block content %} diff --git a/templates/ledger_structured.tmpl b/templates/ledger_structured.tmpl index 1669ce9..654cf80 100644 --- a/templates/ledger_structured.tmpl +++ b/templates/ledger_structured.tmpl @@ -4,6 +4,9 @@ {% block css %} {{ macros.css_td_money() }} {{ macros.css_errors() }} +{{ macros.css_ledger_index_col() }} +table.ledger > tbody > tr > td.date, table.ledger > tbody > tr > td:first-child { font-family: monospace; font-size: 1.3em; text-align: center; } +table.ledger > tbody > tr > td { vertical-align: middle; } {% endblock %} {% block content %} -- 2.30.2 From c4ca973ff2899d919ec0e5ec365756cfb71ba9b2 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 29 Jan 2025 01:50:36 +0100 Subject: [PATCH 11/16] To ledger view, add movement of Bookings up and down. --- ledger.py | 47 ++++++++++++++++++++++++++++---- templates/_macros.tmpl | 8 ++++-- templates/edit_structured.tmpl | 4 +-- templates/ledger_raw.tmpl | 3 +- templates/ledger_structured.tmpl | 3 +- 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/ledger.py b/ledger.py index 098c6f6..0c6ed5e 100755 --- a/ledger.py +++ b/ledger.py @@ -259,6 +259,13 @@ class Handler(PlomHttpHandler): self.server.load() elif 'save_file' in self.postvars.as_dict: self.server.save() + elif self.pagename.startswith('ledger_'): + for key in self.postvars.keys_prefixed('move_'): + toks = key.split('_') + id_ = int(toks[1]) + self.server.move_booking(id_, up=toks[2] == 'up') + self.redirect(Path(self.pagename).joinpath(f'#{id_}')) + return elif self.pagename == 'edit_structured': if self.postvars.first('apply'): line_keys = self.postvars.keys_prefixed('line_') @@ -305,6 +312,8 @@ class Handler(PlomHttpHandler): id_ = int(self.path_toks[2]) if self.pagename.startswith('edit_'): ctx['id'] = id_ + elif self.pagename.startswith('ledger_'): + ctx['max_id'] = self.server.bookings[-1].id_ if self.pagename == 'balance': id_ = int(self.params.first('up_incl') or '-1') valid, balance_roots = self.server.balance_roots(id_) @@ -380,15 +389,41 @@ class Server(PlomHttpServer): '\n'.join([line.raw for line in self.dat_lines]), encoding='utf8') self.load() - def rewrite_booking(self, id_: int, new_dat_lines: list[DatLine]) -> None: - """Rewrite .dat_lines for Booking of .id_ with new_dat_lines.""" - old_booking = self.bookings[id_] - start_idx = self.dat_lines.index(old_booking.dat_lines[0]) - end_idx = self.dat_lines.index(old_booking.dat_lines[-1]) + def _margin_indices(self, booking: Booking) -> tuple[int, int]: + start_idx = self.dat_lines.index(booking.dat_lines[0]) + end_idx = self.dat_lines.index(booking.dat_lines[-1]) + return start_idx, end_idx + + def _replace_from_to(self, + start_idx: int, + end_idx: int, + new_lines: list[DatLine] + ) -> None: self.dat_lines = (self.dat_lines[:start_idx] - + new_dat_lines + self.dat_lines[end_idx+1:]) + + new_lines + + self.dat_lines[end_idx+1:]) self.load_bookings() + def move_booking(self, id_: int, up: bool) -> None: + """Move Booking of id_ one step up or downwards""" + id_other = id_ + (-1 if up else 1) + agent = self.bookings[id_] + other = self.bookings[id_other] + start_agent, end_agent = self._margin_indices(agent) + start_other, end_other = self._margin_indices(other) + gap_lines = self.dat_lines[(end_other if up else end_agent) + 1: + start_agent if up else start_other] + self._replace_from_to(start_other if up else start_agent, + end_agent if up else end_other, + (agent.dat_lines if up else other.dat_lines) + + gap_lines + + (other.dat_lines if up else agent.dat_lines)) + + def rewrite_booking(self, id_: int, new_dat_lines: list[DatLine]) -> None: + """Rewrite .dat_lines for Booking of .id_ with new_dat_lines.""" + self._replace_from_to(*self._margin_indices(self.bookings[id_]), + new_dat_lines) + @property def dat_lines_sans_empty(self) -> list[DatLine]: """Return only those .data_lines with .code or .comment.""" diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index c88bce6..6c2e57c 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -11,7 +11,8 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; } table.ledger tr > td:first-child { background-color: white; } {% endmacro %} -{% macro table_dat_lines(dat_lines, raw) %} +{% macro table_dat_lines(dat_lines, max_id, raw) %} +
 
 
#/b[#][b]{{dat_line.booking_line.date}}{{dat_line.booking_line.amount_short}} {{dat_line.booking_line.currency|truncate(4,true,"…")}}
{% for dat_line in dat_lines %} {% if (not raw) and dat_line.is_intro and loop.index > 1 %} @@ -19,9 +20,9 @@ table.ledger tr > td:first-child { background-color: white; } {% endif %} {% if dat_line.is_intro %} - + {% elif dat_line.booking_line.idx == 1 %} - + {% else %} {% endif %} @@ -56,6 +57,7 @@ table.ledger tr > td:first-child { background-color: white; } {% endif %} {% endfor %}
[#][#][b][b]
+ {% endmacro %} {% macro taint_js() %} diff --git a/templates/edit_structured.tmpl b/templates/edit_structured.tmpl index f85a59d..efa474e 100644 --- a/templates/edit_structured.tmpl +++ b/templates/edit_structured.tmpl @@ -62,10 +62,10 @@ function update_form() { add_td_input('account', dat_line.booking_line.account, 30); // 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); - amt_input.pattern = '^[0-9]+(\.[0-9]+)?$'; + amt_input.pattern = '^-?[0-9]+(\.[0-9]+)?$'; amt_input.classList.add("number_input"); // ensure integer amounts at least line up with double-digit decimals - if (amt_input.value.match(/^[0-9]+$/)) { amt_input.value += '.00'; } + if (amt_input.value.match(/^-?[0-9]+$/)) { amt_input.value += '.00'; } // imply that POST handler will set '€' currency if unset, but amount set const curr_input = add_td_input('currency', dat_line.booking_line.currency, 3); curr_input.placeholder = '€'; diff --git a/templates/ledger_raw.tmpl b/templates/ledger_raw.tmpl index d11e192..d1e13f0 100644 --- a/templates/ledger_raw.tmpl +++ b/templates/ledger_raw.tmpl @@ -5,9 +5,10 @@ table { font-family: monospace; } {{ macros.css_errors() }} {{ macros.css_ledger_index_col() }} +table.ledger > tbody > tr > td:first-child input[type=submit] { font-size: 0.5em; } {% endblock %} {% block content %} -{{ macros.table_dat_lines(dat_lines, raw=true) }} +{{ macros.table_dat_lines(dat_lines, max_id, raw=true) }} {% endblock %} diff --git a/templates/ledger_structured.tmpl b/templates/ledger_structured.tmpl index 654cf80..8d9b88c 100644 --- a/templates/ledger_structured.tmpl +++ b/templates/ledger_structured.tmpl @@ -7,8 +7,9 @@ {{ macros.css_ledger_index_col() }} table.ledger > tbody > tr > td.date, table.ledger > tbody > tr > td:first-child { font-family: monospace; font-size: 1.3em; text-align: center; } table.ledger > tbody > tr > td { vertical-align: middle; } +table.ledger > tbody > tr > td:first-child { white-space: nowrap; } {% endblock %} {% block content %} -{{ macros.table_dat_lines(dat_lines, raw=false) }} +{{ macros.table_dat_lines(dat_lines, max_id, raw=false) }} {% endblock %} -- 2.30.2 From 2b5053b75b953633f5cdaa8194c0992ddd97f909 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 29 Jan 2025 03:41:56 +0100 Subject: [PATCH 12/16] Fix handling of erroneous date fields. --- ledger.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/ledger.py b/ledger.py index 0c6ed5e..ebb188f 100755 --- a/ledger.py +++ b/ledger.py @@ -144,23 +144,17 @@ class IntroLine(BookingLine): def __init__(self, booking: 'Booking', code: str) -> None: super().__init__(booking) - self.date = '#' - self.target = '' if code[0].isspace(): self.errors += ['intro line indented'] toks = code.lstrip().split(maxsplit=1) - if len(toks) != 2: + self.date = toks[0] + self.target = toks[1] if len(toks) > 1 else '' + if len(toks) == 1: self.errors += ['illegal number of tokens'] - elif len(toks) < 1: - return - else: - self.target = toks[1] try: - dt_date.fromisoformat(toks[0]) + dt_date.fromisoformat(self.date) except ValueError: - self.errors += [f'not properly formatted legal date: {toks[0]}'] - else: - self.date = toks[0] + self.errors += [f'not properly formatted legal date: {self.date}'] @property def as_dict(self) -> dict: -- 2.30.2 From 106bf1e521ff02e128863bf8378992299d41bedb Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 29 Jan 2025 03:56:33 +0100 Subject: [PATCH 13/16] Add copying of Bookings. --- ledger.py | 18 ++++++++++++++---- templates/_macros.tmpl | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ledger.py b/ledger.py index ebb188f..f61cbec 100755 --- a/ledger.py +++ b/ledger.py @@ -260,6 +260,11 @@ class Handler(PlomHttpHandler): self.server.move_booking(id_, up=toks[2] == 'up') self.redirect(Path(self.pagename).joinpath(f'#{id_}')) return + for key in self.postvars.keys_prefixed('copy_'): + id_ = int(key.split('_')[1]) + self.server.copy_booking(id_) + self.redirect(Path('/bookings').joinpath(f'{id_}')) + return elif self.pagename == 'edit_structured': if self.postvars.first('apply'): line_keys = self.postvars.keys_prefixed('line_') @@ -350,7 +355,7 @@ class Server(PlomHttpServer): DatLine(line) for line in self._path_dat.read_text(encoding='utf8').splitlines()] self.last_save_hash = self._hash_dat_lines() - self.load_bookings() + self._load_bookings() def _hash_dat_lines(self) -> int: return hash(tuple(dl.raw for dl in self.dat_lines)) @@ -360,8 +365,7 @@ class Server(PlomHttpServer): """If .dat_lines different to those of last .load().""" return self._hash_dat_lines() != self.last_save_hash - def load_bookings(self) -> None: - """Read .dat_lines into Bookings / full ledger.""" + def _load_bookings(self) -> None: self.bookings = [] booking_lines: list[DatLine] = [] last_date = '' @@ -396,7 +400,7 @@ class Server(PlomHttpServer): self.dat_lines = (self.dat_lines[:start_idx] + new_lines + self.dat_lines[end_idx+1:]) - self.load_bookings() + self._load_bookings() def move_booking(self, id_: int, up: bool) -> None: """Move Booking of id_ one step up or downwards""" @@ -418,6 +422,12 @@ class Server(PlomHttpServer): self._replace_from_to(*self._margin_indices(self.bookings[id_]), new_dat_lines) + def copy_booking(self, id_: int) -> None: + """Append copy of Booking of id_ to end of ledger.""" + self.dat_lines += [DatLine('')] + [DatLine(dat_line.raw) for dat_line + in self.bookings[id_].dat_lines] + self._load_bookings() + @property def dat_lines_sans_empty(self) -> list[DatLine]: """Return only those .data_lines with .code or .comment.""" diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index 6c2e57c..dc7b13d 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -23,6 +23,8 @@ table.ledger tr > td:first-child { background-color: white; } [#] {% elif dat_line.booking_line.idx == 1 %} [b] + {% elif dat_line.booking_line.idx == 2 %} + {% else %} {% endif %} -- 2.30.2 From 905cdfa934f8b02292889d5e76b99c30da9a6dcd Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 29 Jan 2025 04:16:02 +0100 Subject: [PATCH 14/16] Differentiate copying between "to end of ledger" and "to after copied". --- ledger.py | 25 ++++++++++++++++++------- templates/_macros.tmpl | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ledger.py b/ledger.py index f61cbec..60106c7 100755 --- a/ledger.py +++ b/ledger.py @@ -261,9 +261,11 @@ class Handler(PlomHttpHandler): self.redirect(Path(self.pagename).joinpath(f'#{id_}')) return for key in self.postvars.keys_prefixed('copy_'): - id_ = int(key.split('_')[1]) - self.server.copy_booking(id_) - self.redirect(Path('/bookings').joinpath(f'{id_}')) + toks = key.split('_', maxsplit=2) + id_ = int(toks[1]) + new_id = self.server.copy_booking(int(toks[1]), + to_end=toks[2] == 'to_end') + self.redirect(Path('/bookings').joinpath(f'{new_id}')) return elif self.pagename == 'edit_structured': if self.postvars.first('apply'): @@ -422,11 +424,20 @@ class Server(PlomHttpServer): self._replace_from_to(*self._margin_indices(self.bookings[id_]), new_dat_lines) - def copy_booking(self, id_: int) -> None: - """Append copy of Booking of id_ to end of ledger.""" - self.dat_lines += [DatLine('')] + [DatLine(dat_line.raw) for dat_line - in self.bookings[id_].dat_lines] + def copy_booking(self, id_: int, to_end: bool) -> int: + """Add copy of Booking of id_ to_end of ledger, or after copied.""" + copied = self.bookings[id_] + new_lines = [DatLine('')] + [DatLine(dat_line.raw) for dat_line + in copied.dat_lines] + if to_end or copied is self.bookings[-1]: + self.dat_lines += new_lines + new_id = self.bookings[-1].id_ + 1 + else: + start = self.dat_lines.index(copied.dat_lines[-1]) + self._replace_from_to(start + 1, start, new_lines) + new_id = copied.id_ + 1 self._load_bookings() + return new_id @property def dat_lines_sans_empty(self) -> list[DatLine]: diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index dc7b13d..f8f9325 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -24,7 +24,7 @@ table.ledger tr > td:first-child { background-color: white; } {% elif dat_line.booking_line.idx == 1 %} [b] {% elif dat_line.booking_line.idx == 2 %} - + {% else %} {% endif %} -- 2.30.2 From ae04079dfdbae57b4253b7dd7b78e85ac539e0e8 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 29 Jan 2025 08:27:37 +0100 Subject: [PATCH 15/16] Re-date to-end Booking copies to current day. --- ledger.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ledger.py b/ledger.py index 60106c7..b9577dc 100755 --- a/ledger.py +++ b/ledger.py @@ -427,17 +427,20 @@ class Server(PlomHttpServer): def copy_booking(self, id_: int, to_end: bool) -> int: """Add copy of Booking of id_ to_end of ledger, or after copied.""" copied = self.bookings[id_] - new_lines = [DatLine('')] + [DatLine(dat_line.raw) for dat_line - in copied.dat_lines] + empty_line = DatLine('') if to_end or copied is self.bookings[-1]: - self.dat_lines += new_lines - new_id = self.bookings[-1].id_ + 1 + intro = DatLine( + f'{dt_date.today().isoformat()} ' + f'{copied.intro_line.target} ; {copied.dat_lines[0].comment}') + self.dat_lines += [empty_line, intro] + copied.dat_lines[1:] + prev_id = self.bookings[-1].id_ else: start = self.dat_lines.index(copied.dat_lines[-1]) - self._replace_from_to(start + 1, start, new_lines) - new_id = copied.id_ + 1 + self._replace_from_to(start + 1, start, + [empty_line] + copied.dat_lines) + prev_id = copied.id_ self._load_bookings() - return new_id + return prev_id + 1 @property def dat_lines_sans_empty(self) -> list[DatLine]: -- 2.30.2 From d84f6f66fd0331967847ba6e5c258bbf0515d7a2 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 29 Jan 2025 08:47:45 +0100 Subject: [PATCH 16/16] Disallow movement into different date. --- ledger.py | 47 ++++++++++++++++++++++++-------- templates/_macros.tmpl | 6 ++-- templates/balance.tmpl | 2 +- templates/ledger_raw.tmpl | 2 +- templates/ledger_structured.tmpl | 2 +- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/ledger.py b/ledger.py index b9577dc..f179efe 100755 --- a/ledger.py +++ b/ledger.py @@ -104,7 +104,12 @@ class DatLine: @property def booking_id(self) -> int: """If .booking_line, its .booking_id, else -1.""" - return self.booking_line.booking.id_ if self.booking_line else -1 + return self.booking.id_ if self.booking else -1 + + @property + def booking(self) -> Optional['Booking']: + """If .booking_line, matching Booking, else None.""" + return self.booking_line.booking if self.booking_line else None @property def error(self) -> str: @@ -203,9 +208,13 @@ class Booking: """Represents lines of individual booking.""" # pylint: disable=too-few-public-methods - def __init__(self, id_: int, dat_lines: list[DatLine]) -> None: - self.intro_line = IntroLine(self, dat_lines[0].code) - dat_lines[0].booking_line = self.intro_line + def __init__(self, + id_: int, + dat_lines: list[DatLine], + prev_booking: Optional[Self] + ) -> None: + self.intro = IntroLine(self, dat_lines[0].code) + dat_lines[0].booking_line = self.intro self._transfer_lines = [] for i, dat_line in enumerate(dat_lines[1:]): dat_line.booking_line = TransferLine(self, dat_line.code, i + 1) @@ -231,11 +240,25 @@ class Booking: self._transfer_lines[-1].errors += ['needed sink missing'] self.id_ = id_ self.dat_lines = dat_lines + self.prev = prev_booking + if self.prev: + self.prev.next = self + self.next: Optional[Self] = None + + def can_move(self, up: bool) -> bool: + """Whether movement rules would allow self to move up or down.""" + if (up and ((not self.prev) + or self.prev.intro.date != self.intro.date)): + return False + if ((not up) and ((not self.next) + or self.next.intro.date != self.intro.date)): + return False + return True @property def is_questionable(self) -> bool: """Whether lines count any errors.""" - for _ in [bl for bl in [self.intro_line] + self._transfer_lines + for _ in [bl for bl in [self.intro] + self._transfer_lines if bl.errors]: return True return False @@ -313,8 +336,6 @@ class Handler(PlomHttpHandler): id_ = int(self.path_toks[2]) if self.pagename.startswith('edit_'): ctx['id'] = id_ - elif self.pagename.startswith('ledger_'): - ctx['max_id'] = self.server.bookings[-1].id_ if self.pagename == 'balance': id_ = int(self.params.first('up_incl') or '-1') valid, balance_roots = self.server.balance_roots(id_) @@ -375,11 +396,13 @@ class Server(PlomHttpServer): if dat_line.code: booking_lines += [dat_line] elif booking_lines: - booking = Booking(len(self.bookings), booking_lines) - if last_date > booking.intro_line.date: - booking.intro_line.errors += ['date < previous valid date'] + booking = Booking( + len(self.bookings), booking_lines, + self.bookings[-1] if self.bookings else None) + if last_date > booking.intro.date: + booking.intro.errors += ['date < previous valid date'] else: - last_date = booking.intro_line.date + last_date = booking.intro.date self.bookings += [booking] booking_lines = [] @@ -431,7 +454,7 @@ class Server(PlomHttpServer): if to_end or copied is self.bookings[-1]: intro = DatLine( f'{dt_date.today().isoformat()} ' - f'{copied.intro_line.target} ; {copied.dat_lines[0].comment}') + f'{copied.intro.target} ; {copied.dat_lines[0].comment}') self.dat_lines += [empty_line, intro] + copied.dat_lines[1:] prev_id = self.bookings[-1].id_ else: diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index f8f9325..6cbeb01 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -11,7 +11,7 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; } table.ledger tr > td:first-child { background-color: white; } {% endmacro %} -{% macro table_dat_lines(dat_lines, max_id, raw) %} +{% macro table_dat_lines(dat_lines, raw) %}
{% for dat_line in dat_lines %} @@ -20,9 +20,9 @@ table.ledger tr > td:first-child { background-color: white; } {% endif %} {% if dat_line.is_intro %} - + {% elif dat_line.booking_line.idx == 1 %} - + {% elif dat_line.booking_line.idx == 2 %} {% else %} diff --git a/templates/balance.tmpl b/templates/balance.tmpl index a8a3b7f..acf81fc 100644 --- a/templates/balance.tmpl +++ b/templates/balance.tmpl @@ -62,7 +62,7 @@ span.indent { letter-spacing: 3em; } {% endblock css %} {% block content %} -

balance after booking {{booking.id_}} ({{booking.intro_line.date}}: {{booking.intro_line.target}})

+

balance after booking {{booking.id_}} ({{booking.intro.date}}: {{booking.intro.target}})

{% for root in roots %} {{ account_with_children(root, indent=0) }} diff --git a/templates/ledger_raw.tmpl b/templates/ledger_raw.tmpl index d1e13f0..7f803ab 100644 --- a/templates/ledger_raw.tmpl +++ b/templates/ledger_raw.tmpl @@ -9,6 +9,6 @@ table.ledger > tbody > tr > td:first-child input[type=submit] { font-size: 0.5em {% endblock %} {% block content %} -{{ macros.table_dat_lines(dat_lines, max_id, raw=true) }} +{{ macros.table_dat_lines(dat_lines, raw=true) }} {% endblock %} diff --git a/templates/ledger_structured.tmpl b/templates/ledger_structured.tmpl index 8d9b88c..da1f46f 100644 --- a/templates/ledger_structured.tmpl +++ b/templates/ledger_structured.tmpl @@ -11,5 +11,5 @@ table.ledger > tbody > tr > td:first-child { white-space: nowrap; } {% endblock %} {% block content %} -{{ macros.table_dat_lines(dat_lines, max_id, raw=false) }} +{{ macros.table_dat_lines(dat_lines, raw=false) }} {% endblock %} -- 2.30.2
[#][#][b][b]