usawa

Signed, immutable accounting.
Log | Files | Refs | Submodules | LICENSE

commit 88df6833c0e674227227ac5fa92104eb5a992f15
parent 816414ef26259d09c04254235bb6199c6ecdac36
Author: lash <dev@holbrook.no>
Date:   Mon, 23 Mar 2026 21:12:49 -0600

Start balancer logic

Diffstat:
Adummy/tests/balance.py | 36++++++++++++++++++++++++++++++++++++
Mdummy/tests/entry.py | 13+++++++++++++
Adummy/usawa/balance.py | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdummy/usawa/entry.py | 12++++++++++++
Mdummy/usawa/error.py | 4++++
5 files changed, 147 insertions(+), 0 deletions(-)

diff --git a/dummy/tests/balance.py b/dummy/tests/balance.py @@ -0,0 +1,36 @@ +import logging +import unittest +import datetime +import os + +from usawa import EntryPart, Entry, UnitIndex +from usawa.balance import Balancer + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +testdir = os.path.realpath(os.path.dirname(__file__)) + +class TestBalancer(unittest.TestCase): + + def setUp(self): + self.parent = bytes.fromhex('0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6') + self.ref = '1bda7dfa-b8fd-400d-8b42-1d2861ad7f70' + self.uidx = UnitIndex('FOO') + self.uidx.add('BAR', 3) + self.description = "foo bar baz" + self.dtreg = datetime.datetime.now() + + + def test_balancer_basic(self): + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'baz', 1337, debit=True) + entry = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + entry.add_part(src) + entry.add_part(dst) + o = Balancer(self.uidx, entry, base='FOO') + self.assertTrue(o.balanced()) + + +if __name__ == '__main__': + unittest.main() diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py @@ -126,5 +126,18 @@ class TestEntry(unittest.TestCase): entry = Entry.from_tree(tree, self.uidx) + def test_entry_mux(self): + dst_one = EntryPart('FOO', 'asset', 'foo', 1337) + dst_two = EntryPart('FOO', 'asset', 'bar', 42) + src = EntryPart('FOO', 'income', 'baz', 1337+42, debit=True) + o = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + o.add_part(src, debit=True) + o.add_part(dst_one) + o.add_part(dst_two) + wallet = DemoWallet() + data = o.wrap(wallet=wallet) + print(list(o)) + + if __name__ == '__main__': unittest.main() diff --git a/dummy/usawa/balance.py b/dummy/usawa/balance.py @@ -0,0 +1,82 @@ +import logging + +logg = logging.getLogger('usawa.balancer') + + +class Balancer: + + def __init__(self, uidx, entry, base=None): + self.r = 0 + self.uidx = uidx + self.max_precision = 0 + self.ex = {} + self.base = base + self.scan(entry) + self.process(entry) + + + def scan(self, entry): + base = None + for part in entry: + precision = self.uidx.get(part.unit) + if precision > self.max_precision: + self.max_precision = precision + self.ex[part.unit] = 1000000000 + if base == None: + base = part.unit + continue + if base != part.unit: + if self.base == None: + logg.warning('base is not set with entry with different units') + logg.debug('have max precision {}'.format(self.max_precision)) + self.base = base + + + def set_rate(self, unit, rate): + if unit == self.base: + raise ValueError('base rate against itself') + self.ex[unit] = rate + + + def process(self, entry): + for part in entry: + precision = self.uidx.get(part.unit) + mod = 10 ** (self.max_precision - precision) + amount = part.amount * mod + ex = self.ex[part.unit] / 1000000000 + amount *= ex + fn = getattr(self, 'handle_' + part.typ) + fn(amount, part.isdebit) + logg.debug('after {} {} (ex {}) = {}'.format(part.unit, amount, ex, self.r)) + + + def balanced(self): + return self.r == 0 + + + def handle_income(self, amount, issrc=False): + if issrc: + self.r += amount + else: + self.r -= amount + + + def handle_expense(self, amount, issrc=False): + if issrc: + self.r -= amount + else: + self.r += amount + + + def handle_asset(self, amount, issrc=False): + if issrc: + self.r += amount + else: + self.r -= amount + + + def handle_liability(self, amount, issrc=False): + if issrc: + self.r -= amount + else: + self.r += amount diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -204,6 +204,7 @@ class Entry(UsawaElement): self.credit = [] self.lookup = None self.lookup_algo = None + self.parts = [] @staticmethod @@ -658,5 +659,16 @@ class Entry(UsawaElement): return b.encode('utf-8') + def __iter__(self): + self.parts = self.debit + self.credit + return self + + + def __next__(self): + if len(self.parts) == 0: + raise StopIteration() + return self.parts.pop(0) + + def __str__(self): return 'entry serial {} date {} parent {}'.format(self.serial, self.dt, self.parent.hex()) diff --git a/dummy/usawa/error.py b/dummy/usawa/error.py @@ -16,3 +16,7 @@ class SocketError(Exception): class AccountError(Exception): pass + + +class BalanceError(Exception): + pass