Initial commit

This commit is contained in:
Joar Wandborg 2013-12-09 17:54:00 +01:00
commit 222ef8b421
2 changed files with 233 additions and 0 deletions

201
accounting/__init__.py Normal file
View file

@ -0,0 +1,201 @@
import subprocess
import logging
import time
from datetime import datetime
from xml.etree import ElementTree
from contextlib import contextmanager
_log = logging.getLogger(__name__)
class Ledger:
def __init__(self, ledger_file=None, ledger_bin=None):
if ledger_file is None:
raise ValueError('ledger_file cannot be None')
self.ledger_bin = ledger_bin or 'ledger'
self.ledger_file = ledger_file
_log.info('ledger file: %s', ledger_file)
self.locked = False
self.ledger_process = None
@contextmanager
def locked_process(self):
if self.locked:
_log.warning('Process is already locked')
for i in range(1, 5):
if i > 4:
raise RuntimeError('Ledger process is already locked')
if not self.locked:
break
else:
_log.info('Waiting for one second... %d/%d', i, 5)
time.sleep(1)
process = self.get_process()
self.locked = True
_log.debug('lock enabled')
yield process
self.locked = False
_log.debug('lock disabled')
def assemble_arguments(self):
return [
self.ledger_bin,
'-f',
self.ledger_file,
]
def init_process(self):
_log.debug('starting ledger process')
self.ledger_process = subprocess.Popen(
self.assemble_arguments(),
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
# Swallow the banner
with self.locked_process() as p:
self.read_until_prompt(p)
return self.ledger_process
def get_process(self):
return self.ledger_process or self.init_process()
def read_until_prompt(self, p):
output = b''
while True:
# _log.debug('reading data')
line = p.stdout.read(1) # XXX: This is a hack
# _log.debug('line: %s', line)
output += line
if b'\n] ' in output:
_log.debug('found prompt!')
break
output = output[:-3] # Cut away the prompt
_log.debug('output: %s', output)
return output
def send_command(self, p, command):
# TODO: Should be extended to handle the locking and return the output
_bytes = p.stdin.write(command + b'\n')
p.stdin.flush()
return _bytes
def bal(self):
output = None
with self.locked_process() as p:
_log.debug('aquired process lock')
self.send_command(p, b'bal --format "%A|%t\\\\n"')
_log.debug('sent command')
output = self.read_until_prompt(p)
if output is None:
raise RuntimeError('bal call returned no output')
accounts = []
for line in output.split(b'\n'):
name, balance = line.decode('utf8').split('|')
accounts.append(Account(name=name, balance=balance))
return accounts
def reg(self):
output = None
with self.locked_process() as p:
_log.debug('aquired process lock')
self.send_command(p, b'xml')
output = self.read_until_prompt(p)
if output is None:
raise RuntimeError('reg call returned no output')
entries = []
reg_xml = ElementTree.fromstring(output.decode('utf8'))
for transaction in reg_xml.findall('./transactions/transaction'):
date = datetime.strptime(transaction.find('./date').text,
'%Y/%m/%d')
payee = transaction.find('./payee').text
postings = []
for posting in transaction.findall('./postings/posting'):
account = posting.find('./account/name').text
amount = posting.find('./post-amount/amount/quantity').text
symbol = posting.find(
'./post-amount/amount/commodity/symbol').text
postings.append(
Posting(account=account, amount=amount, symbol=symbol))
entries.append(
Transaction(date=date, payee=payee, postings=postings))
return entries
class Transaction:
def __init__(self, date=None, payee=None, postings=None):
self.date = date
self.payee = payee
self.postings = postings
def __repr__(self):
return ('<{self.__class__.__name__} {date}' +
' {self.payee} {self.postings}').format(
self=self,
date=self.date.isoformat())
class Posting:
def __init__(self, account=None, amount=None, symbol=None):
self.account = account
self.amount = amount
self.symbol = symbol
def __repr__(self):
return ('<{self.__class__.__name__} "{self.account}"' +
' {self.symbol} {self.amount}>').format(self=self)
class Account:
def __init__(self, name=None, balance=None):
self.name = name
self.balance = balance
def __repr__(self):
return '<{self.__class__.__name__}: "{self.name}" {self.balance} >'.format(
self=self)
def main():
ledger = Ledger(ledger_file='non-profit-test-data.ledger')
print(ledger.bal())
print(ledger.reg())
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
main()

View file

@ -0,0 +1,32 @@
2010/01/01 Kindly T. Donor
Income:Foo:Donation $-100.00
;Invoice: Projects/Foo/Invoices/Invoice20100101.pdf
Assets:Checking $100.00
2011/03/15 Another J. Donor
Income:Foo:Donation $-400.00
;Approval: Projects/Foo/earmark-record.txt
Assets:Checking $400.00
2011/04/20 (1) Baz Hosting Services, LLC
Expenses:Foo:Hosting $250.00
;Receipt: Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf
Assets:Checking $-250.00
2011/05/10 Donation to General Fund
Income:Donation $-50.00
;Invoice: Financial/Invoices/Invoice20110510.pdf
Assets:Checking $50.00
2011/04/20 (2) Baz Hosting Services, LLC
Expenses:Blah:Hosting $250.00
;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf
;Invoice: Projects/Blah/Expenses/hosting/april-invoice.pdf
Assets:Checking $-250.00
;Statement: Financial/BankStuff/bank-statement.pdf
2011-04-25 A transaction with ISO date
Income:Karma KARMA-10
Assets:Karma Account KARMA 10