From: Christian Heller Date: Thu, 29 Jan 2026 01:18:15 +0000 (+0100) Subject: Overhaul .dat parsing to simplify code, preserve more whitespace, prepend gap lines... X-Git-Url: https://plomlompom.com/repos/booking/%7B%7Bdb.prefix%7D%7D/%7B%7B%20web_path%20%7D%7D/%7B%7Bprefix%7D%7D?a=commitdiff_plain;h=12de97698fb0695ab1fb20fd607d1c434db5b2b9;p=ledgplom Overhaul .dat parsing to simplify code, preserve more whitespace, prepend gap lines rather than append them. --- diff --git a/src/ledgplom/ledger.py b/src/ledgplom/ledger.py index dce6140..ac6acff 100644 --- a/src/ledgplom/ledger.py +++ b/src/ledgplom/ledger.py @@ -1,15 +1,17 @@ 'Actual ledger classes.' # standard libs -from abc import ABC, abstractmethod +from abc import ABC from datetime import date as dt_date from decimal import Decimal, InvalidOperation as DecimalInvalidOperation from pathlib import Path from typing import Any, Generic, Iterator, Optional, Self, TypeVar -TypeDatLine = TypeVar('TypeDatLine', bound='_DatLine') +_TypeDatLine = TypeVar('_TypeDatLine', bound='_DatLine') +_INDENT_CHARS = {' ', '\t'} +_SEP_COMMENTS = ';' _PREFIX_DEF = '#def ' DEFAULT_INDENT = 2 * ' ' @@ -117,41 +119,49 @@ class _Account: class _DatLine: 'Line of .dat file parsed into comments and machine-readable data.' - to_copy = ['code', 'comment'] - def __init__( - self, - code: str = '', - comment: str = '', - ) -> None: - self._code_read = code - self.comment = comment + def __init__(self, text: str) -> None: + self._raw = text + + def _into_parts(self) -> tuple[str, str, str]: + stage_count = 0 + indent = '' + code = '' + comment = '' + for c in self._raw: + if stage_count == 0: + if c in _INDENT_CHARS: + indent += c + else: + stage_count += 1 + if stage_count == 1: + if c != _SEP_COMMENTS: + code += c + else: + stage_count += 1 + if stage_count == 2: + comment += c + return indent, code, comment + + @property + def indent(self) -> str: + 'All (maybe zero) the chars of whitespace line starts with.' + return self._into_parts()[0] @property def code(self) -> str: - 'Return collected code (re-generate by subclasses for writing).' - return self._code_read + 'Either IntroLine or TransferLine instructions.' + return self._into_parts()[1] + + @property + def comment(self) -> str: + 'Anything past ";", with whitespace in-between stripped.' + return self._into_parts()[2][1:].lstrip() @property def raw(self) -> str: "Return as how to be written in .dat file's text content." - comment_part = ' ; '.join([''] + [s for s in [self.comment] if s]) - code_part = f'{self.code} ' if self.code else '' - return f'{code_part}{comment_part.lstrip()}'.rstrip() - - def copy(self) -> Self: - 'Create new instance copying the fields named in .to_copy.' - kwargs = {fieldname: getattr(self, fieldname) - for fieldname in self.to_copy} - return self.__class__(**kwargs) - - @classmethod - def from_raw(cls, line: str) -> Self: - 'Parse line into new _DatLine.' - halves = [t.rstrip() for t in line.split(';', maxsplit=1)] - comment = halves[1].lstrip() if len(halves) > 1 else '' - code = halves[0] - return cls(code, comment) + return self._raw[:] @property def comment_instructions(self) -> dict[str, str]: @@ -159,7 +169,7 @@ class _DatLine: instructions = {} if self.comment.startswith(_PREFIX_DEF): parts = [part.strip() for part - in self.comment[len(_PREFIX_DEF):].split(';')] + 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 '' @@ -167,147 +177,100 @@ class _DatLine: return instructions -class _DatLineSubclass(_DatLine, ABC): - - @classmethod - @abstractmethod - def from_dat(cls, dat_line: '_DatLine') -> Self: - 'Evolve from mere dat_line into subclass.' - - -class _GapLine(_DatLineSubclass): - - @classmethod - def from_dat(cls, dat_line: _DatLine) -> Self: - return cls('', dat_line.raw if dat_line.code else dat_line.comment) - - -class _BookingLine(_DatLineSubclass): +class _BookingLine(_DatLine, ABC): 'Parsed _DatLine belonging to a _Booking.' - dictables = {'comment', 'errors'} - - def __init__(self, comment, errors: Optional[list[str]]) -> None: - super().__init__('', comment) - self._errors: list[str] = errors if errors else [] + _field_names = {'errors', 'comment'} @property def as_dict(self) -> dict[str, Any]: - 'Return as JSON-ready dict attributes listed in .dictables.' + 'Return as JSON-ready dict attributes listed in ._field_names.' def to_dictable(value): if isinstance(value, (str, int)): return value if hasattr(value, 'as_dict'): return value.as_dict - if isinstance(value, list): - return [to_dictable(v) for v in value] + if isinstance(value, tuple): + return tuple(to_dictable(v) for v in value) return str(value) d = {} - for name in self.dictables: + for name in self._field_names | _BookingLine._field_names: d[name] = to_dictable(getattr(self, name)) return d + def _code_into_parts(self, n_parts: int) -> tuple[str, ...]: + maxsplit = n_parts + 1 + parts = self.code.split(maxsplit=maxsplit) + return tuple((parts[idx] if len(parts) > idx else '') + for idx in range(maxsplit)) + @property - def errors(self) -> list[str]: - 'Return collected errors (subclasses may add dynamic ones).' - return self._errors[:] + def errors(self) -> tuple[str, ...]: + 'Whatever is wrong with the respective line.' + return tuple(f'{name} empty' for name in self._field_names + if not getattr(self, name)) class _IntroLine(_BookingLine): 'First line of a _Booking, expected to carry date etc.' - to_copy = ['date', 'target', 'comment'] - dictables = {'date', 'target'} | _BookingLine.dictables + _field_names = {'date', 'target'} - def __init__( - self, - date: str, - target: str, - comment: str = '', - errors: Optional[list[str]] = None, - ) -> None: - super().__init__(comment, errors) - self.target = target - self.date = date + @property + def date(self) -> str: + 'YYYY-MM-DD of Booking.' + return self._code_into_parts(2)[0] + + @property + def target(self) -> str: + 'Whatever the ledger definition actually means by that …' + return self._code_into_parts(2)[1] @property - def errors(self) -> list[str]: - errors = super().errors + def errors(self) -> tuple[str, ...]: + errors = list(super().errors) + if self.indent: + errors += ['intro line indented'] try: dt_date.fromisoformat(self.date) except ValueError: errors += [f'not properly formatted legal date: {self.date}'] - if not self.target: - errors += ['target empty'] - return errors - - @classmethod - def from_dat(cls, dat_line: _DatLine) -> Self: - errors = [] - if dat_line.code[0].isspace(): - errors += ['(intro line indented)'] - toks = dat_line.code.lstrip().split(maxsplit=1) - return cls(toks[0], toks[1] if len(toks) > 1 else '', - dat_line.comment, errors) - - @property - def code(self) -> str: - return f'{self.date} {self.target}' + return tuple(errors) class _TransferLine(_BookingLine): 'Non-first _Booking line, expected to carry value movement.' - to_copy = ['account', 'amount', 'currency', 'comment'] - dictables = {'amount', 'account', 'currency'} | _BookingLine.dictables + _field_names = {'account', 'amount', 'currency'} - def __init__( - self, - account: str, - amount: str, - currency: str, - comment: str = '', - errors: Optional[list[str]] = None - ) -> None: - super().__init__(comment, errors) - self.account = account - self._amount_str = amount - self.currency = currency + @property + def account(self) -> str: + 'Name of Account.' + return self._code_into_parts(3)[0] @property def amount(self) -> Optional[Decimal] | str: 'Decimal if amount known, None if not, str if un-decimable.' - if not self._amount_str: + amount_str = self._code_into_parts(3)[1] + if not amount_str: return None try: - return Decimal(self._amount_str) + return Decimal(amount_str) except DecimalInvalidOperation: - return self._amount_str + return amount_str + + @property + def currency(self) -> str: + 'If .amount but itself not found, will default to "€".' + return (self._code_into_parts(3)[2] or '€') if self.amount else '' @property - def errors(self) -> list[str]: - errors = super().errors + def errors(self) -> tuple[str, ...]: + errors = list(super().errors) + # if not self.indent: + # errors += ['transfer line not indented'] if isinstance(self.amount, str): errors += [f'improper amount value: {self.amount}'] if len(self.currency.split()) > 1: errors += ['improper number of tokens'] - return errors - - @classmethod - def from_dat(cls, dat_line: _DatLine) -> Self: - errors = [] - currency: str = '' - if not dat_line.code[0].isspace(): - errors += ['(transfer line not indented)'] - toks = dat_line.code.lstrip().split(maxsplit=2) - amount_str = toks[1] if len(toks) > 1 else '' - if len(toks) > 1: - currency = toks[2] if 3 == len(toks) else '€' - return cls(toks[0], amount_str, currency, dat_line.comment, errors) - - @property - def code(self) -> str: - code = f'{DEFAULT_INDENT}{self.account}' - if self.amount is not None: - code += f' {self.amount} {self.currency}' - return code + return tuple(errors) @property def amount_short(self) -> str: @@ -321,76 +284,70 @@ class _TransferLine(_BookingLine): return '' -class _LinesBlock(Generic[TypeDatLine]): +class _LinesBlock(Generic[_TypeDatLine]): + _lines: list[_TypeDatLine] - def __init__(self, lines: Optional[list[TypeDatLine]] = None) -> None: - self._lines = lines if lines else [] + def __init__(self) -> None: + self._lines = [] @property - def lines(self) -> list[TypeDatLine]: + def lines(self) -> tuple[_TypeDatLine, ...]: 'Return collected lines.' - return self._lines - - def copy(self) -> Self: - "Re-create via .lines' copy()." - return self.__class__([line.copy() for line in self.lines]) + return tuple(self._lines) + def add(self, lines: tuple[_TypeDatLine, ...], at_end=True) -> None: + 'Grow block downwards by this one DatLine.' + if at_end: + self._lines += lines + else: + self._lines[0:0] = list(lines) -class _Gap(_LinesBlock[_GapLine]): - def add(self, lines: list[_GapLine]) -> None: - 'Grow self by lines.' - self._lines += lines +class _Booking(_LinesBlock[_BookingLine]): @property - def redundant_empty_lines(self): - 'If self has more empty lines than necessary.' - redundancies = [] - prev_line = None - idx_last_non_empty = -1 - for idx, line in enumerate(self._lines): - if line.comment: - idx_last_non_empty = idx - elif '' == prev_line and not line.comment: - redundancies += [line] - prev_line = line - redundancies += [line for line in self._lines[idx_last_non_empty + 2:] - if line not in redundancies] - return redundancies + def _sink_account(self) -> str: + for tf_line in [tl for tl in self.transfer_lines + if tl.amount is None and not tl.errors]: + return tf_line.account + return '' - def remove_redundant_empty_lines(self): - 'From self remove redundant empty lines.' - for line in self.redundant_empty_lines: - self._lines.remove(line) + @property + def lines(self) -> tuple[_BookingLine, ...]: + 'Return collected lines.' + return (self.intro_line, ) + self.transfer_lines + @property + def _diffs_targeted(self) -> dict[str, _Wealth]: + balancing_diff = _Wealth() + diffs_targeted: dict[str, _Wealth] = {'': _Wealth()} + for tf_line in [tl for tl in self.transfer_lines if not tl.errors]: + if tf_line.account not in diffs_targeted: + diffs_targeted[tf_line.account] = _Wealth() + if isinstance(tf_line.amount, Decimal): + diff = _Wealth({tf_line.currency: tf_line.amount}) + diffs_targeted[tf_line.account] += diff + balancing_diff += diff + diffs_targeted[self._sink_account] += balancing_diff.as_sink + return diffs_targeted -class _Booking(_LinesBlock[_BookingLine]): + @property + def diffs_targeted(self) -> dict[str, _Wealth]: + 'All wealth differences explicitly defined in transfer lines.' + return {acc: diff for acc, diff in self._diffs_targeted.items() if acc} - def __init__(self, lines: list[_BookingLine]) -> None: - super().__init__(lines) - diffs = _Wealth() - sink_account = None - self.sink_error = '' - self.diffs_targeted: dict[str, _Wealth] = {} - for tf_line in [tl for tl in self.transfer_lines if not tl.errors]: - if tf_line.account not in self.diffs_targeted: - self.diffs_targeted[tf_line.account] = _Wealth() - if tf_line.amount is None: - if sink_account: - self.sink_error = 'too many sinks' - else: - sink_account = tf_line.account - continue - assert isinstance(tf_line.amount, Decimal) - diff = _Wealth({tf_line.currency: tf_line.amount}) - self.diffs_targeted[tf_line.account] += diff - diffs += diff - if sink_account: - self.diffs_targeted[sink_account] += diffs.as_sink - elif diffs.as_sink.moneys: - self.sink_error = 'sink missing for: '\ - + ', '.join(f'{amt} {cur}' - for cur, amt in diffs.as_sink.moneys.items()) + @property + def sink_error(self) -> str: + 'Message on error, if any, regarding sink calculation/placement.' + for _ in [tl for tl in self.transfer_lines + if tl.account != self._sink_account + and tl.amount is None + and not tl.errors]: + return 'too many sinks' + unsunk_moneys = self._diffs_targeted[''].moneys.items() + return ('' if not unsunk_moneys else + ('sink missing for: ' + + (', '.join(f'{amt} {cur}' for cur, amt in unsunk_moneys)))) @property def diffs_inheriting(self) -> dict[str, _Wealth]: @@ -406,17 +363,13 @@ class _Booking(_LinesBlock[_BookingLine]): @property def intro_line(self) -> _IntroLine: 'Return collected _IntroLine.' - assert isinstance(self._lines[0], _IntroLine) - return self._lines[0] - - @property - def transfer_lines(self) -> list[_TransferLine]: - 'Return collected _TransferLines.' # NB: currently no easy way to - return self._lines[1:] # type: ignore # assert mypy list be of type + return _IntroLine(self._lines[0].raw) @property - def lines(self) -> list[_BookingLine]: - return [self.intro_line] + list(self.transfer_lines) + def transfer_lines(self) -> tuple[_TransferLine, ...]: + 'Any lines past the first with .code.' + return tuple(_TransferLine(line.raw) for line in self._lines[1:] + if line.code) @property def date(self) -> str: @@ -428,25 +381,65 @@ class _Booking(_LinesBlock[_BookingLine]): 'Main other party for transaction.' return self.intro_line.target - def copy_to_current_date(self) -> Self: - 'Make copy of same lines but now as date.' - copy = self.copy() - copy.intro_line.date = dt_date.today().isoformat() - return copy +class _DatBlock(_LinesBlock[_DatLine]): + 'Unit of lines with optional .booking, and (possibly zero) .gap_lines.' + _prev: Optional[Self] = None + _next: Optional[Self] = None -class _DatBlock: - 'Unit of lines with optional .booking, and possibly empty .gap.' + @classmethod + def from_lines(cls, lines: tuple[str, ...]) -> tuple['_DatBlock', ...]: + 'Sequence of DatBlocks parsed from lines.' + i_block: _DatBlock = cls() + blocks = [] + for dat_line in (_DatLine(line) for line in lines): + if (not dat_line.indent) and i_block.indented: + blocks += [i_block] + i_block.next = _DatBlock() + i_block = i_block.next + i_block.add((dat_line, )) + return tuple(blocks) - def __init__( - self, - booking: Optional['_Booking'], - gap: Optional['_Gap'] = None - ) -> None: - self.booking = booking - self.gap = gap if gap else _Gap() - self._prev: Optional[Self] = None - self._next: Optional[Self] = None + @property + def indented(self) -> bool: + 'Does the block contain any indented lines?' + return bool([line for line in self.lines if line.indent]) + + @property + def gap_lines(self) -> tuple[_DatLine, ...]: + 'Sequence of all included DatLines without code and indent.' + return tuple(line for line in self.lines + if not line.indent + line.code) + + @property + def redundant_empty_lines(self) -> tuple[_DatLine, ...]: + 'Empty lines in sequence (separated between .gap_lines and .booking).' + redundancies: list[_DatLine] = [] + for lines in (self.gap_lines, + self.booking.lines if self.booking else tuple()): + prev_line: Optional[_DatLine] = None + for line in lines: + if prev_line and not (line.raw.strip() + or prev_line.raw.strip()): + redundancies += [line] + prev_line = line + return tuple(redundancies) + + def remove_redundant_empty_lines(self): + 'From self remove .redundant_empty_lines.' + for line in self.redundant_empty_lines: + self._lines.remove(line) + + @property + def booking(self) -> Optional[_Booking]: + 'Booking made from lines indented or with code.' + booking_lines = tuple(_BookingLine(line.raw) for line in self.lines + if line.indent or line.code) + if not booking_lines: + return None + booking = _Booking() + booking.add(booking_lines) + return booking @property def id_(self) -> int: @@ -465,14 +458,6 @@ class _DatBlock: return 'date < previous date' return '' - @property - def lines(self) -> list[_BookingLine | _GapLine]: - 'Return .lines of .booking and .gap as list[_DatLine].' - lines = (self.booking.lines if self.booking else []) + self.gap.lines - if self.booking and not self.gap.lines: - lines += [_GapLine()] - return lines - def _set_neighbor( self, new_this: Optional[Self], @@ -488,21 +473,21 @@ class _DatBlock: setattr(self, f'_{this}', new_this) @property - def next(self) -> Optional['_DatBlock']: + def next(self) -> Optional[Self]: 'Successor in chain.' return self._next @next.setter - def next(self, new_next: Optional['_DatBlock']) -> None: + def next(self, new_next: Optional[Self]) -> None: self._set_neighbor(new_next, 'next', 'prev') @property - def prev(self) -> Optional['_DatBlock']: + def prev(self) -> Optional[Self]: 'Predecessor in chain.' return self._prev @prev.setter - def prev(self, new_prev: Optional['_DatBlock']): + def prev(self, new_prev: Optional[Self]): self._set_neighbor(new_prev, 'prev', 'next') @property @@ -535,13 +520,6 @@ class _DatBlock: self.prev = old_next old_next.prev = old_prev - def drop(self) -> None: - 'Remove from chain.' - if self.prev: - self.prev.next = self.next - elif self.next: - self.next.prev = self.prev - def fix_position(self): 'Move around in chain until properly positioned by .date.' while self.prev and self.prev.date > self.date: @@ -549,19 +527,18 @@ class _DatBlock: while self.next and self.next.date < self.date: self.move(up=False) - def replace_with(self, new_block: Self) -> None: - 'Have new_block take own position.' - if self.prev: - self.prev.next = new_block - if self.next: - self.next.prev = new_block - new_block.fix_position() - - def copy_to_current_date(self) -> '_DatBlock': - 'Make copy of same lines but now as date, position accordingly.' - copy = _DatBlock( - self.booking.copy_to_current_date() if self.booking else None, - self.gap.copy()) + def copy_to_current_date(self) -> Self: + 'Make copy of same lines but date of now, position accordingly.' + copy = self.__class__() + copy.add(tuple(_DatLine(line.raw) for line in self.gap_lines)) + if self.booking: + copy.add((_DatLine(' '.join((dt_date.today().isoformat(), + self.booking.intro_line.target, + self.booking.intro_line.comment))), + ), + at_end=False) + copy.add(tuple(_DatLine(line.raw) + for line in self.booking.transfer_lines)) if self.next: self.next.prev = copy self.next = copy @@ -579,26 +556,9 @@ class Ledger: def load(self) -> None: 'Re-)read ledger from file at ._path_dat.' - dat_lines: list[_DatLine] = [ - _DatLine.from_raw(line) - for line in self._path_dat.read_text(encoding='utf8').splitlines()] - if (not dat_lines) or dat_lines[-1].code: # ensure final gap line so - dat_lines += [_DatLine()] # last booking gets finished - booking_lines: list[_BookingLine] = [] - i_block = _DatBlock(None, _Gap()) - self._blocks_start = i_block - for dat_line in dat_lines: - if bool(dat_line.code): - if not booking_lines: - booking_lines += [_IntroLine.from_dat(dat_line)] - else: - booking_lines += [_TransferLine.from_dat(dat_line)] - else: # enter new gap -> ready to start next block - if booking_lines: - i_block.next = _DatBlock(_Booking(booking_lines)) - i_block = i_block.next - booking_lines = [] - i_block.gap.add([_GapLine.from_dat(dat_line)]) + blocks = _DatBlock.from_lines( + tuple(self._path_dat.read_text(encoding='utf8').splitlines())) + self._blocks_start = blocks[0] if blocks else _DatBlock() self.last_save_hash = self._hash_dat_lines() @property @@ -612,12 +572,12 @@ class Ledger: return blocks @property - def _dat_lines(self) -> list[_DatLine]: + def _dat_lines(self) -> tuple[_DatLine, ...]: 'From .blocks build list of current _DatLines.' lines = [] for block in self.blocks: - lines += block.lines - return [_DatLine(line.code, line.comment) for line in lines] + lines += list(block.lines) + return tuple(lines) def _calc_accounts(self) -> dict[str, _Account]: 'Build mapping of account names to _Accounts.' @@ -666,14 +626,13 @@ class Ledger: @property def _has_redundant_empty_lines(self) -> bool: - 'If any gaps have redunant empty lines.' - return bool([b for b in self.blocks if b.gap.redundant_empty_lines]) + 'If any blocks have redunant empty lines.' + return bool([b for b in self.blocks if b.redundant_empty_lines]) def remove_redundant_empty_lines(self) -> None: 'From all .blocks remove redundant empty lines.' - for gap in [b.gap for b in self.blocks - if b.gap.redundant_empty_lines]: - gap.remove_redundant_empty_lines() + for block in self.blocks: + block.remove_redundant_empty_lines() @property def tainted(self) -> bool: @@ -688,46 +647,28 @@ class Ledger: def rewrite_block(self, old_id: int, new_lines: list[str]) -> int: 'Rewrite with new_lines, move if changed date.' - lines_gap_pre_booking: list[_GapLine] = [] - lines_booking: list[_BookingLine] = [] - lines_gap_post_booking: list[_GapLine] = [] - for dat_line in [_DatLine.from_raw(line) for line in new_lines]: - if dat_line.code: - if lines_gap_post_booking: # .code belongs to _next_ Booking - lines_gap_post_booking += [_GapLine.from_dat(dat_line)] - elif not lines_booking: - lines_booking += [_IntroLine.from_dat(dat_line)] - else: - lines_booking += [_TransferLine.from_dat(dat_line)] - else: - if not lines_booking: - lines_gap_pre_booking += [_GapLine.from_dat(dat_line)] - else: - lines_gap_post_booking += [_GapLine.from_dat(dat_line)] old_block = self.blocks[old_id] - if not lines_booking: - if old_block.prev: - old_block.prev.gap.add(lines_gap_pre_booking) - old_block.drop() - else: - old_block.booking = None - old_block.gap.add(lines_gap_pre_booking) - return max(0, old_id - 1) - new_block = _DatBlock(_Booking(lines_booking), - _Gap(lines_gap_post_booking)) - self.blocks[old_id].replace_with(new_block) - if not new_block.prev: - self._blocks_start = _DatBlock(None, _Gap()) - self._blocks_start.next = new_block - assert new_block.prev is not None - if lines_gap_pre_booking: - new_block.prev.gap.add(lines_gap_pre_booking) - return new_block.id_ + old_prev, old_next = old_block.prev, old_block.next + new_blocks = _DatBlock.from_lines(tuple(new_lines)) + if old_next: + if not new_blocks[0].booking: + assert len(new_blocks) == 1 + old_next.add(new_blocks[0].lines) + old_next.prev = old_prev + return old_next.id_ + old_next.prev = new_blocks[-1] + if old_prev: + old_prev.next = new_blocks[0] + else: + self._blocks_start = new_blocks[0] + for block in new_blocks: + block.fix_position() + return new_blocks[0].id_ def add_empty_block(self) -> int: 'Add new _DatBlock of empty _Booking to end of ledger.' - new_block = _DatBlock( - _Booking([_IntroLine(dt_date.today().isoformat(), '?')])) + new_block = _DatBlock() + new_block.add((_DatLine(f'{dt_date.today().isoformat()} ?'), )) self.blocks[-1].next = new_block new_block.fix_position() return new_block.id_ @@ -751,12 +692,10 @@ class Ledger: 'All context data relevant for rendering an edit view.' block = self.blocks[id_] if lines: - return {'raw_gap_lines': [dl.raw for dl in block.gap.lines], - 'booking_lines': ( - [block.booking.intro_line.as_dict] - + [tf_line.as_dict - for tf_line in block.booking.transfer_lines] - ) if block.booking else []} + return {'raw_gap_lines': [dl.raw for dl in block.gap_lines], + 'booking_lines': ([line.as_dict + for line in block.booking.lines] + if block.booking else tuple())} accounts = self._calc_accounts() roots: list[dict[str, Any]] = [] for full_path in sorted(block.booking.diffs_targeted.keys() @@ -791,7 +730,7 @@ class Ledger: 'valid': self._blocks_valid_up_incl(id_), 'roots': roots} if not raw: - ctx['raw_gap_lines'] = [dl.raw for dl in block.gap.lines] + ctx['raw_gap_lines'] = [dl.raw for dl in block.gap_lines] ctx['all_accounts'] = sorted(accounts.keys()) return ctx diff --git a/src/templates/edit_structured.html b/src/templates/edit_structured.html index 675954a..f62b85b 100644 --- a/src/templates/edit_structured.html +++ b/src/templates/edit_structured.html @@ -37,8 +37,6 @@ to
- -
Gap:
+ +
{% for acc in all_accounts %} diff --git a/src/templates/edit_structured.js b/src/templates/edit_structured.js index e83816a..00f0932 100644 --- a/src/templates/edit_structured.js +++ b/src/templates/edit_structured.js @@ -95,10 +95,10 @@ const updateForm = () => { table = document.getElementById("booking_lines"), textarea = document.getElementById("gap_lines"); - // empty and redo gapLines, empty bookingLines table - textarea.value = ""; + // empty and redo gapLines textarea, empty bookingLines table + textarea.innerHTML = "" rawGapLines.forEach((line) => { - textarea.value += `${line}\n`; + textarea.innerHTML += `${line}\n`; }); table.innerHTML = ""; diff --git a/src/templates/ledger_structured.html b/src/templates/ledger_structured.html index 6c06acd..26318f0 100644 --- a/src/templates/ledger_structured.html +++ b/src/templates/ledger_structured.html @@ -16,6 +16,11 @@ {% for block in blocks %} {{ macros.ledger_block_columns('structured', block) -}} +{##}{% for line in block.gap_lines %} + + {{ line.raw }}  + +{##}{% endfor %} {##}{% if block.booking %} {####}{% endfor %} {##}{% endif %} -{##}{% for line in block.gap.lines %} - - {{ line.raw }}  - -{##}{% endfor %} {% endfor %} diff --git a/src/tests/empty.ledger_raw b/src/tests/empty.ledger_raw index def169c..a479a03 100644 --- a/src/tests/empty.ledger_raw +++ b/src/tests/empty.ledger_raw @@ -55,20 +55,19 @@ table { - +

- + [#]
[b]
[e] -   diff --git a/src/tests/empty.ledger_structured b/src/tests/empty.ledger_structured index 34da947..8654781 100644 --- a/src/tests/empty.ledger_structured +++ b/src/tests/empty.ledger_structured @@ -60,22 +60,19 @@ td.currency { - +

- + [#]
[b]
[e] - -   - diff --git a/src/tests/full.balance b/src/tests/full.balance index f68cfd4..d5684a1 100644 --- a/src/tests/full.balance +++ b/src/tests/full.balance @@ -78,10 +78,10 @@ span.indent {

- prev + prev next | - balance after booking 5 (2001-01-03: test) + balance after booking 4 (2001-01-03: test)

diff --git a/src/tests/full.balance.3 b/src/tests/full.balance.3 new file mode 100644 index 0000000..37e623d --- /dev/null +++ b/src/tests/full.balance.3 @@ -0,0 +1,239 @@ + + + + + + +

+ prev + next + | + balance after booking 3 (2001-01-03: test) +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
-10€
+
+ + + + + + + +
-1USD
+
+
+ bar: +
+ + + + + + + +
-10€
+
+  :x: + bla bla bla
+ + + + + + + +
-10€
+
+   :y +
+ + + + + + + +
-1USD
+
+  :z +
+ + + + + + + +
-10€
+
+ baz +
+
+ + + + + + + + +
20€
+
+ + + + + + + +
1USD
+
+
+ foo: +
+
+ + + + + + + + +
10€
+
+ + + + + + + +
1USD
+
+
+  :x +
+ + diff --git a/src/tests/full.balance.4 b/src/tests/full.balance.4 deleted file mode 100644 index 6785650..0000000 --- a/src/tests/full.balance.4 +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - -

- prev - next - | - balance after booking 4 (2001-01-03: test) -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - -
-10€
-
- - - - - - - -
-1USD
-
-
- bar: -
- - - - - - - -
-10€
-
-  :x: - bla bla bla
- - - - - - - -
-10€
-
-   :y -
- - - - - - - -
-1USD
-
-  :z -
- - - - - - - -
-10€
-
- baz -
-
- - - - - - - - -
20€
-
- - - - - - - -
1USD
-
-
- foo: -
-
- - - - - - - - -
10€
-
- - - - - - - -
1USD
-
-
-  :x -
- - diff --git a/src/tests/full.edit_raw.0 b/src/tests/full.edit_raw.0 new file mode 100644 index 0000000..7b98e50 --- /dev/null +++ b/src/tests/full.edit_raw.0 @@ -0,0 +1,169 @@ + + + + + + + +
+ +prev +next + + + + +switch to structured +· +balance after +· +in ledger + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
accountbeforediffafter
bar + + + + + + + +
0€
+
+ + + + + + + +
-10€
+
+ + + + + + + +
-10€
+
foo + + + + + + + +
0€
+
+ + + + + + + +
10€
+
+ + + + + + + +
10€
+
+ + diff --git a/src/tests/full.edit_raw.1 b/src/tests/full.edit_raw.1 deleted file mode 100644 index bf3f02b..0000000 --- a/src/tests/full.edit_raw.1 +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - -
- -prev -next - - - - -switch to structured -· -balance after -· -in ledger - -
- - -
- - - - - - - - - - - - - - - - - - - - - -
accountbeforediffafter
bar - - - - - - - -
0€
-
- - - - - - - -
-10€
-
- - - - - - - -
-10€
-
foo - - - - - - - -
0€
-
- - - - - - - -
10€
-
- - - - - - - -
10€
-
- - diff --git a/src/tests/full.edit_raw.3 b/src/tests/full.edit_raw.3 new file mode 100644 index 0000000..f7197ef --- /dev/null +++ b/src/tests/full.edit_raw.3 @@ -0,0 +1,338 @@ + + + + + + + +
+ +prev +next + + + + +switch to structured +· +balance after +· +in ledger + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
accountbeforediffafter
bar: + + + + + + + + + + + +
0€
0USD
+
+ + + + + + + + + + + +
-10€
-1USD
+
+ + + + + + + + + + + +
-10€
-1USD
+
bar:x: + + + + + + + +
0€
+
+ + + + + + + +
-10€
+
+ + + + + + + +
-10€
+
bar:x:y + + + + + + + +
0€
+
+ + + + + + + +
-10€
+
+ + + + + + + +
-10€
+
bar:z + + + + + + + +
0USD
+
+ + + + + + + +
-1USD
+
+ + + + + + + +
-1USD
+
foo: + + + + + + + + + + + +
10€
0USD
+
+ + + + + + + + + + + +
10€
1USD
+
+ + + + + + + + + + + +
20€
1USD
+
foo:x + + + + + + + + + + + +
0€
0USD
+
+ + + + + + + + + + + +
10€
1USD
+
+ + + + + + + + + + + +
10€
1USD
+
+ + diff --git a/src/tests/full.edit_raw.4 b/src/tests/full.edit_raw.4 deleted file mode 100644 index 158fa53..0000000 --- a/src/tests/full.edit_raw.4 +++ /dev/null @@ -1,338 +0,0 @@ - - - - - - - -
- -prev -next - - - - -switch to structured -· -balance after -· -in ledger - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
accountbeforediffafter
bar: - - - - - - - - - - - -
0€
0USD
-
- - - - - - - - - - - -
-10€
-1USD
-
- - - - - - - - - - - -
-10€
-1USD
-
bar:x: - - - - - - - -
0€
-
- - - - - - - -
-10€
-
- - - - - - - -
-10€
-
bar:x:y - - - - - - - -
0€
-
- - - - - - - -
-10€
-
- - - - - - - -
-10€
-
bar:z - - - - - - - -
0USD
-
- - - - - - - -
-1USD
-
- - - - - - - -
-1USD
-
foo: - - - - - - - - - - - -
10€
0USD
-
- - - - - - - - - - - -
10€
1USD
-
- - - - - - - - - - - -
20€
1USD
-
foo:x - - - - - - - - - - - -
0€
0USD
-
- - - - - - - - - - - -
10€
1USD
-
- - - - - - - - - - - -
10€
1USD
-
- - diff --git a/src/tests/full.edit_structured.0 b/src/tests/full.edit_structured.0 new file mode 100644 index 0000000..ea6ffe1 --- /dev/null +++ b/src/tests/full.edit_structured.0 @@ -0,0 +1,259 @@ + + + + + + + +
+ +prev +next + + + + +switch to raw +· +balance after +· +in ledger + +
+ + + + +| + +from + +to + + +
+
Gap:
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
accountbeforediffafter
bar + + + + + + + +
0€
+
+ + + + + + + +
-10€
+
+ + + + + + + +
-10€
+
foo + + + + + + + +
0€
+
+ + + + + + + +
10€
+
+ + + + + + + +
10€
+
+ + diff --git a/src/tests/full.edit_structured.1 b/src/tests/full.edit_structured.1 deleted file mode 100644 index 4d9fbb5..0000000 --- a/src/tests/full.edit_structured.1 +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - -
- -prev -next - - - - -switch to raw -· -balance after -· -in ledger - -
- - - - -| - -from - -to - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - -
-
-
Gap:
- -
- - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
accountbeforediffafter
bar - - - - - - - -
0€
-
- - - - - - - -
-10€
-
- - - - - - - -
-10€
-
foo - - - - - - - -
0€
-
- - - - - - - -
10€
-
- - - - - - - -
10€
-
- - diff --git a/src/tests/full.edit_structured.3 b/src/tests/full.edit_structured.3 new file mode 100644 index 0000000..469bcbb --- /dev/null +++ b/src/tests/full.edit_structured.3 @@ -0,0 +1,470 @@ + + + + + + + +
+ +prev +next + + + + +switch to raw +· +balance after +· +in ledger + +
+ + + + +| + +from + +to + + +
+
Gap:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
accountbeforediffafter
bar: + + + + + + + + + + + +
0€
0USD
+
+ + + + + + + + + + + +
-10€
-1USD
+
+ + + + + + + + + + + +
-10€
-1USD
+
bar:x: + + + + + + + +
0€
+
+ + + + + + + +
-10€
+
+ + + + + + + +
-10€
+
bar:x:y + + + + + + + +
0€
+
+ + + + + + + +
-10€
+
+ + + + + + + +
-10€
+
bar:z + + + + + + + +
0USD
+
+ + + + + + + +
-1USD
+
+ + + + + + + +
-1USD
+
foo: + + + + + + + + + + + +
10€
0USD
+
+ + + + + + + + + + + +
10€
1USD
+
+ + + + + + + + + + + +
20€
1USD
+
foo:x + + + + + + + + + + + +
0€
0USD
+
+ + + + + + + + + + + +
10€
1USD
+
+ + + + + + + + + + + +
10€
1USD
+
+ + diff --git a/src/tests/full.edit_structured.4 b/src/tests/full.edit_structured.4 index 2626557..c7097a3 100644 --- a/src/tests/full.edit_structured.4 +++ b/src/tests/full.edit_structured.4 @@ -78,7 +78,7 @@ input.amount {
prev -next +next @@ -90,6 +90,12 @@ input.amount { in ledger
+
+ block-wide errors: + + sink missing for: -1 €, -2 USD +
+
@@ -102,6 +108,9 @@ to
+
Gap:
+ @@ -149,7 +158,7 @@ to
- + @@ -171,7 +180,7 @@ to - + @@ -208,8 +217,6 @@ to
-
Gap:
-
@@ -221,7 +228,7 @@ to
- +
@@ -235,11 +242,11 @@ to
account
- + - + @@ -249,7 +256,7 @@ to
0-10 €
0-1 USD
- + @@ -263,11 +270,11 @@ to
-10-9 €
- + - + @@ -280,7 +287,7 @@ to
-10-19 €
-1-2 USD
- + @@ -290,7 +297,7 @@ to
0-10 €
- + @@ -300,7 +307,7 @@ to
-10-9 €
- + @@ -313,7 +320,7 @@ to
-10-19 €
- + @@ -323,7 +330,7 @@ to
0-10 €
- + @@ -333,7 +340,7 @@ to
-10-9 €
- + @@ -346,7 +353,7 @@ to
-10-19 €
- + @@ -366,7 +373,7 @@ to
0-1 USD
- + @@ -379,11 +386,11 @@ to
-1-2 USD
- + - + @@ -397,7 +404,7 @@ to - + @@ -407,11 +414,11 @@ to
1020 €
01 USD
€
13 USD
- + - + @@ -424,11 +431,11 @@ to
2030 €
14 USD
- + - + @@ -442,7 +449,7 @@ to - + @@ -452,11 +459,11 @@ to
010 €
01 USD
€
13 USD
- + - + diff --git a/src/tests/full.edit_structured.5 b/src/tests/full.edit_structured.5 deleted file mode 100644 index b85a6e6..0000000 --- a/src/tests/full.edit_structured.5 +++ /dev/null @@ -1,475 +0,0 @@ - - - - - - - - - -prev -next - - - - -switch to raw -· -balance after -· -in ledger - -
-
- block-wide errors: - - sink missing for: -1 €, -2 USD -
-
- - - - -| - -from - -to - - -
- -
1020 €
14 USD
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - -
- - - - - - - - - - - - - -
- -
Gap:
- - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
accountbeforediffafter
bar: - - - - - - - - - - - -
-10€
-1USD
-
- - - - - - - - - - - -
-9€
-1USD
-
- - - - - - - - - - - -
-19€
-2USD
-
bar:x: - - - - - - - -
-10€
-
- - - - - - - -
-9€
-
- - - - - - - -
-19€
-
bar:x:y - - - - - - - -
-10€
-
- - - - - - - -
-9€
-
- - - - - - - -
-19€
-
bar:z - - - - - - - -
-1USD
-
- - - - - - - -
-1USD
-
- - - - - - - -
-2USD
-
foo: - - - - - - - - - - - -
20€
1USD
-
- - - - - - - - - - - -
10€
3USD
-
- - - - - - - - - - - -
30€
4USD
-
foo:x - - - - - - - - - - - -
10€
1USD
-
- - - - - - - - - - - -
10€
3USD
-
- - - - - - - - - - - -
20€
4USD
-
- - diff --git a/src/tests/full.ledger_raw b/src/tests/full.ledger_raw index 89896b6..40fef85 100644 --- a/src/tests/full.ledger_raw +++ b/src/tests/full.ledger_raw @@ -57,14 +57,14 @@ Detected redundant empty lines in gaps, - +

- + [#]
[b]
[e] @@ -72,12 +72,15 @@ Detected redundant empty lines in gaps,
- +
@@ -87,16 +90,16 @@ Detected redundant empty lines in gaps, e] - 2001-01-01 test ; foo  - foo 10 €  - bar -10 €    + 2001-01-02 test  + bar -10 € ; bar  + baz 10 €  - +
- +
@@ -106,72 +109,53 @@ Detected redundant empty lines in gaps, e] - 2001-01-02 test  - bar -10 € ; bar  - baz 10 €      + 2001-01-02 test  + bar 20 €  + baz -20 € ; baz  - - + +
- +
- + [#]
[b]
[e] - 2001-01-02 test  - bar 20 €  - baz -20 € ; baz    + 2001-01-03 test  + foo:x 10 €  + foo:x 1 USD  + bar:x:y -10 €  + bar:z -1 USD  - +
- +
- + [#]
[b]
[e] - 2001-01-03 test  - foo:x 10 €  - foo:x 1 USD  - bar:x:y -10 €  - bar:z -1 USD    - - - - -
- -
- - - - [#]
- [b]
- [e] - - 2001-01-03 test  - foo:x 10 €  - foo:x 3 USD  - bar:x:y -9 €  - bar:z -1 USD  -   + foo:x 10 €  + foo:x 3 USD  + bar:x:y -9 €  + bar:z -1 USD  diff --git a/src/tests/full.ledger_structured b/src/tests/full.ledger_structured index cf0ea78..6227d8e 100644 --- a/src/tests/full.ledger_structured +++ b/src/tests/full.ledger_structured @@ -62,14 +62,14 @@ Detected redundant empty lines in gaps, - +

- + [#]
[b]
[e] @@ -81,12 +81,28 @@ Detected redundant empty lines in gaps,   + + 2001-01-01 test + foo + + + 10.00 + € + foo + + + + -10.00 + € + bar + +
- +
@@ -97,30 +113,30 @@ Detected redundant empty lines in gaps, 2001-01-01 test - foo +   - 10.00 - € - foo + 2001-01-02 test -10.00 € bar - + bar -   + 10.00 + € + baz + - +
- +
@@ -130,43 +146,12 @@ Detected redundant empty lines in gaps, e] - - 2001-01-02 test - - - - -10.00 - € - bar - bar - - - 10.00 - € - baz - -     - - - - -
- -
- - - - [#]
- [b]
- [e] - - 2001-01-02 test @@ -183,24 +168,24 @@ Detected redundant empty lines in gaps,   - - + - +
- +
- + - [#]
- [b]
- [e] + [#]
+ [b]
+ [e] + +   + 2001-01-03 test @@ -229,24 +214,24 @@ Detected redundant empty lines in gaps,   - - + - +
- +
- + - [#]
- [b]
- [e] + [#]
+ [b]
+ [e] + +   + 2001-01-03 test @@ -275,9 +260,6 @@ Detected redundant empty lines in gaps,   -