usawa

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

commit d72a9c8fbb56ab51b6739f37750b68ff57f6fe9f
parent c20eceaa8616fabcc5c8342f4d25192a51f76ec2
Author: lash <dev@holbrook.no>
Date:   Sun, 22 Mar 2026 19:36:25 -0600

Integrate account listing in craft tool

Diffstat:
Mdummy/usawa/account.py | 23+++++++++++++++++++----
Mdummy/usawa/data/usawa.ini | 5++++-
Mdummy/usawa/entry.py | 4++--
Mdummy/usawa/runnable/craft.py | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
4 files changed, 169 insertions(+), 13 deletions(-)

diff --git a/dummy/usawa/account.py b/dummy/usawa/account.py @@ -64,12 +64,19 @@ class Account: return Account(o[0], o[1], o[2]) - def to_path(self): + def to_path(self, display=AccountDisplay.full): path = '/'.join(self.segments) - path = '{}.{}/{}'.format(self.sym, self.typ.value.lower(), path) + if display == AccountDisplay.full: + path = '{}.{}/{}'.format(self.sym, self.typ.value.lower(), path) + elif display == AccountDisplay.typ: + path = '{}/{}'.format(self.typ.value.lower(), path) return path + def __str__(self): + return self.to_path() + + class AccountIndex: def __init__(self, unitindex): #, pathvalidator=default_check): @@ -118,6 +125,7 @@ class AccountIndex: path = account.to_path() logg.info('add account {}'.format(path)) self.accounts[sym].append(path) + return account def lock(self): @@ -126,10 +134,17 @@ class AccountIndex: def check(self, sym, typ, path): s = '{}.{}/{}'.format(sym, typ.value.lower(), path) + r = False try: - return s in self.accounts[sym] + r = s in self.accounts[sym] except KeyError: - return False + return None + return s + + + def check_path(self, path): + o = Account.path_parser(path) + return self.check(o[0], o[1], o[2]) def set_filter(self, sym=None, typ=None, display=AccountDisplay.full): diff --git a/dummy/usawa/data/usawa.ini b/dummy/usawa/data/usawa.ini @@ -1,8 +1,11 @@ [main] gpg_dir = -accounts_file = legder_file = +[accounts] +file = +strict = 0 + [valkey] id = 2434 host = localhost diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -207,9 +207,9 @@ class Entry(UsawaElement): @staticmethod - def empty(): + def empty(*args, **kwargs): dt = datetime.datetime.utcnow() - return Entry(-1, dt) + return Entry(-1, dt, *args, **kwargs) """Add an entry part to the entry. diff --git a/dummy/usawa/runnable/craft.py b/dummy/usawa/runnable/craft.py @@ -4,9 +4,10 @@ import logging from whee.valkey import ValkeyStore import usawa.config -from usawa import Entry, Ledger +from usawa import Entry, Ledger, EntryPart from usawa.store import EntryStore -from usawa.account import AccountIndex +from usawa.account import Account, AccountIndex, AccountType, AccountDisplay +from usawa.constant import CATEGORIES logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() @@ -17,7 +18,18 @@ class Context: def __init__(self, args): self.cfg = usawa.config.load_config(config_dir=args.c) self.cmd = args.cmd - + self.state = 0 + + # entry parts + self.description = None + self.src = [None, None, None] + self.dst = [None, None, None] + self.amount = None + self.part = [] + self.output = None + self.f = None + self.attach = [] + # set up ledger s = args.l if not s: @@ -28,14 +40,19 @@ class Context: if not s: 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 # set up accounts hierarchy, if applicable self.accounts = None - s = self.cfg.get('MAIN_ACCOUNTS_FILE') + s = self.cfg.get('ACCOUNTS_FILE') if s: - self.accounts = AccountIndex.from_file(self.ledger.uidx, s) + self.accounts = AccountIndex.from_file(self.uidx, s) else: - self.accounts = AccountIndex(self.ledger.uidx) + self.accounts = AccountIndex(self.uidx) + if self.cfg.true('ACCOUNTS_STRICT'): + self.accounts.lock() self.entry = Entry.empty() self.db = None @@ -47,8 +64,59 @@ class Context: self.store = EntryStore(self.db) if args.r: self.entry = self.store.get_draft(self.entry) + self.state = 1 else: self.store.put_draft(self.entry) + self.ref = self.entry.get_ref() + + + def parse_type(self, v): + v = v.lower() + r = None + for k in CATEGORIES: + if k.startswith(v): + r = k + logg.info("expanded input '{}' to category {}".format(v, r)) + break + if not r: + raise ValueError('invalid type: ' + v) + o = getattr(AccountType, r) + logg.debug('accounttype {}'.format(o)) + return o + + + def parse_unit(self, v): + return self.uidx.sym(v) + + + def parse_account(self, v, typ, sym): + account = self.accounts.check(sym, typ, v) + if account: + account = Account.from_path(account) + else: + account = self.accounts.add(v, sym=sym, typ=typ) + return account + + + def parse_amount(self, uidx, sym, v): + return uidx.from_floatstring(sym, 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') + if self.ref == None: + raise ValueError('invalid ref') + argp = argparse.ArgumentParser() @@ -63,3 +131,73 @@ if args.v: logg.setLevel(getattr(logging, args.v.upper())) ctx = Context(args) + + + + +def input_or_default(prompt, default=None, postfix=': ', validate_fn=None): + if default != None: + postfix = ' [{}]'.format(default) + postfix + v = input(prompt + postfix) + if len(v) == 0: + if default == None: + raise ValueError('empty value and no default') + v = default + if validate_fn != None: + validate_fn(v) + return v + + +def do_interactive(ctx): + v = input_or_default('Entry description', ctx.description) + ctx.description = v + + amounts = { + 'src': None, + 'dst': None, + } + + for k in ['src', 'dst']: + o = vars(ctx) + #v = input('Entry {} type: '.format(k)) + + v = input_or_default('Entry {} unit'.format(k), o[k][2]) + unit = ctx.parse_unit(v) + o[k][2] = unit + + v = input_or_default('Entry {} type'.format(k), o[k][0]) + typ = ctx.parse_type(v) + o[k][0] = typ + + v = input_or_default('Entry {} account'.format(k), o[k][1]) + 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) + 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) + + ctx.ref = input_or_default('External ref', ctx.ref) + + #output = input_or_default('Output file', ctx.output) + #logg.debug('output {}'.format(output)) + #return ctx.open(output) + + +if ctx.state == 0: + do_interactive(ctx) + ctx.validate() + #entry = Entry(-1, dt, parent=ledger.current(), description=ctx.description, ref=ctx.ref, unitindex=ctx.uidx) + entry = Entry.empty(description=ctx.description, ref=ctx.ref, unitindex=ctx.uidx) + entry.add_part(ctx.part[0], debit=True) + entry.add_part(ctx.part[1]) + +ctx.store.put_draft(entry) +print(entry)