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:
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')