From: Christian Heller Date: Sun, 2 Feb 2025 18:07:37 +0000 (+0100) Subject: When re-writing Booking, move within order according to date if changed. X-Git-Url: https://plomlompom.com/repos/%7B%7Bdb.prefix%7D%7D/%7B%7B%20web_path%20%7D%7D/static/%7B%7Bprefix%7D%7D/day?a=commitdiff_plain;h=HEAD;p=plomledger When re-writing Booking, move within order according to date if changed. --- diff --git a/ledger.py b/ledger.py index f179efe..dcf1c4d 100755 --- a/ledger.py +++ b/ledger.py @@ -206,19 +206,26 @@ class TransferLine(BookingLine): class Booking: """Represents lines of individual booking.""" - # pylint: disable=too-few-public-methods + next: Optional[Self] + prev: Optional[Self] def __init__(self, id_: int, - dat_lines: list[DatLine], - prev_booking: Optional[Self] + booked_lines: list[DatLine], + gap_lines: Optional[list[DatLine]] = None ) -> 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) - self._transfer_lines += [dat_line.booking_line] + self.next, self.prev = None, None + self.id_, self.booked_lines = id_, booked_lines[:] + self._gap_lines = gap_lines[:] if gap_lines else [] + # parse booked_lines into Intro- and TransferLines + self.intro_line = IntroLine(self, self.booked_lines[0].code) + self._transfer_lines = [ + TransferLine(self, b_line.code, i+1) for i, b_line + in enumerate(self.booked_lines[1:])] + self.booked_lines[0].booking_line = self.intro_line + for i, b_line in enumerate(self._transfer_lines): + self.booked_lines[i + 1].booking_line = b_line + # calculate .account_changes changes = Wealth() sink_account = None self.account_changes: dict[str, Wealth] = {} @@ -238,27 +245,58 @@ class Booking: self.account_changes[sink_account] += changes.as_sink elif not changes.sink_empty: self._transfer_lines[-1].errors += ['needed sink missing'] - self.id_ = id_ - self.dat_lines = dat_lines - self.prev = prev_booking + + def recalc_prev_next(self, bookings: list[Self]) -> None: + """Assuming .id_ to be index in bookings, link prev + next Bookings.""" + self.prev = bookings[self.id_ - 1] if self.id_ > 0 else None if self.prev: self.prev.next = self - self.next: Optional[Self] = None + self.next = (bookings[self.id_ + 1] if self.id_ + 1 < len(bookings) + else None) + if self.next: + self.next.prev = self + + @property + def gap_lines(self) -> list[DatLine]: + """Return ._gap_lines or, if empty, list of one empty DatLine.""" + return self._gap_lines if self._gap_lines else [DatLine('')] + + @gap_lines.setter + def gap_lines(self, gap_lines=list[DatLine]) -> None: + self._gap_lines = gap_lines[:] + + @property + def gap_lines_copied(self) -> list[DatLine]: + """Return new DatLines generated from .raw's of .gap_lines.""" + return [DatLine(dat_line.raw) for dat_line in self.gap_lines] + + @property + def booked_lines_copied(self) -> list[DatLine]: + """Return new DatLines generated from .raw's of .booked_lines.""" + return [DatLine(dat_line.raw) for dat_line in self.booked_lines] + + @property + def target(self) -> str: + """Return main other party for transaction.""" + return self.intro_line.target + + @property + def date(self) -> str: + """Return Booking's day's date.""" + return self.intro_line.date 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)): + if up and ((not self.prev) or self.prev.date != self.date): return False - if ((not up) and ((not self.next) - or self.next.intro.date != self.intro.date)): + if (not up) and ((not self.next) or self.next.date != self.date): return False return True @property def is_questionable(self) -> bool: """Whether lines count any errors.""" - for _ in [bl for bl in [self.intro] + self._transfer_lines + for _ in [bl for bl in [self.intro_line] + self._transfer_lines if bl.errors]: return True return False @@ -270,28 +308,14 @@ class Handler(PlomHttpHandler): def do_POST(self) -> None: # pylint: disable=invalid-name,missing-function-docstring - if self.pagename.startswith('edit_'): - id_ = int(self.path_toks[2]) - if 'reload_file' in self.postvars.as_dict: - 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 - for key in self.postvars.keys_prefixed('copy_'): - 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'): + prefix_file, prefix_ledger = 'file_', 'ledger_' + if (file_prefixed := self.postvars.keys_prefixed(prefix_file)): + getattr(self.server, file_prefixed[0][len(prefix_file):])() + elif (self.pagename.startswith('edit_') + and self.postvars.first('apply')): + booking = self.server.bookings[int(self.path_toks[2])] + new_lines = [] + if self.pagename == 'edit_structured': line_keys = self.postvars.keys_prefixed('line_') lineno_to_inputs: dict[int, list[str]] = {} for key in line_keys: @@ -300,33 +324,38 @@ class Handler(PlomHttpHandler): if lineno not in lineno_to_inputs: lineno_to_inputs[lineno] = [] lineno_to_inputs[lineno] += [toks[2]] - new_dat_lines = [] indent = ' ' for lineno, input_names in lineno_to_inputs.items(): data = '' comment = self.postvars.first(f'line_{lineno}_comment') for name in input_names: - input_ = self.postvars.first(f'line_{lineno}_{name}') + input_ = self.postvars.first(f'line_{lineno}_{name}' + ).strip() if name == 'date': data = input_ elif name == 'target': - data += f' {input_}' + data += f' {input_}' elif name == 'error': data = f'{indent}{input_}' elif name == 'account': data = f'{indent}{input_}' elif name in {'amount', 'currency'}: data += f' {input_}' - new_dat_lines += [ + new_lines += [ DatLine(f'{data} ; {comment}' if comment else data)] - # pylint: disable=possibly-used-before-assignment - self.server.rewrite_booking(id_, new_dat_lines) - elif self.pagename == 'edit_raw': - if self.postvars.first('apply'): - new_dat_lines = [ - DatLine(line) for line - in self.postvars.first('booking').splitlines()] - self.server.rewrite_booking(id_, new_dat_lines) + else: # edit_raw + new_lines += [DatLine(line) for line + in self.postvars.first('booking').splitlines()] + new_id = self.server.rewrite_booking(booking.id_, new_lines) + self.redirect(Path('/bookings').joinpath(f'{new_id}')) + return + elif self.pagename.startswith(prefix_ledger): + action, id_str, dir_ =\ + self.postvars.keys_prefixed(prefix_ledger)[0].split('_')[1:] + new_id = getattr(self.server, f'{action}_booking')( + int(id_str), dir_ == 'up' if action == 'move' else 'to_end') + self.redirect(Path(self.path).joinpath(f'#{new_id}')) + return self.redirect(Path(self.path)) def do_GET(self) -> None: @@ -347,11 +376,12 @@ class Handler(PlomHttpHandler): 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] + ctx['dat_lines'] = [ + dl.as_dict for dl + in self.server.bookings[ctx['id']].booked_lines] self.send_rendered(Path('edit_structured.tmpl'), ctx) elif self.pagename == 'edit_raw': - ctx['dat_lines'] = self.server.bookings[ctx['id']].dat_lines + ctx['dat_lines'] = self.server.bookings[ctx['id']].booked_lines self.send_rendered(Path('edit_raw.tmpl'), ctx) elif self.pagename == 'ledger_raw': self.send_rendered(Path('ledger_raw.tmpl'), @@ -366,6 +396,7 @@ class Server(PlomHttpServer): """Extends parent by loading .dat file into database for Handler.""" bookings: list[Booking] dat_lines: list[DatLine] + initial_gap_lines: list[DatLine] def __init__(self, path_dat: Path, *args, **kwargs) -> None: super().__init__(PATH_TEMPLATES, (SERVER_HOST, SERVER_PORT), Handler) @@ -380,6 +411,12 @@ class Server(PlomHttpServer): self.last_save_hash = self._hash_dat_lines() self._load_bookings() + def save(self) -> None: + """Save current state to ._path_dat.""" + self._path_dat.write_text( + '\n'.join([line.raw for line in self.dat_lines]), encoding='utf8') + self.load() + def _hash_dat_lines(self) -> int: return hash(tuple(dl.raw for dl in self.dat_lines)) @@ -389,81 +426,118 @@ class Server(PlomHttpServer): return self._hash_dat_lines() != self.last_save_hash def _load_bookings(self) -> None: - self.bookings = [] - booking_lines: list[DatLine] = [] - last_date = '' + booked_lines: list[DatLine] = [] + gap_lines: list[DatLine] = [] + booking: Optional[Booking] = None + self.bookings, self.initial_gap_lines, last_date = [], [], '' for dat_line in self.dat_lines + [DatLine('')]: if dat_line.code: - booking_lines += [dat_line] - elif booking_lines: - 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.date - self.bookings += [booking] - booking_lines = [] - - def save(self) -> None: - """Save current state to ._path_dat.""" - self._path_dat.write_text( - '\n'.join([line.raw for line in self.dat_lines]), encoding='utf8') - self.load() - - 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_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) + if gap_lines: + if booking: + booking.gap_lines = gap_lines[:] + else: + self.initial_gap_lines = gap_lines[:] + gap_lines.clear() + booked_lines += [dat_line] + else: + if booked_lines: + booking = Booking(len(self.bookings), booked_lines[:]) + if last_date > booking.date: + booking.intro_line.errors += [ + 'date < previous valid date'] + else: + last_date = booking.date + self.bookings += [booking] + booked_lines.clear() + gap_lines += [dat_line] + for booking in self.bookings: + booking.recalc_prev_next(self.bookings) + if booking: + booking.gap_lines = gap_lines[:-1] + + def _recalc_dat_lines(self) -> None: + self.dat_lines = self.initial_gap_lines[:] + for booking in self.bookings: + self.dat_lines += booking.booked_lines + self.dat_lines += booking.gap_lines + + def _move_booking(self, idx_from, idx_to) -> None: + moving = self.bookings[idx_from] + if idx_from >= idx_to: # moving upward, deletion must + del self.bookings[idx_from] # precede insertion to keep + self.bookings[idx_to:idx_to] = [moving] # deletion index, downwards + if idx_from < idx_to: # the other way around keeps + del self.bookings[idx_from] # insertion index + min_idx, max_idx = min(idx_from, idx_to), max(idx_from, idx_to) + for idx, booking in enumerate(self.bookings[min_idx:max_idx + 1]): + booking.id_ = min_idx + idx + booking.recalc_prev_next(self.bookings) + + def move_booking(self, old_id: int, up: bool) -> int: + """Move Booking of old_id one step up or downwards""" + new_id = old_id + (-1 if up else 1) + self._move_booking(old_id, # moving down implies + new_id + (0 if up else 1)) # jumping over next item + self._recalc_dat_lines() + return new_id + + def rewrite_booking(self, old_id: int, new_booked_lines: list[DatLine] + ) -> int: + """Rewrite Booking with new_booked_lines, move if changed date.""" + old_booking = self.bookings[old_id] + new_date = new_booked_lines[0].code.lstrip().split(maxsplit=1)[0] + if new_date == old_booking.date: + new_booking = Booking( + old_id, new_booked_lines, old_booking.gap_lines_copied) + self.bookings[old_id] = new_booking + new_booking.recalc_prev_next(self.bookings) + else: + i_booking = self.bookings[0] + new_idx = None + while i_booking.next: + if not i_booking.prev and i_booking.date > new_date: + new_idx = i_booking.id_ + break + if i_booking.next.date > new_date: + break + i_booking = i_booking.next + if new_idx is None: + new_idx = i_booking.id_ + 1 + # ensure that, if we land in group of like-dated Bookings, we land + # on the edge closest to our last position + if i_booking.date == new_date and old_id < i_booking.id_: + new_idx = [b for b in self.bookings + if b.date == new_date][0].id_ + new_booking = Booking( + new_idx, new_booked_lines, old_booking.gap_lines_copied) + self.bookings[old_id] = new_booking + self._move_booking(old_id, new_idx) + self._recalc_dat_lines() + return new_booking.id_ 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_] - empty_line = DatLine('') - if to_end or copied is self.bookings[-1]: + new_id = len(self.bookings) if to_end else copied.id_ + 1 + if to_end: + intro_comment = copied.booked_lines[0].comment intro = DatLine( - f'{dt_date.today().isoformat()} ' - 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_ + f'{dt_date.today().isoformat()} {copied.target}' + + ' ; {intro_comment}' if intro_comment else '') + new_booking = Booking(new_id, + [intro] + copied.booked_lines_copied[:1], + copied.gap_lines_copied) + self.bookings += [new_booking] else: - start = self.dat_lines.index(copied.dat_lines[-1]) - self._replace_from_to(start + 1, start, - [empty_line] + copied.dat_lines) - prev_id = copied.id_ - self._load_bookings() - return prev_id + 1 + new_booking = Booking(new_id, + copied.booked_lines_copied, + copied.gap_lines_copied) + self.bookings[new_id:new_id] = [new_booking] + for b in self.bookings[new_id + 1:]: + b.id_ += 1 + new_booking.recalc_prev_next(self.bookings) + self._recalc_dat_lines() + return new_id @property def dat_lines_sans_empty(self) -> list[DatLine]: diff --git a/templates/_base.tmpl b/templates/_base.tmpl index bc15e6b..15c54cb 100644 --- a/templates/_base.tmpl +++ b/templates/_base.tmpl @@ -18,7 +18,7 @@ span.warning, table.warning tbody tr td, tr.warning td { background-color: #ff88
-ledger structured / raw · balance · {% if tainted %} · unsaved changes: {% endif %} +ledger structured / raw · balance · {% if tainted %} · unsaved changes: {% endif %}

{% block content %}{% endblock %} diff --git a/templates/_macros.tmpl b/templates/_macros.tmpl index 6cbeb01..5cba91a 100644 --- a/templates/_macros.tmpl +++ b/templates/_macros.tmpl @@ -20,11 +20,11 @@ table.ledger tr > td:first-child { background-color: white; } {% endif %} {% if dat_line.is_intro %} - [#] + [#] {% elif dat_line.booking_line.idx == 1 %} - [b] + [b] {% elif dat_line.booking_line.idx == 2 %} - + {% else %} {% endif %} @@ -38,8 +38,8 @@ table.ledger tr > td:first-child { background-color: white; } {% else %} {% if dat_line.is_intro %} - {{dat_line.booking_line.date}} - {{dat_line.booking_line.target}} + {{dat_line.booking.date}} + {{dat_line.booking.target}} {% elif not dat_line.error %} {{dat_line.booking_line.amount_short}} {{dat_line.booking_line.currency|truncate(4,true,"…")}} diff --git a/templates/balance.tmpl b/templates/balance.tmpl index acf81fc..4cde274 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.date}}: {{booking.intro.target}})

+

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

{% for root in roots %} {{ account_with_children(root, indent=0) }}