From: Christian Heller Date: Tue, 21 Jan 2025 03:26:45 +0000 (+0100) Subject: Validate bookings balances. X-Git-Url: https://plomlompom.com/repos/%7B%7B%20web_path%20%7D%7D/decks/%7B%7B%20deck_id%20%7D%7D/%7B%7Bdb.prefix%7D%7D/balance?a=commitdiff_plain;p=plomledger Validate bookings balances. --- diff --git a/ledger.py b/ledger.py index aa0619b..f82cc41 100755 --- a/ledger.py +++ b/ledger.py @@ -5,7 +5,7 @@ from decimal import Decimal, InvalidOperation as DecimalInvalidOperation from os import environ from pathlib import Path from sys import exit as sys_exit -from typing import Optional +from typing import Optional, Self from plomlib.web import PlomHttpHandler, PlomHttpServer, PlomQueryMap @@ -15,6 +15,39 @@ SERVER_HOST = '127.0.0.1' PATH_TEMPLATES = Path('templates') +class Wealth: + """Collects amounts mapped to currencies.""" + + def __init__(self, moneys: Optional[dict[str, Decimal]] = None) -> None: + self.moneys = moneys if moneys else {} + + def _inc_by(self, other: Self, add=True) -> Self: + for currency, amount in other.moneys.items(): + if currency not in self.moneys: + self.moneys[currency] = Decimal(0) + self.moneys[currency] += amount if add else -amount + return self + + def __iadd__(self, other: Self) -> Self: + return self._inc_by(other, True) + + def __isub__(self, other: Self) -> Self: + return self._inc_by(other, False) + + @property + def sink_empty(self) -> bool: + """Return if all evens out to zero.""" + return not bool(self.as_sink.moneys) + + @property + def as_sink(self) -> 'Wealth': + """Drop zero amounts, invert non-zero ones.""" + sink = Wealth() + for moneys in [Wealth({c: a}) for c, a in self.moneys.items() if a]: + sink = moneys + return sink + + class DatLine: """Line of .dat file parsed into comments and machine-readable data.""" @@ -83,23 +116,22 @@ class TransferLine(BookingLine): def __init__(self, booking_id: int, code: str) -> None: super().__init__(booking_id) - self.account, self.currency = '', '' - self.amount: Optional[Decimal] = None if not code[0].isspace(): - self.error = 'non-intro line not indented' + self.error = 'transfer line not indented' return toks = code.lstrip().split() self.account = toks[0] - if len(toks) not in {1, 3}: - self.error = 'illegal number of tokens' - return - if 3 == len(toks): + self.amount: Optional[Decimal] = None + if 1 == len(toks): + self.currency = '' + elif 3 == len(toks): self.currency = toks[2] try: self.amount = Decimal(toks[1]) except DecimalInvalidOperation: self.error = 'improper amount value' - return + else: + self.error = 'illegal number of tokens' @property def amount_short(self) -> str: @@ -122,6 +154,30 @@ class Booking: self.dat_lines[0].code) for dat_line in self.dat_lines[1:]: dat_line.booking_line = TransferLine(self.id_, dat_line.code) + self.account_changes: dict[str, Wealth] = {} + self.sink_account = None + for dat_line in [dl for dl in self.dat_lines if dl.error]: + return + changes = Wealth() + for transfer_line in [dl.booking_line for dl in self.dat_lines[1:]]: + assert isinstance(transfer_line, TransferLine) + if transfer_line.account not in self.account_changes: + self.account_changes[transfer_line.account] = Wealth() + if transfer_line.amount is None: + if self.sink_account: + transfer_line.error = 'second sink' + return + self.sink_account = transfer_line.account + continue + change = Wealth({transfer_line.currency: transfer_line.amount}) + self.account_changes[transfer_line.account] += change + changes += change + if self.sink_account: + self.account_changes[self.sink_account] += changes.as_sink + elif not changes.sink_empty: + last_line = self.dat_lines[-1] + assert isinstance(last_line.booking_line, BookingLine) + last_line.booking_line.error = 'needed sink missing' class Handler(PlomHttpHandler):