usawa

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

commit 2a344b0820949f98d49375a6c3d9e6affd586b65
parent 4b9d730cbefa7a0cf4100013a44d3c85c48001eb
Author: lash <dev@holbrook.no>
Date:   Sat, 28 Mar 2026 19:14:32 -0600

Account searcher, multi-unit ledger

Diffstat:
Mdummy/usawa/account.py | 17+++++++++++++----
Mdummy/usawa/asset.py | 12+++++++++---
Mdummy/usawa/entry.py | 1+
Mdummy/usawa/ledger.py | 2+-
Mdummy/usawa/runnable/create.py | 40+++++++++++++++++++++++++++++++---------
Mdummy/usawa/runnable/entry.py | 1-
Mdummy/usawa/unit.py | 14+++++++++++---
7 files changed, 66 insertions(+), 21 deletions(-)

diff --git a/dummy/usawa/account.py b/dummy/usawa/account.py @@ -1,5 +1,6 @@ import enum import logging +import re from .error import AccountError @@ -40,6 +41,7 @@ class AccountType(enum.Enum): expense = 'Expense' imprt = 'Import' export = 'Export' + equity = 'Equity' class AccountDisplay(enum.IntEnum): @@ -149,10 +151,14 @@ class AccountIndex: return self.check(o[0], o[1], o[2]) - def set_filter(self, sym=None, typ=None, display=AccountDisplay.full): + def set_filter(self, sym=None, typ=None, path=None, display=AccountDisplay.full): if typ != None: typ = typ.value - self.iterfilter = (sym, typ, display,) + self.iterfilter = (sym, typ, path, display,) + + + def reset_filter(self): + self.iterfilter = None def __iter__(self, fltr=None): @@ -176,12 +182,15 @@ class AccountIndex: if fltr[1] != None: if typ.casefold() != fltr[1].casefold(): continue + if fltr[2] != None: + if not re.search(fltr[2], v, re.IGNORECASE): + continue if self.iterval == None: self.iterval = [] if fltr != None: - if fltr[2] != AccountDisplay.full: + if fltr[3] != AccountDisplay.full: path = v - if fltr[2] == AccountDisplay.typ: + if fltr[3] == AccountDisplay.typ: path = typ + '/' + path self.iterval.append(path) return self diff --git a/dummy/usawa/asset.py b/dummy/usawa/asset.py @@ -137,7 +137,7 @@ class Asset(UsawaElement): """ @staticmethod - def from_file(filepath, description=None, slug=None, mimetype=None, ref=None, extref=None): + def from_file(filepath, description=None, slug=None, mimetype=None, ref=None, extref=None, w=None): f = open(filepath, "rb") return Asset.from_io( f, @@ -148,6 +148,7 @@ class Asset(UsawaElement): mimetype=mimetype, ref=ref, extref=extref, + w=w, ) """Instantiate an asset object from an input stream. @@ -175,11 +176,13 @@ class Asset(UsawaElement): @staticmethod def from_io( - io, src, closer=None, description=None, slug=None, mimetype=None, extref=None, ref=None + io, src, closer=None, description=None, slug=None, mimetype=None, extref=None, ref=None, w=None, ): o = Asset(ref=ref) h = hashlib.sha512() b = io.read(BLOCKSIZE) + if w != None: + w.write(b) if mimetype == None: v = mimetypes.guess_file_type(src, strict=True) if v != None: @@ -194,13 +197,16 @@ class Asset(UsawaElement): b = io.read(BLOCKSIZE) if len(b) == 0: break + if w != None: + w.write(b) h.update(b) c += len(b) if closer != None: closer() o.digest = h.digest() - (o.slug, o.ext) = parse_path(src) + if src != None: + (o.slug, o.ext) = parse_path(src) s = mimetypes.guess_extension(o.mime, strict=True) if s != None: o.ext = s[1:] diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py @@ -238,6 +238,7 @@ class Entry(UsawaElement): self.credit.append(part) if self.balancer != None: self.balancer.apply_part(part) + logg.debug('add part {} to entry {}'.format(part, self)) """Append a single media asset to the attachment list for the entry. diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py @@ -572,6 +572,7 @@ class Ledger(UsawaElement): units = tree.find('units', namespaces=nsmap()) unitindex = UnitIndex.from_tree(units) + unit = units.get('base') part = tree.find('incoming', namespaces=nsmap()) serial = int(part.get('serial')) @@ -607,7 +608,6 @@ class Ledger(UsawaElement): liability = int(v.find('liability', namespaces=nsmap()).text) sym = v.get('unit') ledger.running[sym] = RunningTotal(sym, unitindex, income=income, expense=expense, asset=asset, liability=liability) - logg.debug(r.running[sym]) if ledger.running.get(unit) == None: ledger.running[unit] = RunningTotal(unit, unitindex) diff --git a/dummy/usawa/runnable/create.py b/dummy/usawa/runnable/create.py @@ -20,8 +20,8 @@ logg = logging.getLogger() class Context: def __init__(self): - self.unit = None - self.unit_precision = None + #self.unit = None + #self.unit_precision = None self.uidx = None self.topic = None self.uri = None @@ -46,9 +46,32 @@ class Context: @staticmethod def from_args(args): ctx = Context() - ctx.unit = args.unit - ctx.unit_precision = args.unit_precision - ctx.uidx = UnitIndex(ctx.unit, precision=ctx.unit_precision) + unitspec = None + unit = None + unit_precision = None + try: + unitspec = args.unit[0].split(':') + unit = unitspec[0] + logg.debug('have unit {}'.format(unit)) + unit_precision = None + try: + unit_precision = unitspec[1] + except IndexError: + unit_precision = UnitIndex.default_precision + except IndexError: + unit = UnitIndex.default_unit + unit_precision = UnitIndex.default_precision + ctx.uidx = UnitIndex(unit, precision=unit_precision) + for v in args.unit[1:]: + unitspec = v.split(':') + unit = unitspec[0] + unit_precision = None + try: + unit_precision = unitspec[1] + except IndexError: + unit_precision = UnitIndex.default_precision + ctx.uidx.add(unit, precision=unit_precision) + ctx.topic = args.topic if ctx.topic == None: ctx.topic = str(uuid.uuid4()) @@ -63,8 +86,8 @@ class Context: def validate(self): self.topic = parse_topic(self.topic) self.uri = parse_uri(self.uri) - self.unit = parse_unit(self.unit) - self.unit_precision = parse_unit_precision(self.unit_precision) + #self.unit = parse_unit(self.unit) + #self.unit_precision = parse_unit_precision(self.unit_precision) return self @@ -112,11 +135,10 @@ def input_or_default(prompt, default=None, postfix=': ', validate_fn=None): argp = argparse.ArgumentParser() argp.add_argument('-i', action='store_true', help='interactive edit') argp.add_argument('-t', dest='topic', type=str, help='ledger topic') -argp.add_argument('-u', '--unit', type=str, default=UnitIndex.default_unit, help='Unit to use for transaction') +argp.add_argument('-u', '--unit', type=str, action='append', default=[], help='Units to use for transaction') argp.add_argument('-o', type=str, dest='output', help='output file for updated XML document') argp.add_argument('-l', type=str, dest='src_uri', help='URI for data source') argp.add_argument('-c', type=str, help='override config dir') -argp.add_argument('--unit-precision', type=int, default=UnitIndex.default_precision, help='Unit precision') arg = argp.parse_args() ctx = Context.from_args(arg) cfg = usawa.config.load_config(config_dir=arg.c) diff --git a/dummy/usawa/runnable/entry.py b/dummy/usawa/runnable/entry.py @@ -198,7 +198,6 @@ def do_interactive_one(ctx): def do_interactive_two(ctx, entry): - r = True while r: try: diff --git a/dummy/usawa/unit.py b/dummy/usawa/unit.py @@ -57,6 +57,8 @@ class UnitIndex: def val(self, unit, amount): + if isinstance(amount, float): + amount = self.from_float(unit, amount) r = self.rate[unit] * amount logg.debug('rate {} {} {}'.format(r, amount, self.rate[unit])) # TODO: embed in rate @@ -107,9 +109,10 @@ class UnitIndex: base = tree.get('base') r = UnitIndex(base) for o in tree.iter(NSPREFIX + 'unit'): - logg.debug('add unit ' + o.get('sym')) - r.detail[o.get('sym')] = int(o.find('precision', namespaces=nsmap()).text) - r.check() + precision = int(o.find('precision', namespaces=nsmap()).text) + logg.debug('add unit {} precision {}'.format(o.get('sym'), precision)) + r.detail[o.get('sym')] = precision + r.check() return r @@ -232,6 +235,11 @@ class UnitIndex: return int(r) + def from_float(self, sym, v): + adj = 10 ** self.detail[sym] + return int(v * adj) + + """Generate the simple data structure used for rencode serialization. :returns: data structure