commit 4fd0bf5db3a41814259960ba41af22bdae0e09f4
parent 0c29dffe38c6635752e614ef5041d475a3f229b4
Author: lash <dev@holbrook.no>
Date: Sat, 17 Jan 2026 11:26:26 +0000
Allow multiple debit and credit
Diffstat:
6 files changed, 130 insertions(+), 64 deletions(-)
diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py
@@ -23,8 +23,10 @@ class TestEntry(unittest.TestCase):
def test_entry_serialize(self):
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)
+ src = EntryPart('FOO', 'income', 'foo', 1337, 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)
oo = copy.deepcopy(o)
s = o.serialize()
@@ -45,8 +47,10 @@ class TestEntry(unittest.TestCase):
def test_entry_sign_verify(self):
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)
+ src = EntryPart('FOO', 'income', 'foo', 1337, 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)
wallet = DemoWallet()
data = o.wrap(wallet=wallet)
r = Entry.unwrap(data)
@@ -54,8 +58,10 @@ class TestEntry(unittest.TestCase):
def test_entry_acl_verify(self):
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)
+ src = EntryPart('FOO', 'income', 'foo', 1337, 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)
wallet = DemoWallet()
data = o.wrap(wallet)
pubk_wrong = bytes.fromhex('72f25d90ef4cfecda8fa2c47561af5af0a10a92bfd15986b1f916358bf6ac8a37858a14d27329506a3766bad0f34d2e04caf397c1607b4380eb33c97d37dfc37')
diff --git a/dummy/tests/ledger.py b/dummy/tests/ledger.py
@@ -50,15 +50,19 @@ class TestLedger(unittest.TestCase):
print(o.to_string())
wallet = DemoWallet()
- x = EntryPart(s, 'income', 'foo', 1337, src=True)
+ x = EntryPart(s, 'income', 'foo', 1337, debit=True)
y = EntryPart(s, 'asset', 'foo', 1337)
- v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current())
+ v = Entry(o.peek(), datetime.datetime.now(), parent=o.current())
+ v.add_part(x, debit=True)
+ v.add_part(y)
v.sign(wallet)
o.add_entry(v)
- x = EntryPart(s, 'expense', 'bar̈́', 42, src=True)
+ x = EntryPart(s, 'expense', 'bar̈́', 42, debit=True)
y = EntryPart(s, 'liability', 'bar', 42)
- v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current())
+ v = Entry(o.peek(), datetime.datetime.now(), parent=o.current())
+ v.add_part(x, debit=True)
+ v.add_part(y)
v.sign(wallet)
o.add_entry(v)
@@ -84,15 +88,19 @@ class TestLedger(unittest.TestCase):
wallet = DemoWallet()
o.set_wallet(wallet)
- x = EntryPart(s, 'income', 'foo', 1337, src=True)
+ x = EntryPart(s, 'income', 'foo', 1337, debit=True)
y = EntryPart(s, 'asset', 'foo', 1337)
- v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current())
+ v = Entry(o.peek(), datetime.datetime.now(), parent=o.current())
+ v.add_part(x, debit=True)
+ v.add_part(y)
v.sign(wallet)
o.add_entry(v)
- x = EntryPart(s, 'expense', 'bar̈́', 42, src=True)
+ x = EntryPart(s, 'expense', 'bar̈́', 42, debit=True)
y = EntryPart(s, 'liability', 'bar', 42)
- v = Entry(x, y, o.peek(), datetime.datetime.now(), parent=o.current())
+ v = Entry(o.peek(), datetime.datetime.now(), parent=o.current())
+ v.add_part(x, debit=True)
+ v.add_part(y)
v.sign(wallet)
o.add_entry(v)
diff --git a/dummy/tests/store.py b/dummy/tests/store.py
@@ -33,8 +33,10 @@ class TestStore(unittest.TestCase):
ledger = Ledger(uidx, serial=42, base=self.parent)
store = LedgerStore(self.store, ledger)
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)
+ src = EntryPart('FOO', 'income', 'foo', 1337, 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, unitindex=uidx)
+ o.add_part(src, debit=True)
+ o.add_part(dst)
wallet = DemoWallet()
o.sign(wallet)
store.add_entry(o)
@@ -52,8 +54,10 @@ class TestStore(unittest.TestCase):
store = LedgerStore(self.store, ledger)
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)
+ src = EntryPart('FOO', 'income', 'foo', 1337, 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, unitindex=uidx)
+ o.add_part(src, debit=True)
+ o.add_part(dst)
o.sign(wallet)
store.add_entry(o)
@@ -62,8 +66,10 @@ class TestStore(unittest.TestCase):
description = 'barbarbar'
dtreg = datetime.datetime.now()
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)
+ src = EntryPart('FOO', 'liability', 'bar', 4200, debit=True)
+ o = Entry(43, datetime.datetime.strptime('2025-11-12', '%Y-%m-%d'), parent=parent, ref=ref, description=description, tx_datereg=dtreg, unitindex=uidx)
+ o.add_part(src, debit=True)
+ o.add_part(dst)
o.sign(wallet)
store.add_entry(o)
diff --git a/dummy/usawa/data/schema.xsd b/dummy/usawa/data/schema.xsd
@@ -122,8 +122,8 @@
<xs:element name="serial" type="xs:positiveInteger" />
<xs:element name="date" type="xs:date" />
<xs:element name="dateTimeRegistered" type="xs:dateTime" />
- <xs:element name="src" type="EntryPart"/>
- <xs:element name="dst" type="EntryPart"/>
+ <xs:element name="debit" type="EntryPart" minOccurs="1" maxOccurs="unbounded" />
+ <xs:element name="credit" type="EntryPart" minOccurs="1" maxOccurs="unbounded" />
<xs:element name="attachment" type="Attachment" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py
@@ -24,8 +24,10 @@ class KeyStoreFormat(enum.IntEnum):
class EntryPart:
- """EntryPart is one of the two parts of a transaction, representing either side of a double-accounting ledger.
-
+ """EntryPart is a single part of transaction, representing either side of a double-accounting ledger.
+
+ Each side of the transaction may have several parts.
+
:param typ: One of 'asset', 'liability', 'income', 'expense'
:type typ: str
:param account: A path-like account name.
@@ -36,12 +38,12 @@ class EntryPart:
:type src: boolean
:todo: Make typ enum
"""
- def __init__(self, unit, typ, account, amount, src=False):
+ def __init__(self, unit, typ, account, amount, debit=False):
self.unit = unit
self.typ = typ
self.account = account
self.amount = amount
- self.issrc = src
+ self.isdebit = debit
"""Create object from an entry part defined as an XML tree.
@@ -52,12 +54,12 @@ class EntryPart:
:type src: boolean
"""
@staticmethod
- def from_tree(tree, src=False):
+ def from_tree(tree, debit=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(unit, typ, account, amount, src=src)
+ return EntryPart(unit, typ, account, amount, debit=debit)
"""Commit the object state to XML.
@@ -67,9 +69,9 @@ class EntryPart:
:todo: Not an API function?
"""
def apply_tree(self, tree):
- tag = 'dst'
- if self.issrc:
- tag = 'src'
+ tag = 'credit'
+ if self.isdebit:
+ tag = 'debit'
part = etree.Element(tag, type=self.typ)
@@ -91,9 +93,9 @@ class EntryPart:
def __str__(self):
- pfx = 'dst'
- if self.issrc:
- pfx = 'src'
+ pfx = 'credit'
+ if self.isdebit:
+ pfx = 'debit'
return '[{}] {}:{} {}'.format(pfx, self.typ, self.account, self.amount)
@@ -128,7 +130,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, serial, tx_date, ref=None, description=None, parent=None, tx_datereg=None):
+ def __init__(self, serial, tx_date, ref=None, description=None, parent=None, tx_datereg=None, unitindex=None):
if isinstance(parent, str):
parent = bytes.fromhex(parent)
elif parent == None:
@@ -141,14 +143,24 @@ class Entry:
self.parent = parent
self.serial = serial
self.dt = tx_date
+ self.uidx = unitindex
if tx_datereg == None:
tx_datereg = datetime.datetime.now()
self.dtreg = tx_datereg
self.attachment = []
self.sigs = {}
self.description = description
- self.src = src
- self.dst = dst
+ self.debit = []
+ self.credit = []
+
+
+ def add_part(self, part, debit=False):
+ if self.uidx != None:
+ self.uidx.sym(part.unit)
+ if debit:
+ self.debit.append(part)
+ else:
+ self.credit.append(part)
"""Append a single media asset to the attachment list for the entry.
@@ -207,12 +219,17 @@ class Entry:
description = description.text
dt = datetime.date.fromisoformat(o.find('date', namespaces=nsmap()).text)
dtreg = datetime.datetime.strptime(o.find('dateTimeRegistered', namespaces=nsmap()).text, '%Y-%m-%dT%H:%M:%SZ')
- src = EntryPart.from_tree(tree.find('src', namespaces=nsmap()), src=True)
+
+ o = Entry(serial, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description, unitindex=unitindex)
+
+ src = EntryPart.from_tree(tree.find('src', namespaces=nsmap()), debit=True)
dst = EntryPart.from_tree(tree.find('dst', namespaces=nsmap()))
+ o.add_part(src, debit=True)
+ o.add_part(dst)
- 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
@@ -222,8 +239,15 @@ class Entry:
:rtype: str
"""
def serialize(self):
- 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]
+
+ debit = []
+ credit = []
+ for v in self.debit:
+ debit.append((v.unit, v.typ, v.account, v.amount,))
+
+ for v in self.credit:
+ credit.append((v.unit, v.typ, v.account, v.amount,))
+
d = [
self.parent,
self.serial,
@@ -231,8 +255,8 @@ class Entry:
self.dtreg.strftime('%Y%m%d%H%M%S'),
self.dt.strftime('%Y%m%d'),
self.description,
- src,
- dst,
+ debit,
+ credit,
]
logg.debug('serialize entry {}'.format(d))
return rencode.dumps(d)
@@ -256,10 +280,17 @@ class Entry:
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)
+ o = Entry(serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg)
+ for v in src_data:
+ src = EntryPart(v[0].decode('utf-8'), v[1].decode('utf-8'), v[2].decode('utf-8'), v[3], debit=True)
+ o.add_part(src, debit=True)
+
+ for v in dst_data:
+ dst = EntryPart(v[0].decode('utf-8'), v[1].decode('utf-8'), v[2].decode('utf-8'), v[3], debit=True)
+ o.add_part(dst)
+ return o
+
"""Calculate and return the digest of the entry.
@@ -326,6 +357,7 @@ class Entry:
]
return rencode.dumps(d)
+
"""Create an entry object from serialized data containing valid public keys for signing, generated by the wrap() method.
If ACL is defined, the public keys defined therein will override the public keys ocontained in the wrapped, serialized data.
@@ -404,8 +436,11 @@ class Entry:
o.text = self.description
data.append(o)
- self.src.apply_tree(data)
- self.dst.apply_tree(data)
+ for v in self.debit:
+ v.apply_tree(data)
+
+ for v in self.credit:
+ v.apply_tree(data)
tree.append(data)
diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py
@@ -482,23 +482,34 @@ class Ledger:
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
+ for v in entry.debit:
+# 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
+ amount = v.amount
+ if v.isdebit:
+ amount *= -1
+ self.running[v.unit].apply(v.typ, amount)
+
+ for v in entry.credit:
+ amount = v.amount
+ if v.isdebit:
+ amount *= -1
+ self.running[v.unit].apply(v.typ, amount)
- 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))
+# 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.debit, entry.credit))
def apply_signature(self, identity):