Skip to content

Instantly share code, notes, and snippets.

@erpreciso
Forked from nonducor/gcash2ledger.py
Created July 31, 2019 08:28

Revisions

  1. @nonducor nonducor revised this gist Jan 7, 2018. 1 changed file with 2 additions and 4 deletions.
    6 changes: 2 additions & 4 deletions gcash2ledger.py
    Original file line number Diff line number Diff line change
    @@ -67,8 +67,7 @@ def toLedgerFormat(self, indent=0):
    If provided, `indent` will be the indentation (in spaces) of the entry.
    """
    outPattern = ('{spaces}commodity {id}\n'
    '{spaces} note {name} ({space}:{id})\n'
    '{spaces} default\n')
    '{spaces} note {name} ({space}:{id})\n')
    return outPattern.format(spaces=' '*indent, **self.__dict__)


    @@ -96,8 +95,7 @@ def fullName(self):

    def toLedgerFormat(self, indent=0):
    outPattern = ('{spaces}account {fullName}\n'
    '{spaces} note {description} (type: {type})\n'
    '{spaces} default\n')
    '{spaces} note {description} (type: {type})\n')
    return outPattern.format(spaces=' '*indent, fullName=self.fullName(),
    **self.__dict__)

  2. @nonducor nonducor created this gist Jan 6, 2018.
    245 changes: 245 additions & 0 deletions gcash2ledger.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,245 @@
    #! /usr/bin/python3

    import os
    import sys
    import dateutil.parser
    import xml.etree.ElementTree

    nss = {'gnc': 'http://www.gnucash.org/XML/gnc',
    'act': 'http://www.gnucash.org/XML/act',
    'book': 'http://www.gnucash.org/XML/book',
    'cd': 'http://www.gnucash.org/XML/cd',
    'cmdty': 'http://www.gnucash.org/XML/cmdty',
    'price': 'http://www.gnucash.org/XML/price',
    'slot': 'http://www.gnucash.org/XML/slot',
    'split': 'http://www.gnucash.org/XML/split',
    'sx': 'http://www.gnucash.org/XML/sx',
    'trn': 'http://www.gnucash.org/XML/trn',
    'ts': 'http://www.gnucash.org/XML/ts',
    'fs': 'http://www.gnucash.org/XML/fs',
    'bgt': 'http://www.gnucash.org/XML/bgt',
    'recurrence': 'http://www.gnucash.org/XML/recurrence',
    'lot': 'http://www.gnucash.org/XML/lot',
    'addr': 'http://www.gnucash.org/XML/addr',
    'owner': 'http://www.gnucash.org/XML/owner',
    'billterm': 'http://www.gnucash.org/XML/billterm',
    'bt-days': 'http://www.gnucash.org/XML/bt-days',
    'bt-prox': 'http://www.gnucash.org/XML/bt-prox',
    'cust': 'http://www.gnucash.org/XML/cust',
    'employee': 'http://www.gnucash.org/XML/employee',
    'entry': 'http://www.gnucash.org/XML/entry',
    'invoice': 'http://www.gnucash.org/XML/invoice',
    'job': 'http://www.gnucash.org/XML/job',
    'order': 'http://www.gnucash.org/XML/order',
    'taxtable': 'http://www.gnucash.org/XML/taxtable',
    'tte': 'http://www.gnucash.org/XML/tte',
    'vendor': 'http://www.gnucash.org/XML/vendor', }


    class DefaultAttributeProducer:
    def __init__(self, defaultValue):
    self.__defaultValue = defaultValue

    def __getattr__(self, value):
    return self.__defaultValue


    def orElse(var, default=''):
    if var is None:
    return DefaultAttributeProducer(default)
    else:
    return var


    class Commodity:
    def __init__(self, e):
    """From a XML e representing a commodity, generates a representation of
    the commodity
    """

    self.space = orElse(e.find('cmdty:space', nss)).text
    self.id = orElse(e.find('cmdty:id', nss)).text
    self.name = orElse(e.find('cmdty:name', nss)).text

    def toLedgerFormat(self, indent=0):
    """Format the commodity in a way good to be interpreted by ledger.
    If provided, `indent` will be the indentation (in spaces) of the entry.
    """
    outPattern = ('{spaces}commodity {id}\n'
    '{spaces} note {name} ({space}:{id})\n'
    '{spaces} default\n')
    return outPattern.format(spaces=' '*indent, **self.__dict__)


    class Account:
    def __init__(self, accountDb, e):
    self.accountDb = accountDb
    self.name = e.find('act:name', nss).text
    self.id = e.find('act:id', nss).text
    self.accountDb[self.id] = self
    self.description = orElse(e.find('act:description', nss)).text
    self.type = e.find('act:type', nss).text
    self.parent = orElse(e.find('act:parent', nss), None).text
    self.used = False # Mark accounts that were in a transaction
    self.commodity = orElse(e.find('act:commodity/cmdty:id', nss), None).text

    def getParent(self):
    return self.accountDb[self.parent]

    def fullName(self):
    if self.parent is not None and self.getParent().type != 'ROOT':
    prefix = self.getParent().fullName() + ':'
    else:
    prefix = '' # ROOT will not be displayed
    return prefix + self.name

    def toLedgerFormat(self, indent=0):
    outPattern = ('{spaces}account {fullName}\n'
    '{spaces} note {description} (type: {type})\n'
    '{spaces} default\n')
    return outPattern.format(spaces=' '*indent, fullName=self.fullName(),
    **self.__dict__)


    class Split:
    """Represents a single split in a transaction"""

    def __init__(self, accountDb, e):
    self.accountDb = accountDb
    self.reconciled = e.find('split:reconciled-state', nss).text == 'y'
    self.accountId = e.find('split:account', nss).text
    accountDb[self.accountId].used = True

    # Some special treatment for value and quantity
    rawValue = e.find('split:value', nss).text
    self.value = self.convertValue(rawValue)

    # Quantity is the amount on the commodity of the account
    rawQuantity = e.find('split:quantity', nss).text
    self.quantity = self.convertValue(rawQuantity)

    def getAccount(self):
    return self.accountDb[self.accountId]

    def toLedgerFormat(self, commodity='$', indent=0):
    outPattern = '{spaces} {flag}{accountName} {value}'

    # Check if commodity conversion will be needed
    conversion = ''
    if commodity == self.getAccount().commodity:
    value = '{value} {commodity}'.format(commodity=commodity,
    value=self.value)
    else:
    conversion = ' {destValue} "{destCmdty}" @@ {value} {commodity}'
    realValue = self.value[1:] if self.value.startswith('-') else self.value
    value = conversion.format(destValue=self.quantity,
    destCmdty=self.getAccount().commodity,
    value=realValue,
    commodity=commodity)

    return outPattern.format(flag='* ' if self.reconciled else '',
    spaces=indent*' ',
    accountName=self.getAccount().fullName(),
    conversion=conversion,
    value=value)

    def convertValue(self, rawValue):
    (intValue, decPoint) = rawValue.split('/')

    # Decimal points are a little annoying, since I don't want to convert
    # to numbers to avoid loosing precision
    if decPoint == '100':
    signFlag = intValue.startswith('-')
    if signFlag:
    intValue = intValue[1:]
    if len(intValue) < 3:
    intValue = '0'*(3-len(intValue)) + intValue
    if signFlag:
    intValue = '-' + intValue
    return intValue[:-2] + '.' + intValue[-2:]
    else:
    raise Exception('Do not know how to deal with other fractional '
    'values')


    class Transaction:
    def __init__(self, accountDb, e):
    self.accountDb = accountDb
    self.date = dateutil.parser.parse(e.find('trn:date-posted/ts:date',
    nss).text)
    self.commodity = e.find('trn:currency/cmdty:id', nss).text
    self.description = e.find('trn:description', nss).text
    self.splits = [Split(accountDb, s)
    for s in e.findall('trn:splits/trn:split', nss)]

    def toLedgerFormat(self, indent=0):
    outPattern = ('{spaces}{date} {description}\n'
    '{splits}\n')
    splits = '\n'.join(s.toLedgerFormat(self.commodity, indent)
    for s in self.splits)
    return outPattern.format(
    spaces=' '*indent,
    date=self.date.strftime('%Y/%m/%d'),
    description=self.description,
    splits=splits)


    def convert2Ledger(inputFile):
    """Reads a XML file and converts it to a ledger file."""

    e = xml.etree.ElementTree.parse(inputFile).getroot()
    b = e.find('gnc:book', nss)

    # Find all commodities
    commodities = []
    for cmdty in b.findall('gnc:commodity', nss):
    commodities.append(Commodity(cmdty))

    # Find all accounts
    accountDb = {}
    for acc in b.findall('gnc:account', nss):
    Account(accountDb, acc)

    # Finally, find all transactions
    transactions = []
    for xact in b.findall('gnc:transaction', nss):
    transactions.append(Transaction(accountDb, xact))

    # Generate output
    output = ''

    # First, add the commodities definition
    output = '\n'.join(c.toLedgerFormat() for c in commodities)
    output += '\n\n'

    # Then, output all accounts
    output += '\n'.join(a.toLedgerFormat()
    for a in accountDb.values() if a.used)
    output += '\n\n'

    # And finally, output all transactions
    output += '\n'.join(t.toLedgerFormat()
    for t in sorted(transactions, key=lambda x: x.date))

    return (output, commodities, accountDb, transactions)


    if __name__ == '__main__':
    if len(sys.argv) not in (2, 3):
    print('Usage: gcash2ledger.py inputXMLFile [outputLedgedFile]\n')
    print('If output is not provided, output to stdout')
    print('If output exists, it will not be overwritten.')
    exit(1)

    if len(sys.argv) == 3 and os.path.exists(sys.argv[2]):
    print('Output file exists. It will not be overwritten.')
    exit(2)

    (data, commodities, accountDb, transactions) = convert2Ledger(sys.argv[1])

    if len(sys.argv) == 3:
    with open(sys.argv[2], 'w') as fh:
    fh.write(data)
    else:
    print(data)