usawa

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

commit cb2707e8ac8c4777381b30e852abf63b35f88f29
parent 88df6833c0e674227227ac5fa92104eb5a992f15
Author: lash <dev@holbrook.no>
Date:   Tue, 24 Mar 2026 19:29:19 -0600

Move rates calculations to unit

Diffstat:
Mdummy/tests/balance.py | 34+++++++++++++++++++++++++---------
Mdummy/tests/unit.py | 21++++++++++++++++++++-
Mdummy/usawa/balance.py | 176+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mdummy/usawa/data/schema.xsd | 6------
Mdummy/usawa/unit.py | 71++++++++++++++++++++++++++++++++++++++++++++---------------------------
5 files changed, 193 insertions(+), 115 deletions(-)

diff --git a/dummy/tests/balance.py b/dummy/tests/balance.py @@ -4,7 +4,7 @@ import datetime import os from usawa import EntryPart, Entry, UnitIndex -from usawa.balance import Balancer +from usawa.balance import Translator logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() @@ -22,14 +22,30 @@ class TestBalancer(unittest.TestCase): self.dtreg = datetime.datetime.now() - def test_balancer_basic(self): - dst = EntryPart('FOO', 'asset', 'foo', 1337) - src = EntryPart('FOO', 'income', 'baz', 1337, debit=True) - entry = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) - entry.add_part(src) - entry.add_part(dst) - o = Balancer(self.uidx, entry, base='FOO') - self.assertTrue(o.balanced()) + def test_translate_ex(self): + o = Translator(self.uidx, 'FOO') + o.set_ex('BAR', 230000) + r = o.val('BAR', 42333) + print(r) + +# def test_balancer_process(self): +# dst = EntryPart('FOO', 'asset', 'foo', 1337) +# src = EntryPart('FOO', 'income', 'baz', 1337, debit=True) +# entry = Entry(42, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), parent=self.parent, ref=self.ref, description=self.description, tx_datereg=self.dtreg) +# entry.add_part(src) +# entry.add_part(dst) +# o = Balancer(self.uidx, entry=entry, base='FOO') +# self.assertTrue(o.balanced()) +# +# +# def test_balancer_parts_simple(self): +# o = Balancer(self.uidx, base='FOO') +# src = EntryPart('FOO', 'income', 'baz', 1337, debit=True) +# o.apply_part(src) +# dst = EntryPart('FOO', 'asset', 'foo', 1337) +# o.apply_part(dst) +# self.assertTrue(o.balanced()) + if __name__ == '__main__': diff --git a/dummy/tests/unit.py b/dummy/tests/unit.py @@ -1,7 +1,10 @@ import unittest +import logging from usawa import UnitIndex +logging.basicConfig(level=logging.DEBUG) + class TestUnit(unittest.TestCase): @@ -9,7 +12,7 @@ class TestUnit(unittest.TestCase): self.uidx_default = UnitIndex('FOO') self.uidx_three = UnitIndex('FOO', precision=3) self.uidx_none = UnitIndex('FOO', precision=0) - self.uidx_default.add('BAR', precision=4, ex=0.2) + self.uidx_default.add('BAR', precision=4, rate=0.23) def test_tostring(self): @@ -46,5 +49,21 @@ class TestUnit(unittest.TestCase): self.assertEqual(v, 12345) + def test_unit_rates(self): + #self.uidx_default.add('BAR', precision=3) + #r = self.uidx_default.val('BAR', 42333) + with self.assertRaises(ValueError): + self.uidx_default.set_rate('FOO', 230000) + #self.uidx_default.set_rate('BAR', 230000) + r = self.uidx_default.val('BAR', 4233300) + self.assertEqual(r[0], 9736) + self.assertEqual(r[1], 590000) + + r = self.uidx_default.set_rate('BAR', 1000000) + r = self.uidx_default.val('BAR', 4233300) + self.assertEqual(r[0], 42333) + self.assertEqual(r[1], 0) + + if __name__ == '__main__': unittest.main() diff --git a/dummy/usawa/balance.py b/dummy/usawa/balance.py @@ -5,78 +5,110 @@ logg = logging.getLogger('usawa.balancer') class Balancer: - def __init__(self, uidx, entry, base=None): + def __init__(self, translator): + self.tr = translator self.r = 0 - self.uidx = uidx - self.max_precision = 0 - self.ex = {} - self.base = base - self.scan(entry) - self.process(entry) - def scan(self, entry): - base = None - for part in entry: - precision = self.uidx.get(part.unit) - if precision > self.max_precision: - self.max_precision = precision - self.ex[part.unit] = 1000000000 - if base == None: - base = part.unit - continue - if base != part.unit: - if self.base == None: - logg.warning('base is not set with entry with different units') - logg.debug('have max precision {}'.format(self.max_precision)) - self.base = base - - - def set_rate(self, unit, rate): - if unit == self.base: - raise ValueError('base rate against itself') - self.ex[unit] = rate - - - def process(self, entry): - for part in entry: - precision = self.uidx.get(part.unit) - mod = 10 ** (self.max_precision - precision) - amount = part.amount * mod - ex = self.ex[part.unit] / 1000000000 - amount *= ex - fn = getattr(self, 'handle_' + part.typ) - fn(amount, part.isdebit) - logg.debug('after {} {} (ex {}) = {}'.format(part.unit, amount, ex, self.r)) - - - def balanced(self): - return self.r == 0 - - - def handle_income(self, amount, issrc=False): - if issrc: - self.r += amount - else: - self.r -= amount - - - def handle_expense(self, amount, issrc=False): - if issrc: - self.r -= amount - else: - self.r += amount - - - def handle_asset(self, amount, issrc=False): - if issrc: - self.r += amount - else: - self.r -= amount - - - def handle_liability(self, amount, issrc=False): - if issrc: - self.r -= amount - else: - self.r += amount + def apply_part(self, part): + precision = self.uidx.get(part.unit) + mod = 10 ** (self.max_precision - precision) + amount = part.amount * mod + ex = self.ex[part.unit] / 1000000000 + amount *= ex + fn = getattr(self, 'handle_' + part.typ) + fn(amount, part.isdebit) + logg.debug('after {} {} (ex {}) = {}'.format(part.unit, amount, ex, self.r)) + + +#class Balancer: +# +# def __init__(self, uidx, entry=None, base=None): +# self.r = 0 +# self.uidx = uidx +# self.max_precision = 0 +# self.ex = {} +# self.running = {} +# self.base = base +# if entry != None: +# self.scan(entry) +# self.process_entry(entry) +# +# +# def scan_units(self, uidx): +# +# +# def scan(self, entry): +# base = None +# for part in entry: +# precision = self.uidx.get(part.unit) +# if precision > self.max_precision: +# self.max_precision = precision +# self.ex[part.unit] = 1000000000 +# if base == None: +# base = part.unit +# continue +# if base != part.unit: +# if self.base == None: +# logg.warning('base is not set with entry with different units') +# logg.debug('have max precision {}'.format(self.max_precision)) +# self.base = base +# +# +# def init_unit(self, unit): +# self.ex[unit] = 100000000 +# self. +# +# +# def set_rate(self, unit, rate): +# if unit == self.base: +# raise ValueError('base rate against itself') +# self.ex[unit] = rate +# +# +# def process_entry(self, entry): +# for part in entry: +# self.apply_part(part) +# +# +# def apply_part(self, part): +# precision = self.uidx.get(part.unit) +# mod = 10 ** (self.max_precision - precision) +# amount = part.amount * mod +# ex = self.ex[part.unit] / 1000000000 +# amount *= ex +# fn = getattr(self, 'handle_' + part.typ) +# fn(amount, part.isdebit) +# logg.debug('after {} {} (ex {}) = {}'.format(part.unit, amount, ex, self.r)) +# +# +# def balanced(self): +# return self.r == 0 +# +# +# def handle_income(self, amount, issrc=False): +# if issrc: +# self.r += amount +# else: +# self.r -= amount +# +# +# def handle_expense(self, amount, issrc=False): +# if issrc: +# self.r -= amount +# else: +# self.r += amount +# +# +# def handle_asset(self, amount, issrc=False): +# if issrc: +# self.r += amount +# else: +# self.r -= amount +# +# +# def handle_liability(self, amount, issrc=False): +# if issrc: +# self.r -= amount +# else: +# self.r += amount diff --git a/dummy/usawa/data/schema.xsd b/dummy/usawa/data/schema.xsd @@ -79,12 +79,6 @@ <xs:complexType name="Unit"> <xs:sequence> <xs:element name="precision" type="xs:positiveInteger" maxOccurs="1" minOccurs="1" /> - <xs:choice> - <xs:element name="exchange" type="xs:positiveInteger" maxOccurs="1" minOccurs="1" /> - <xs:sequence> - <xs:element name="rate" type="Rate" minOccurs="1" maxOccurs="unbounded" /> - </xs:sequence> - </xs:choice> </xs:sequence> <xs:attribute name="sym" type="xs:string" /> </xs:complexType> diff --git a/dummy/usawa/unit.py b/dummy/usawa/unit.py @@ -1,4 +1,5 @@ import logging +import copy import rencode import lxml.etree @@ -27,12 +28,48 @@ class UnitIndex: :param precision: The decimal precision of the base unit. Default is 2. :type precision: int """ - def __init__(self, base, precision=None): + def __init__(self, base=None, precision=None): + self.detail = {} + self.rate = {} self.base = base - if precision == None: - precision = UnitIndex.default_precision - self.detail = {base: precision} - self.exchange = {base: UnitIndex.default_exchange} + if base != None: + if precision == None: + precision = UnitIndex.default_precision + self.detail = {base: precision} + self.exchange = {base: UnitIndex.default_exchange} + + + def clone(self): + o = UnitIndex() + o.detail = self.detail + o.base = self.base + o.exchange = copy.copy(self.exchange) + return o + + + def set_rate(self, unit, rate): + if unit == self.base: + raise ValueError('cannot adjust rate for base') + if not isinstance(rate, float): + rate /= 1000000 + self.rate[unit] = rate + + + def val(self, unit, amount): + r = self.rate[unit] * amount + # TODO: embed in rate + base_precision = self.detail[self.base] + adj = base_precision - self.detail[unit] + if adj < 0: + logg.debug('r {}'.format(r)) + r /= (10 ** abs(adj)) + else: + r *= (10 ** adj) + + v = int(r) + m = int((r - v) * 1000000) + logg.debug('val {} -> {},{} adj {}'.format(r, v, m, adj)) + return (v, m,) """Add a unit to the index. @@ -46,11 +83,9 @@ class UnitIndex: :param ex: The exchange rate of the unit, relative to the base unit. Default is 1000000000 (1.0). :type ex: int or float """ - def add(self, sym, precision=2, ex=1000000000): + def add(self, sym, precision=2, rate=1000000): self.detail[sym] = precision - if isinstance(ex, float): - ex = int(ex*1000000000) # nano resolution - self.exchange[sym] = ex + self.set_rate(sym, rate) """Create a unit index object from XML. @@ -72,7 +107,6 @@ class UnitIndex: 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.exchange[o.get('sym')] = int(o.find('exchange', namespaces=nsmap()).text) r.check() return r @@ -113,20 +147,6 @@ class UnitIndex: return k - """Retrieve the exchange rate for the unit. - - The value represents a decimal number with nano precision. For example, a value of 4200000000 corresponds to a float value of 4.2. - - :param k: Unit symbol. - :type k: str - :raises: KeyError if symbol not found. - :returns: Rate - :rtype: int - """ - def ex(self, k): - return self.exchange[k] - - """Retrieve a list of all the units in the index. :returns: The list of symbols. @@ -261,9 +281,6 @@ class UnitIndex: o = lxml.etree.SubElement(unit, 'precision') o.text = str(self.detail[k]) unit.append(o) - o = lxml.etree.SubElement(unit, 'exchange') - o.text = str(self.exchange[k]) - unit.append(o) tree.append(unit) return tree