From 646599faf0031f522e66098b2ca377a1eca08986 Mon Sep 17 00:00:00 2001 From: Jorgen Schaefer Date: Fri, 7 Sep 2012 19:27:39 +0200 Subject: Added support for slots, account descriptions, and split reconcile dates. --- gnucashxml.py | 151 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 127 insertions(+), 24 deletions(-) diff --git a/gnucashxml.py b/gnucashxml.py index 908fbda..da9ca0e 100644 --- a/gnucashxml.py +++ b/gnucashxml.py @@ -14,11 +14,12 @@ class Book(object): a reference to the accounts, transactions, and commodities. """ def __init__(self, guid, transactions=None, root_account=None, - commodities=None): + commodities=None, slots=None): self.guid = guid self.transactions = transactions or [] self.root_account = root_account self.commodities = commodities or [] + self.slots = slots or {} def __repr__(self): return "".format(self.guid) @@ -54,15 +55,18 @@ class Account(object): An account is part of a tree structure of accounts and contains splits. """ def __init__(self, name, guid, actype, parent=None, - commodity=None, commodity_scu=None): + commodity=None, commodity_scu=None, + description=None, slots=None): self.name = name self.guid = guid self.actype = actype + self.description = description self.parent = parent self.children = [] self.commodity = commodity self.commodity_scu = commodity_scu self.splits = [] + self.slots = slots or {} def __repr__(self): return "".format(self.guid) @@ -97,13 +101,15 @@ class Transaction(object): def __init__(self, guid=None, currency=None, date=None, date_entered=None, - description=None, splits=None): + description=None, splits=None, + slots=None): self.guid = guid self.currency = currency self.date = date self.date_entered = date_entered self.description = description self.splits = splits or [] + self.slots = slots or {} def __repr__(self): return u"".format(self.guid) @@ -122,15 +128,18 @@ class Split(object): """ def __init__(self, guid=None, memo=None, - reconciled_state=None, value=None, - quantity=None, account=None, transaction=None): + reconciled_state=None, reconcile_date=None, value=None, + quantity=None, account=None, transaction=None, + slots=None): self.guid = guid self.reconciled_state = reconciled_state + self.reconcile_date = reconcile_date self.value = value self.quantity = quantity self.account = account self.transaction = transaction self.memo = memo + self.slots = slots def __repr__(self): return "".format(self.guid) @@ -152,6 +161,12 @@ def from_filename(filename): return parse(gzip.open(filename, "rb")) +# Implemented: +# - gnc:book +# +# Not implemented: +# - gnc:count-data +# - This seems to be primarily for integrity checks? def parse(fobj): """Parse GNU Cash XML data from a file object and return a Book object.""" tree = ElementTree.parse(fobj) @@ -161,6 +176,18 @@ def parse(fobj): return _book_from_tree(root.find("{http://www.gnucash.org/XML/gnc}book")) +# Implemented: +# - book:id +# - book:slots +# - gnc:commodity +# - gnc:account +# - gnc:transaction +# +# Not implemented: +# - gnc:schedxaction +# - gnc:template-transactions +# - gnc:count-data +# - This seems to be primarily for integrity checks? def _book_from_tree(tree): guid = tree.find('{http://www.gnucash.org/XML/book}id').text @@ -192,28 +219,42 @@ def _book_from_tree(tree): transactions.append(_transaction_from_tree(child, accountdict, commoditydict)) - # '{http://www.gnucash.org/XML/gnc}schedxaction' - # '{http://www.gnucash.org/XML/gnc}template-transactions' - # '{http://www.gnucash.org/XML/gnc}count-data' + + slots = _slots_from_tree( + tree.find('{http://www.gnucash.org/XML/book}slots')) return Book(guid=guid, transactions=transactions, root_account=root_account, - commodities=commodities) - - + commodities=commodities, + slots=slots) + + +# Implemented: +# - cmdty:id +# - cmdty:space +# +# Not implemented: +# - cmdty:get_quotes => unknown, empty, optional +# - cmdty:quote_tz => unknown, empty, optional +# - cmdty:source => text, optional, e.g. "currency" +# - cmdty:name => optional, e.g. "template" +# - cmdty:xcode => optional, e.g. "template" +# - cmdty:fraction => optional, e.g. "1" def _commodity_from_tree(tree): name = tree.find('{http://www.gnucash.org/XML/cmdty}id').text space = tree.find('{http://www.gnucash.org/XML/cmdty}space').text - # Ignored: - # - cmdty:get_quotes - # - cmdty:quote_tz - # - cmdty:source - # - cmdty:name - # - cmdty:xcode - # - cmdty:fraction return Commodity(name=name, space=space) +# Implemented: +# - act:name +# - act:id +# - act:type +# - act:description +# - act:commodity +# - act:commodity-scu +# - act:parent +# - act:slots def _account_from_tree(tree, commoditydict): act = '{http://www.gnucash.org/XML/act}' cmdty = '{http://www.gnucash.org/XML/cmdty}' @@ -221,6 +262,10 @@ def _account_from_tree(tree, commoditydict): name = tree.find(act + 'name').text guid = tree.find(act + 'id').text actype = tree.find(act + 'type').text + description = tree.find(act + "description") + if description is not None: + description = description.text + slots = _slots_from_tree(tree.find(act + 'slots')) if actype == 'ROOT': parent_guid = None commodity = None @@ -233,14 +278,22 @@ def _account_from_tree(tree, commoditydict): cmdty + 'id').text commodity_scu = tree.find(act + 'commodity-scu').text commodity = commoditydict[(commodity_space, commodity_name)] - # We ignore act:slots return parent_guid, Account(name=name, + description=description, guid=guid, actype=actype, commodity=commodity, - commodity_scu=commodity_scu) - - + commodity_scu=commodity_scu, + slots=slots) + +# Implemented: +# - trn:id +# - trn:currency +# - trn:date-posted +# - trn:date-entered +# - trn:description +# - trn:splits / trn:split +# - trn:slots def _transaction_from_tree(tree, accountdict, commoditydict): trn = '{http://www.gnucash.org/XML/trn}' cmdty = '{http://www.gnucash.org/XML/cmdty}' @@ -258,11 +311,13 @@ def _transaction_from_tree(tree, accountdict, commoditydict): date_entered = parse_date(tree.find(trn + "date-entered/" + ts + "date").text) description = tree.find(trn + "description").text + slots = _slots_from_tree(tree.find(trn + "slots")) transaction = Transaction(guid=guid, currency=currency, date=date, date_entered=date_entered, - description=description) + description=description, + slots=slots) for subtree in tree.findall(trn + "splits/" + trn + "split"): split = _split_from_tree(subtree, accountdict, transaction) @@ -271,29 +326,77 @@ def _transaction_from_tree(tree, accountdict, commoditydict): return transaction +# Implemented: +# - split:id +# - split:memo +# - split:reconciled-state +# - split:reconcile-date +# - split:value +# - split:quantity +# - split:account +# - split:slots def _split_from_tree(tree, accountdict, transaction): split = '{http://www.gnucash.org/XML/split}' + ts = "{http://www.gnucash.org/XML/ts}" guid = tree.find(split + "id").text memo = tree.find(split + "memo") if memo is not None: memo = memo.text reconciled_state = tree.find(split + "reconciled-state").text + reconcile_date = tree.find(split + "reconcile-date/" + ts + "date") + if reconcile_date is not None: + reconcile_date = parse_date(reconcile_date.text) value = _parse_number(tree.find(split + "value").text) quantity = _parse_number(tree.find(split + "quantity").text) account_guid = tree.find(split + "account").text account = accountdict[account_guid] + slots = _slots_from_tree(tree.find(split + "slots")) split = Split(guid=guid, memo=memo, reconciled_state=reconciled_state, + reconcile_date=reconcile_date, value=value, quantity=quantity, account=account, - transaction=transaction) + transaction=transaction, + slots=slots) account.splits.append(split) return split +# Implemented: +# - slot +# - slot:key +# - slot:value +# - ts:date +# - gdate +def _slots_from_tree(tree): + if tree is None: + return {} + slot = "{http://www.gnucash.org/XML/slot}" + ts = "{http://www.gnucash.org/XML/ts}" + slots = {} + for elt in tree.findall("slot"): + key = elt.find(slot + "key").text + value = elt.find(slot + "value") + type_ = value.get('type', 'string') + if type_ == 'integer': + slots[key] = long(value.text) + elif type_ == 'numeric': + slots[key] = _parse_number(value.text) + elif type_ in ('string', 'guid'): + slots[key] = value.text + elif type_ == 'gdate': + slots[key] = parse_date(value.find("gdate").text) + elif type_ == 'timespec': + slots[key] = parse_date(value.find(ts + "date").text) + elif type_ == 'frame': + slots[key] = _slots_from_tree(value) + else: + raise RuntimeError("Unknown slot type {}".format(type_)) + return slots + def _parse_number(numstring): num, denum = numstring.split("/") return decimal.Decimal(num) / decimal.Decimal(denum) -- cgit v1.2.3