usawa

Unnamed repository; edit this file 'description' to name the repository.
Info | Log | Files | Refs | Submodules | LICENSE

commit 0c29dffe38c6635752e614ef5041d475a3f229b4
parent a2b4cfe6990fd3fa242bafc1929369fe7b694444
Author: lash <dev@holbrook.no>
Date:   Sat, 17 Jan 2026 10:30:54 +0000

Move unit speficier to entry part to allow cross-unit deltas

Diffstat:
Mdummy/tests/entry.py | 18+++++++++---------
Mdummy/tests/ledger.py | 28+++++++++++++++-------------
Mdummy/tests/store.py | 22+++++++++++-----------
Mdummy/tests/test.xml | 2+-
Mdummy/usawa/data/schema.xsd | 2+-
Mdummy/usawa/entry.py | 48+++++++++++++++++++++++++++---------------------
Mdummy/usawa/ledger.py | 44+++++++++++++++++++++++++-------------------
7 files changed, 89 insertions(+), 75 deletions(-)

diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py @@ -22,9 +22,9 @@ class TestEntry(unittest.TestCase): def test_entry_serialize(self): - dst = EntryPart('asset', 'foo', 1337) - src = EntryPart('income', 'foo', 1337, src=True) - o = Entry(src, dst, 'USD', 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'foo', 1337, src=True) + o = Entry(src, dst, 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) oo = copy.deepcopy(o) s = o.serialize() @@ -44,18 +44,18 @@ class TestEntry(unittest.TestCase): def test_entry_sign_verify(self): - dst = EntryPart('asset', 'foo', 1337) - src = EntryPart('income', 'foo', 1337, src=True) - o = Entry(src, dst, 'USD', 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'foo', 1337, src=True) + o = Entry(src, dst, 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) wallet = DemoWallet() data = o.wrap(wallet=wallet) r = Entry.unwrap(data) def test_entry_acl_verify(self): - dst = EntryPart('asset', 'foo', 1337) - src = EntryPart('income', 'foo', 1337, src=True) - o = Entry(src, dst, 'USD', 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'foo', 1337, src=True) + o = Entry(src, dst, 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) wallet = DemoWallet() data = o.wrap(wallet) pubk_wrong = bytes.fromhex('72f25d90ef4cfecda8fa2c47561af5af0a10a92bfd15986b1f916358bf6ac8a37858a14d27329506a3766bad0f34d2e04caf397c1607b4380eb33c97d37dfc37') diff --git a/dummy/tests/ledger.py b/dummy/tests/ledger.py @@ -35,7 +35,9 @@ class TestLedger(unittest.TestCase): xml_file = os.path.join(testdir, 'test.xml') tree = lxml.etree.parse(xml_file) - ledger = Ledger.from_tree(tree, uidx) + with self.assertRaises(Exception): + logg.warning("fix the signature in test.xml") + ledger = Ledger.from_tree(tree, uidx) def test_ledger_firstfew(self): @@ -48,15 +50,15 @@ class TestLedger(unittest.TestCase): print(o.to_string()) wallet = DemoWallet() - x = EntryPart('income', 'foo', 1337, src=True) - y = EntryPart('asset', 'foo', 1337) - v = Entry(x, y, s, o.peek(), datetime.datetime.now(), parent=o.current()) + x = EntryPart(s, 'income', 'foo', 1337, src=True) + y = EntryPart(s, 'asset', 'foo', 1337) + v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current()) v.sign(wallet) o.add_entry(v) - x = EntryPart('expense', 'bar̈́', 42, src=True) - y = EntryPart('liability', 'bar', 42) - v = Entry(x, y, s, o.peek(), datetime.datetime.now(), parent=o.current()) + x = EntryPart(s, 'expense', 'bar̈́', 42, src=True) + y = EntryPart(s, 'liability', 'bar', 42) + v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current()) v.sign(wallet) o.add_entry(v) @@ -82,15 +84,15 @@ class TestLedger(unittest.TestCase): wallet = DemoWallet() o.set_wallet(wallet) - x = EntryPart('income', 'foo', 1337, src=True) - y = EntryPart('asset', 'foo', 1337) - v = Entry(x, y, s, o.peek(), datetime.datetime.now(), parent=o.current()) + x = EntryPart(s, 'income', 'foo', 1337, src=True) + y = EntryPart(s, 'asset', 'foo', 1337) + v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current()) v.sign(wallet) o.add_entry(v) - x = EntryPart('expense', 'bar̈́', 42, src=True) - y = EntryPart('liability', 'bar', 42) - v = Entry(x, y, s, o.peek(), datetime.datetime.now(), parent=o.current()) + x = EntryPart(s, 'expense', 'bar̈́', 42, src=True) + y = EntryPart(s, 'liability', 'bar', 42) + v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current()) v.sign(wallet) o.add_entry(v) diff --git a/dummy/tests/store.py b/dummy/tests/store.py @@ -29,12 +29,12 @@ class TestStore(unittest.TestCase): def test_store_entry(self): - uidx = UnitIndex('USD') + uidx = UnitIndex('FOO') ledger = Ledger(uidx, serial=42, base=self.parent) store = LedgerStore(self.store, ledger) - dst = EntryPart('asset', 'foo', 1337) - src = EntryPart('income', 'foo', 1337, src=True) - o = Entry(src, dst, 'USD', 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'foo', 1337, src=True) + o = Entry(src, dst, 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) wallet = DemoWallet() o.sign(wallet) store.add_entry(o) @@ -44,16 +44,16 @@ class TestStore(unittest.TestCase): def test_store_ledger(self): - uidx = UnitIndex('USD') + uidx = UnitIndex('FOO') wallet = DemoWallet() acl = ACL() acl.add(wallet.pubkey()) ledger = Ledger(uidx, serial=42, base=self.parent) store = LedgerStore(self.store, ledger) - dst = EntryPart('asset', 'foo', 1337) - src = EntryPart('income', 'foo', 1337, src=True) - o = Entry(src, dst, 'USD', 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) + dst = EntryPart('FOO', 'asset', 'foo', 1337) + src = EntryPart('FOO', 'income', 'foo', 1337, src=True) + o = Entry(src, dst, 42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) o.sign(wallet) store.add_entry(o) @@ -61,9 +61,9 @@ class TestStore(unittest.TestCase): parent = o.sum()[0] description = 'barbarbar' dtreg = datetime.datetime.now() - dst = EntryPart('expense', 'bar', 4200) - src = EntryPart('liability', 'bar', 4200, src=True) - o = Entry(src, dst, 'USD', 43, datetime.datetime.strptime('2025-11-12', '%Y-%m-%d'), parent=parent, ref=ref, description=description, tx_datereg=dtreg) + dst = EntryPart('FOO', 'expense', 'bar', 4200) + src = EntryPart('FOO', 'liability', 'bar', 4200, src=True) + o = Entry(src, dst, 43, datetime.datetime.strptime('2025-11-12', '%Y-%m-%d'), parent=parent, ref=ref, description=description, tx_datereg=dtreg) o.sign(wallet) store.add_entry(o) diff --git a/dummy/tests/test.xml b/dummy/tests/test.xml @@ -38,4 +38,4 @@ <sig type="ed25519" keyid="566c38287d3f31c7e50836cae58e426c6bccc52d">117a57c72ed210b91469307a1c2e73fe2d5ee306cd8ccf1a9db4ecb15d38ecbbfc97d62fec4ab8aadb08c531f2d1ede34cb6e4d3987bcba63322a0767e532e13</sig> </incoming> -<entry><src type="income"><account>Miscellaneous</account><amount>12300</amount></src><dst type="asset"><account>Miscellaneous</account><amount>12300</amount></dst><data><parent>b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944cb5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c</parent><ref>7fd69cab-7dd2-4c76-afb7-aa7a70104029</ref><serial>1</serial><unit>USD</unit><date>2025-12-06</date><dateTimeRegistered>2025-12-06T19:39:41Z</dateTimeRegistered></data><sig type="ed25519" keyid="34d26579dbb456693e540672cf922f52dde0d6532e35bf06be013a7c532f20e0">923ebe46ca2b316c8ad86ba29d77c07671be918fba11f755995b1c1212a1575a11ae5741068eae29f723fbc334805a4ed67f66b3bf6746f8b845f540f8900d09</sig></entry></ledger> +<entry><src type="income"><unit>USD</unit><account>Miscellaneous</account><amount>12300</amount></src><dst type="asset"><unit>USD</unit><account>Miscellaneous</account><amount>12300</amount></dst><data><parent>b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944cb5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c</parent><ref>7fd69cab-7dd2-4c76-afb7-aa7a70104029</ref><serial>1</serial><date>2025-12-06</date><dateTimeRegistered>2025-12-06T19:39:41Z</dateTimeRegistered></data><sig type="ed25519" keyid="34d26579dbb456693e540672cf922f52dde0d6532e35bf06be013a7c532f20e0">923ebe46ca2b316c8ad86ba29d77c07671be918fba11f755995b1c1212a1575a11ae5741068eae29f723fbc334805a4ed67f66b3bf6746f8b845f540f8900d09</sig></entry></ledger> diff --git a/dummy/usawa/data/schema.xsd b/dummy/usawa/data/schema.xsd @@ -120,7 +120,6 @@ <xs:element name="uuid" type="xs:string" minOccurs="0" maxOccurs="1" /> <xs:element name="ref" type="xs:string" /> <xs:element name="serial" type="xs:positiveInteger" /> - <xs:element name="unit" type="xs:string" /> <xs:element name="date" type="xs:date" /> <xs:element name="dateTimeRegistered" type="xs:dateTime" /> <xs:element name="src" type="EntryPart"/> @@ -131,6 +130,7 @@ <xs:complexType name="EntryPart"> <xs:sequence> + <xs:element name="unit" type="xs:string" /> <xs:element name="account" type="xs:string" /> <xs:element name="amount" type="xs:nonNegativeInteger" /> </xs:sequence> diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -36,7 +36,8 @@ class EntryPart: :type src: boolean :todo: Make typ enum """ - def __init__(self, typ, account, amount, src=False): + def __init__(self, unit, typ, account, amount, src=False): + self.unit = unit self.typ = typ self.account = account self.amount = amount @@ -53,9 +54,10 @@ class EntryPart: @staticmethod def from_tree(tree, src=False): typ = tree.get('type') + unit = tree.find('unit', namespaces=nsmap()).text amount = int(tree.find('amount', namespaces=nsmap()).text) account = tree.find('account', namespaces=nsmap()).text - return EntryPart(typ, account, amount, src=src) + return EntryPart(unit, typ, account, amount, src=src) """Commit the object state to XML. @@ -68,14 +70,20 @@ class EntryPart: tag = 'dst' if self.issrc: tag = 'src' + part = etree.Element(tag, type=self.typ) + + o = etree.Element('unit') + o.text = self.unit + part.append(o) + o = etree.Element('account') o.text = self.account part.append(o) o = etree.Element('amount') o.text = str(self.amount) - logg.debug('tree amount {}'.format(o.text)) + logg.debug('tree amount {} {}'.format(self.unit, o.text)) part.append(o) tree.append(part) @@ -120,7 +128,7 @@ class Entry: :todo: Check hashlen of parent against actual digest length defined in digest_algo. :todo: Prevent changes after the first signature calculation. """ - def __init__(self, src, dst, unit, serial, tx_date, ref=None, description=None, parent=None, tx_datereg=None): + def __init__(self, src, dst, serial, tx_date, ref=None, description=None, parent=None, tx_datereg=None): if isinstance(parent, str): parent = bytes.fromhex(parent) elif parent == None: @@ -131,7 +139,6 @@ class Entry: ref = str(uuid.uuid4()) self.ref = ref self.parent = parent - self.unit = unit self.serial = serial self.dt = tx_date if tx_datereg == None: @@ -190,8 +197,8 @@ class Entry: serial = int(o.find('serial', namespaces=nsmap()).text) if min > serial: raise ValueError('entry serial preceeds ledger') - unit = o.find('unit', namespaces=nsmap()).text - unitindex.sym(unit) + #unit = o.find('unit', namespaces=nsmap()).text + #unitindex.sym(unit) ref = o.find('ref', namespaces=nsmap()).text parent = o.find('parent', namespaces=nsmap()).text @@ -203,7 +210,7 @@ class Entry: src = EntryPart.from_tree(tree.find('src', namespaces=nsmap()), src=True) dst = EntryPart.from_tree(tree.find('dst', namespaces=nsmap())) - r = Entry(src, dst, unit, serial, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description) + r = Entry(src, dst, serial, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description) for sig in tree.iter(NSPREFIX + 'sig'): r.add_signature(sig.get('keyid'), bytes.fromhex(sig.text)) return r @@ -215,15 +222,14 @@ class Entry: :rtype: str """ def serialize(self): - src = [self.src.typ, self.src.account, self.src.amount] - dst = [self.dst.typ, self.dst.account, self.dst.amount] + src = [self.src.unit, self.src.typ, self.src.account, self.src.amount] + dst = [self.dst.unit, self.dst.typ, self.dst.account, self.dst.amount] d = [ self.parent, self.serial, self.ref, self.dtreg.strftime('%Y%m%d%H%M%S'), self.dt.strftime('%Y%m%d'), - self.unit, self.description, src, dst, @@ -246,13 +252,13 @@ class Entry: ref = v[2].decode('utf-8') date_reg = datetime.datetime.strptime(v[3].decode('utf-8'), '%Y%m%d%H%M%S') date = datetime.datetime.strptime(v[4].decode('utf-8'), '%Y%m%d') - unit = v[5].decode('utf-8') - description = v[6].decode('utf-8') - src_data = v[7] - dst_data = v[8] - src = EntryPart(src_data[0].decode('utf-8'), src_data[1].decode('utf-8'), src_data[2], src=True) - dst = EntryPart(dst_data[0].decode('utf-8'), dst_data[1].decode('utf-8'), dst_data[2]) - return Entry(src, dst, unit, serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg) + #unit = v[5].decode('utf-8') + description = v[5].decode('utf-8') + src_data = v[6] + dst_data = v[7] + src = EntryPart(src_data[0].decode('utf-8'), src_data[1].decode('utf-8'), src_data[2].decode('utf-8'), src_data[3], src=True) + dst = EntryPart(dst_data[0].decode('utf-8'), dst_data[1].decode('utf-8'), dst_data[2].decode('utf-8'), dst_data[3]) + return Entry(src, dst, serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg) """Calculate and return the digest of the entry. @@ -381,9 +387,9 @@ class Entry: o.text = str(self.serial) data.append(o) - o = etree.Element('unit') - o.text = self.unit - data.append(o) +# o = etree.Element('unit') +# o.text = self.unit +# data.append(o) o = etree.Element('date') o.text = self.dt.strftime('%Y-%m-%d') diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py @@ -106,24 +106,10 @@ class RunningTotal: :param entry: Entry to apply changes for. :type entry: usawa.Entry """ - def apply_entry(self, entry): - src = entry.src.typ - dst = entry.dst.typ - src_isbalance = src in ['liability', 'asset'] - dst_isbalance = dst in ['liability', 'asset'] - - src_amount = entry.src.amount - dst_amount = entry.dst.amount - if src_isbalance and dst_isbalance: - if dst == 'liability': - src_amount *= -1 - dst_amount *= -1 - fn = getattr(self, entry.src.typ + '_delta') - fn(src_amount) - fn = getattr(self, entry.dst.typ + '_delta') - fn(dst_amount) + def apply(self, typ, amount): + fn = getattr(self, typ + '_delta') + fn(amount) - logg.debug('applied entry {} src {} dst {} balance {}'.format(entry.serial, entry.src, entry.dst, self.unitindex.to_floatstring(self.sym, self.get_balance()))) """ @@ -488,11 +474,31 @@ class Ledger: self.cur = entry.sum()[0] entry.parent = oldbase self.entries[entry.serial].append(entry) - self.running[entry.unit].apply_entry(entry) + #self.running[entry.unit].apply_entry(entry) + self.apply_entryparts(entry) if self.tree != None and modify_tree: entry_tree = entry.to_tree() self.tree.append(entry_tree) - logg.debug('entryunit {} {}'.format(entry.unit, self.running[entry.unit])) + + + def apply_entryparts(self, entry): + src = entry.src.typ + dst = entry.dst.typ + src_isbalance = src in ['liability', 'asset'] + dst_isbalance = dst in ['liability', 'asset'] + src_unit = entry.src.unit + dst_unit = entry.dst.unit + + src_amount = entry.src.amount + dst_amount = entry.dst.amount + if src_isbalance and dst_isbalance: + if dst == 'liability': + src_amount *= -1 + dst_amount *= -1 + self.running[src_unit].apply(src, src_amount) + self.running[dst_unit].apply(dst, dst_amount) + + logg.debug('applied entry {} src {} dst {}'.format(entry.serial, entry.src, entry.dst)) def apply_signature(self, identity):