usawa

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

commit 41db8d1466c7df7dc80e2133c4dbdf3980993051
parent 1af8e7036cc73edb4c4050ea2ca20010a26c9b88
Author: lash <dev@holbrook.no>
Date:   Wed, 25 Mar 2026 19:26:20 -0600

Enable multiple parts in entry cli

Diffstat:
Mdummy/tests/entry.py | 16++++++++--------
Mdummy/tests/ledger.py | 16++++++++--------
Mdummy/tests/store.py | 14+++++++-------
Mdummy/usawa/account.py | 4+++-
Mdummy/usawa/entry.py | 12++++--------
Mdummy/usawa/runnable/entry.py | 122++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
6 files changed, 110 insertions(+), 74 deletions(-)

diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py @@ -26,7 +26,7 @@ class TestEntry(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) s = o.serialize() @@ -49,7 +49,7 @@ class TestEntry(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) wallet = DemoWallet() data = o.wrap(wallet=wallet) @@ -60,7 +60,7 @@ class TestEntry(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) wallet = DemoWallet() (digest, sig, msg) = o.sign(wallet) @@ -77,7 +77,7 @@ class TestEntry(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) wallet = DemoWallet() data = o.wrap(wallet) @@ -96,7 +96,7 @@ class TestEntry(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) wallet = DemoWallet() o.sign(wallet) @@ -111,7 +111,7 @@ class TestEntry(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) fp = os.path.join(testdir, 'test.xml') @@ -131,7 +131,7 @@ class TestEntry(unittest.TestCase): 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(src) o.add_part(dst_one) o.add_part(dst_two) wallet = DemoWallet() @@ -143,7 +143,7 @@ class TestEntry(unittest.TestCase): 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, unitindex=self.uidx) - o.add_part(src, debit=True) + o.add_part(src) o.add_part(dst_one) o.add_part(dst_two) diff --git a/dummy/tests/ledger.py b/dummy/tests/ledger.py @@ -64,7 +64,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'income', 'foo', 1337, debit=True) y = EntryPart(s, 'asset', 'foo', 1337) v = Entry(o.peek(), datetime.datetime.now(), parent=o.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) o.add_entry(v) @@ -72,7 +72,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'expense', 'bar̈́', 42, debit=True) y = EntryPart(s, 'liability', 'bar', 42) v = Entry(o.peek(), datetime.datetime.now(), parent=o.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) o.add_entry(v) @@ -102,7 +102,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'income', 'foo', 1337, debit=True) y = EntryPart(s, 'asset', 'foo', 1337) v = Entry(o.peek(), datetime.datetime.now(), parent=o.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) o.add_entry(v) @@ -110,7 +110,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'expense', 'bar̈́', 42, debit=True) y = EntryPart(s, 'liability', 'bar', 42) v = Entry(o.peek(), datetime.datetime.now(), parent=o.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) o.add_entry(v) @@ -142,7 +142,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'income', 'foo', 1337, debit=True) y = EntryPart(s, 'asset', 'foo', 1337) v = Entry(ledger.peek(), datetime.datetime.now(), parent=ledger.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) ledger.add_entry(v) @@ -150,7 +150,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'expense', 'bar̈́', 42, debit=True) y = EntryPart(s, 'liability', 'bar', 42) v = Entry(ledger.peek(), datetime.datetime.now(), parent=ledger.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) ledger.add_entry(v) @@ -175,7 +175,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'income', 'foo', 1337, debit=True) y = EntryPart(s, 'asset', 'foo', 1337) v = Entry(ledger.peek(), datetime.datetime.now(), parent=ledger.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) ledger.add_entry(v) @@ -184,7 +184,7 @@ class TestLedger(unittest.TestCase): x = EntryPart(s, 'income', 'foo', 1337, debit=True) y = EntryPart(s, 'asset', 'foo', 1337) v = Entry(ledger.peek(), datetime.datetime.now(), parent=ledger.current()) - v.add_part(x, debit=True) + v.add_part(x) v.add_part(y) v.sign(wallet) with self.assertRaises(VerifyError): diff --git a/dummy/tests/store.py b/dummy/tests/store.py @@ -38,7 +38,7 @@ class TestStore(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) wallet = DemoWallet() o.sign(wallet) @@ -57,7 +57,7 @@ class TestStore(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) fp = os.path.join(testdir, 'test.xml') @@ -86,7 +86,7 @@ class TestStore(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) src = EntryPart('FOO', 'income', 'foo', 1337, debit=True) o = Entry(ledger.next_serial(), 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(src) o.add_part(dst) o.sign(wallet) store.add_entry(o) @@ -98,7 +98,7 @@ class TestStore(unittest.TestCase): dst = EntryPart('FOO', 'expense', 'bar', 4200) src = EntryPart('FOO', 'liability', 'bar', 4200, debit=True) o = Entry(ledger.next_serial(), 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(src) o.add_part(dst) o.sign(wallet) store.add_entry(o) @@ -135,7 +135,7 @@ class TestStore(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) src = EntryPart('FOO', 'income', 'foo', 1337, debit=True) o = Entry(ledger.next_serial(), 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(src) o.add_part(dst) o.sign(wallet) store.add_entry(o) @@ -147,7 +147,7 @@ class TestStore(unittest.TestCase): dst = EntryPart('FOO', 'expense', 'bar', 4200) src = EntryPart('FOO', 'liability', 'bar', 4200, debit=True) o = Entry(ledger.next_serial(), 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(src) o.add_part(dst) o.sign(wallet) store.add_entry(o) @@ -169,7 +169,7 @@ class TestStore(unittest.TestCase): dst = EntryPart('FOO', 'asset', 'foo', 1337) 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(src) o.add_part(dst) o.add_tag('foo') o.add_pair('bar', 'baz') diff --git a/dummy/usawa/account.py b/dummy/usawa/account.py @@ -26,7 +26,9 @@ def from_account_path(p, sym=None, typ=None): if typ == None: o = check_path_parts(o[1]) typ = o[0] - path = o[1] + path = o[1] + else: + path = o[1].split('/') return (sym, typ, path,) diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -226,10 +226,10 @@ class Entry(UsawaElement): :type debit: boolean :raises KeyError: Symbol does not exist in unit index. """ - def add_part(self, part, debit=False): + def add_part(self, part): if self.uidx != None: self.uidx.sym(part.unit) - if debit: + if part.isdebit: self.debit.append(part) else: self.credit.append(part) @@ -297,7 +297,7 @@ class Entry(UsawaElement): src = EntryPart.from_tree(src_tree, debit=True) dst = EntryPart.from_tree(dst_tree) entry.add_part(dst) - entry.add_part(src, debit=True) + entry.add_part(src) for v in o.findall('attachment', namespaces=nsmap()): asset = Asset.from_tree(v) @@ -375,13 +375,11 @@ class Entry(UsawaElement): @staticmethod def deserialize(data): v = rencode.loads(data) - #parent = v[0].hex() parent = v[1] serial = v[2] ref = v[3].decode('utf-8') date_reg = datetime.datetime.strptime(v[4].decode('utf-8'), '%Y%m%d%H%M%S') date = datetime.datetime.strptime(v[5].decode('utf-8'), '%Y%m%d') - #unit = v[5].decode('utf-8') description = v[6].decode('utf-8') dst_data = v[7] src_data = v[8] @@ -389,12 +387,10 @@ class Entry(UsawaElement): o = Entry(serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg) super(Entry, o).deserialize(v[0]) 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) src = EntryPart.deserialize(v, debit=True) - o.add_part(src, debit=True) + o.add_part(src) for v in dst_data: - #dst = EntryPart(v[0].decode('utf-8'), v[1].decode('utf-8'), v[2].decode('utf-8'), v[3]) dst = EntryPart.deserialize(v) o.add_part(dst) diff --git a/dummy/usawa/runnable/entry.py b/dummy/usawa/runnable/entry.py @@ -2,6 +2,8 @@ import argparse import logging import uuid import datetime +import signal +import sys from whee.valkey import ValkeyStore @@ -24,10 +26,11 @@ class Context: # entry parts self.description = None - self.src = [None, None, None] - self.dst = [None, None, None] + #self.src = [None, None, None] + #self.dst = [None, None, None] + self.src = [] + self.dst = [] self.amount = None - self.part = [] self.output = None self.f = None if args.d: @@ -35,7 +38,7 @@ class Context: else: self.txdate = datetime.datetime.utcnow() self.attach = [] - + # set up ledger s = args.l if not s: @@ -47,8 +50,8 @@ class Context: raise ValueError('ledger file required') self.ledger = Ledger.from_file(s) self.uidx = self.ledger.uidx - self.src[2] = self.uidx.base - self.dst[2] = self.uidx.base + #self.src[2] = self.uidx.base + #self.dst[2] = self.uidx.base # set up accounts hierarchy, if applicable self.accounts = None @@ -75,6 +78,10 @@ class Context: self.store.put_draft(self.entry) self.ref = self.entry.get_ref() + self.base = self.uidx.base + self.k = 'src' + self.havedst = False + self.i = 0 def parse_type(self, v): @@ -109,18 +116,27 @@ class Context: return uidx.from_floatstring(sym, v) + def parse_side(self, v): + for k in ['src', 'dst']: + if k.startswith(v): + return k + raise ValueError('invalid side: ' + v) + + def add_part(self, part): logg.info('add part {}'.format(part)) self.part.append(part) def validate(self): - for v in self.src: - if v == None: - raise ValueError('invalid src') - for v in self.dst: - if v == None: - raise ValueError('invalid dst') + #for k in self.src: + # if v == None: + if len(self.src) == 0: + raise ValueError('no src') + #for v in self.dst: + # if v == None: + if len(self.dst) == 0: + raise ValueError('no dst') if self.ref == None: raise ValueError('invalid ref') @@ -142,6 +158,11 @@ if args.v: ctx = Context(args) +def croak(*args, **kwargs): + sys.exit(1) + +signal.signal(signal.SIGINT, croak) +signal.signal(signal.SIGTERM, croak) def input_or_default(prompt, default=None, postfix=': ', validate_fn=None): if default != None: @@ -156,54 +177,66 @@ def input_or_default(prompt, default=None, postfix=': ', validate_fn=None): return v -def do_interactive(ctx): - v = input_or_default('Entry description', ctx.description) - ctx.description = v +def do_interactive_one(ctx): + ctx.description = input_or_default('Entry description', ctx.description) + ctx.ref = input_or_default('External ref', ctx.ref) - amounts = { - 'src': None, - 'dst': None, - } - for k in ['src', 'dst']: - o = vars(ctx) - #v = input('Entry {} type: '.format(k)) +def do_interactive_two(ctx, entry): - v = input_or_default('Entry {} unit'.format(k), o[k][2]) + r = True + while r: + try: + r = enter_part(ctx, entry) + except Exception as e: + logg.error('err {}'.format(e)) + pass + + +def enter_part(ctx, entry): + if ctx.i > 0: + if ctx.havedst: + ctx.k = '' + else: + ctx.k = 'dst' + v = input_or_default('Entry side', ctx.k) + if v == '': + return False + ctx.k = ctx.parse_side(v) + if v == 'dst': + ctx.havedst = True + + v = input_or_default('Entry {} unit'.format(ctx.k), ctx.base) unit = ctx.parse_unit(v) - o[k][2] = unit + if ctx.base != unit: + ctx.base = unit - v = input_or_default('Entry {} type'.format(k), o[k][0]) + v = input_or_default('Entry {} type'.format(ctx.k)) typ = ctx.parse_type(v) - o[k][0] = typ - v = input_or_default('Entry {} account'.format(k), o[k][1]) + v = input_or_default('Entry {} account'.format(ctx.k)) account = ctx.parse_account(v, sym=unit, typ=typ) - o[k][1] = account amount = None - if k =='dst': - amount = amounts['src'] - v = input_or_default('Entry {} amount'.format(k), amount) + v = input_or_default('Entry {} amount'.format(ctx.k), amount) amount = ctx.parse_amount(ctx.uidx, unit, v) - amount *= -1 - amounts[k] = str(amount) - part = EntryPart(unit, typ.value, account.to_path(display=AccountDisplay.path), amount, debit=k=='src') - ctx.add_part(part) + isdebit = ctx.k=='src' + part = EntryPart(unit, typ.value, account.to_path(display=AccountDisplay.path), amount, debit=isdebit) + entry.add_part(part) - ctx.ref = input_or_default('External ref', ctx.ref) + ctx.i += 1 + + return True entry = None if ctx.state == 0: - do_interactive(ctx) - ctx.validate() - entry = Entry.empty(ref=ctx.ref) + do_interactive_one(ctx) + entry = Entry.empty(ref=ctx.ref, unitindex=ctx.uidx) entry = ctx.store.get_draft(entry) - entry.description = description - entry.add_part(ctx.part[0], debit=True) - entry.add_part(ctx.part[1]) + entry.description = ctx.description + do_interactive_two(ctx, entry) else: entry = ctx.entry @@ -225,5 +258,10 @@ if ctx.commit: entry.serial = ctx.ledger.next_serial() +#v = input_or_default('Commit? (type YES, any other input is no)', '') +#if v == 'YES': + #ctx.store.put_entry(entry) +# pass +#else: ctx.store.put_draft(entry) print(entry)