usawa

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

commit 12c0ee36c027ed07915251f9bcd369f08b0cf6f3
parent 9596246ed4ae2428e404f21b132c681d839cc7a6
Author: lash <dev@holbrook.no>
Date:   Sat,  4 Apr 2026 20:00:41 -0600

Balancer side sums, ledger runningtotal from xml

Diffstat:
Mdummy/usawa/balance.py | 23++++++++++++++++++++---
Mdummy/usawa/cli/entry.py | 59++++++++++++++++++++++++++++++++++++++++++++++-------------
Mdummy/usawa/ledger.py | 89++++++++++++++-----------------------------------------------------------------
Mdummy/usawa/unit.py | 5+++--
4 files changed, 84 insertions(+), 92 deletions(-)

diff --git a/dummy/usawa/balance.py b/dummy/usawa/balance.py @@ -5,18 +5,27 @@ logg = logging.getLogger('usawa.balancer') class Balancer: - def __init__(self, unitindex, value=None): + def __init__(self, unitindex): self.uidx = unitindex self.r = 0 self.m = 0 self.z = 0 + self.zsrc = 0 + self.zdst = 0 def apply_part(self, part): amount = self.uidx.val(part.unit, part.amount) fn = getattr(self, '_handle_' + part.typ) - v = fn(amount[0], part.isdebit) - self.z += abs(amount[0]) + fn(amount[0], part.isdebit) + v = abs(amount[0]) + logg.debug('amount {} v {}'.format(amount, v)) + self.z += v + self.m += amount[1] + if part.isdebit: + self.zsrc += v + else: + self.zdst += v logg.debug('after {} {} => {} = {}'.format(part.unit, part.amount, amount[0], self.r)) @@ -32,6 +41,14 @@ class Balancer: return int(self.z / 2) + def src(self): + return self.zsrc + + + def dst(self): + return self.zdst + + def _handle_income(self, amount, issrc=False): if not issrc: raise TypeError('income can only be src') diff --git a/dummy/usawa/cli/entry.py b/dummy/usawa/cli/entry.py @@ -11,6 +11,9 @@ from usawa.account import Account, AccountDisplay, AccountType logg = logging.getLogger('cli.entry') +class AbortMenu(Exception): + pass + def choose_account(ctx, include_create=False): r = None @@ -150,16 +153,19 @@ def try_entry(ctx, entry_spec): return Entry.empty(unitindex=ctx.uidx) try: - return try_entry_uuid(ctx, entry_spec) + return try_entry_serial(ctx, entry_spec) except ValueError: pass - except FileNotFoundError: - pass try: - return try_entry_serial(ctx, entry_spec) + v = try_entry_uuid(ctx, entry_spec) + if v == None: + raise AttributeError('entry is committed') + return v except ValueError: pass + except FileNotFoundError: + pass try: return try_entry_digest(ctx, entry_spec) @@ -171,10 +177,12 @@ def try_entry(ctx, entry_spec): class EntrySession: - def __init__(self, ctx, entry=None, heading=None, description=None, extref=None, dt=None, ref=None, amount=None, lines=[]): + def __init__(self, ctx, entry=None, heading=None, description=None, extref=None, dt=None, ref=None, amount=None, lines=[], base=None): # context props self.ctx = ctx - self.unitbase = ctx.uidx.base + self.unitbase = base + if self.unitbase == None: + self.unitbase = self.ctx.uidx.base # override props self.description = description @@ -202,6 +210,25 @@ class EntrySession: self._do_prepare() + def _cur_amount(self): + amount = self.ctx.uidx.val(self.unitbase, self.amount) + if amount == None: + amount = self.entry.balancer.value() + else: + amount = amount[0] + return amount + + + def _src_remaining(self): + amount = self._cur_amount() + return amount - self.entry.balancer.src() + + + def _dst_remaining(self): + amount = self._cur_amount() + return amount - self.entry.balancer.dst() + + def _do_prepare(self): uu = uuid.uuid4() self.ref = str(uu) @@ -239,7 +266,10 @@ class EntrySession: #v = input_or_default('Entry {} account'.format(ctx.get('partk'))) #account = parse_account(ctx, v, sym=unit, typ=typ) - v = choose_account(self.ctx) + try: + v = choose_account(self.ctx) + except AbortMenu: + return False account = Account.from_path(v) #amount = None @@ -349,7 +379,7 @@ class EntrySession: def get_description(self): description = self.entry.description if self.heading != None: - description = self.heading + " " + description + description = self.heading + "; " + description return description @@ -363,21 +393,24 @@ Extref: {} self.entry.extref, ) - if self.amount: - s += "Amount: " + str(self.amount) + "\n" - s += "Description: " + self.get_description() + "\n" for v in self.lines: s += "\t" + v + "\n" - s += "Accounts src:\n" + amount = self._cur_amount() + amount = self.ctx.uidx.to_floatstring(self.unitbase, amount) + rem = self._src_remaining() + rem = self.ctx.uidx.to_floatstring(self.unitbase, rem) + s += "Accounts src: {}/{}\n".format(rem, amount) for v in self.entry.debit: typ = getattr(AccountType, v.typ) o = Account.from_path(v.account, sym=v.unit, typ=typ) s += "\t" + o.to_path() + " " + self.ctx.uidx.to_floatstring(v.unit, v.amount) + "\n" - s += "Accounts dst:\n" + rem = self._dst_remaining() + rem = self.ctx.uidx.to_floatstring(self.unitbase, rem) + s += "Accounts dst: {}/{}\n".format(rem, amount) for v in self.entry.credit: typ = getattr(AccountType, v.typ) o = Account.from_path(v.account, sym=v.unit, typ=typ) diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py @@ -139,12 +139,13 @@ class RunningTotal: :rtype: usawa.UnitIndex """ @staticmethod - def from_tree(tree): - unit = self.tree.get('unit') - asset = int(self.tree.find('asset', namespaces=nsmap()).text) - liability = int(self.tree.find('liability', namespaces=nsmap()).text) - return RunningTotal(unit, asset=asset, liability=liability) - + def from_tree(tree, unitindex): + unit = tree.get('unit') + asset = int(tree.find('asset', namespaces=nsmap()).text) + liability = int(tree.find('liability', namespaces=nsmap()).text) + income = int(tree.find('income', namespaces=nsmap()).text) + expense = int(tree.find('expense', namespaces=nsmap()).text) + return RunningTotal(unit, unitindex, asset=int(asset), liability=int(liability), income=int(income), expense=int(expense)) @@ -598,18 +599,17 @@ class Ledger(UsawaElement): logg.warning('currently only support for single identity') break - o = part.find('real', namespaces=nsmap()) - asset = int(o.find('asset', namespaces=nsmap()).text) - liability = int(o.find('liability', namespaces=nsmap()).text) - ledger.real = RunningTotal(unit, unitindex, asset=asset, liability=liability) + for v in part.iter(NSPREFIX + 'real'): + sym = v.get('unit') + o = RunningTotal.from_tree(v, unitindex) + ledger.running[o.sym] = o + logg.debug('ledger running total {} (real): {}'.format(o.sym, o)) for v in part.iter(NSPREFIX + 'virt'): - income = int(v.find('income', namespaces=nsmap()).text) - expense = int(v.find('expense', namespaces=nsmap()).text) - asset = int(v.find('asset', namespaces=nsmap()).text) - 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) + o = RunningTotal.from_tree(v, unitindex) + ledger.running[o.sym] = o + logg.debug('ledger running total {} (virt): {}'.format(o.sym, o)) if ledger.running.get(unit) == None: ledger.running[unit] = RunningTotal(unit, unitindex) @@ -645,7 +645,6 @@ class Ledger(UsawaElement): i = 0 for v in tree.iter(NSPREFIX + 'entry'): i += 1 - logg.debug('>>>>>>>>>>>>> processing entry {}'.format(lxml.etree.tostring(v))) o = Entry.from_tree(v, self.uidx, min=self.serial) self.add_entry(o) (k, v) = o.get_lookup('sha512') @@ -765,63 +764,5 @@ class Ledger(UsawaElement): return r -# """Generate the simple data structure used for rencode serialization. -# -# :returns: data structure -# :rtype: list -# """ -# def to_list(self): -# ts = int(self.dt.timestamp()) -# ts_bytes = ts.to_bytes(4, byteorder='big') -# #units = self.uidx.serialize() -# units = self.uidx.to_list() -# #identities = self.acl.serialize() -# identities = self.acl.to_list() -# totals = [] -# #v = self.running[self.uidx.base].serialize() -# v = self.running[self.uidx.base].to_list() -# totals.append(v) -# for k in self.running.keys(): -# if k == self.uidx.base: -# continue -# v = self.running[k].serialize() -# totals.append(v) -# d = [ -# self.topic, -# varints.leb128s.encode(self.serial), -# self.cur, -# ts_bytes, -# units, -# identities, -# totals, -# ] -# return d -# -# -# """Generate the unit index part of an Entry in wire format. -# -# :returns: String representation of the entry, in rencode format. -# :rtype: str -# """ -# def serialize(self): -# b = self.to_list() -# return rencode.dumps(b) -# - - -# """Create a ledger object from serialized data. -# -# :param data: rencoded ledger object, as produced by the serialize() method. -# :type data: str -# :returns: Ledger object. -# :rtype: usawa.Ledger -# """ -# @staticmethod -# def deserialize(self, unitindex, serial=None, base=None, acl=None, src=None): -# v = rencode.loads(data) -# o = Ledger(base=base, serial=serial, acl=acl, src=src, topic=v[0]) -# return o - - def __str__(self): return "state: " + self.base.hex() + " serial " + str(self.serial) diff --git a/dummy/usawa/unit.py b/dummy/usawa/unit.py @@ -73,7 +73,7 @@ class UnitIndex: v = int(r) m = int((r - v) * 1000000000) logg.debug('val {} -> {},{} adj {}'.format(r, v, m, adj)) - return (v, m,) + return (v, m, ) """Add a unit to the index. @@ -112,7 +112,6 @@ class UnitIndex: 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.rate[o.get('sym')] = 1000000000 r.check() return r @@ -177,6 +176,8 @@ class UnitIndex: :todo: Rename to to_decimalstring """ def to_floatstring(self, sym, v, allow_negative=True): + if isinstance(v, float): + v = self.from_float(sym, v) neg = v < 0 if neg and not allow_negative: raise ValueError('negative value not allowed')