- Made the storage model slightly more flexible
- Made a small P-o-C GUI application in GTK
- Polished accounting.client
- models.Transaction.id is now a str
- Fixed transaction.id marshalling for storage.ledgercli
This commit is contained in:
Joar Wandborg 2013-12-16 07:33:56 +01:00
parent 124bd1706d
commit f2b9decf27
7 changed files with 180 additions and 74 deletions

View file

@ -2,6 +2,7 @@ import sys
import argparse
import json
import logging
import locale
from datetime import datetime
from decimal import Decimal
@ -11,6 +12,8 @@ import requests
from accounting.models import Transaction, Posting, Amount
from accounting.transport import AccountingDecoder, AccountingEncoder
locale.setlocale(locale.LC_ALL, '')
_log = logging.getLogger(__name__)
@ -43,15 +46,20 @@ class Client:
return self._decode_response(requests.post(self.host + path, **kw))
def simple_transaction(self, from_acc, to_acc, amount):
def simple_transaction(self, from_acc, to_acc, amount, symbol=None,
payee=None):
if symbol is None:
# Get the currency from the environment locale
symbol = locale.localeconv()['int_curr_symbol'].strip()
t = Transaction(
date=datetime.today(),
payee='PayPal donation',
payee=payee,
postings=[
Posting(account=from_acc,
amount=Amount(symbol='$', amount=-amount)),
amount=Amount(symbol=symbol, amount=-amount)),
Posting(account=to_acc,
amount=Amount(symbol='$', amount=amount))
amount=Amount(symbol=symbol, amount=amount))
]
)
@ -86,7 +94,7 @@ def print_balance_accounts(accounts, level=0):
print(' ' * level + ' {amount.symbol} {amount.amount}'.format(
amount=amount))
print_balance_accounts(account.accounts, level+1)
print_balance_accounts(account.accounts, level + 1)
def main(argv=None, prog=None):
@ -100,9 +108,16 @@ def main(argv=None, prog=None):
insert = actions.add_parser('insert',
aliases=['in'])
insert.add_argument('payee',
help='The payee line of the transaction')
insert.add_argument('from_account')
insert.add_argument('to_account')
insert.add_argument('amount', type=Decimal)
insert.add_argument('amount', type=Decimal,
help='The amount deducted from from_account and added'
' to to_account')
insert.add_argument('-s', '--symbol',
help='The symbol for the amount, e.g. $ or USD for'
' USD. Defaults to your locale\'s setting.')
actions.add_parser('balance', aliases=['bal'])
@ -122,7 +137,8 @@ def main(argv=None, prog=None):
if args.action in ['insert', 'in']:
print(client.simple_transaction(args.from_account, args.to_account,
args.amount))
args.amount, payee=args.payee,
symbol=args.symbol))
elif args.action in ['balance', 'bal']:
print_balance_accounts(client.get_balance())
elif args.action in ['register', 'reg']:

93
accounting/gtkclient.py Normal file
View file

@ -0,0 +1,93 @@
import sys
import logging
import threading
from datetime import datetime
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import GObject
from accounting.client import Client
_log = logging.getLogger(__name__)
class Accounting(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title='Accounting Client')
self.client = Client()
self.set_border_width(3)
# Table
self.table = Gtk.Table(3, 2, True)
self.add(self.table)
# Button
self.btn_load_transactions = Gtk.Button(label='Load transactions')
self.btn_load_transactions.connect('clicked', self.on_button_clicked)
self.spinner = Gtk.Spinner()
# Transaction stuff
self.transaction_store = Gtk.ListStore(str, str)
self.transaction_view = Gtk.TreeView(self.transaction_store)
renderer = Gtk.CellRendererText()
date_column = Gtk.TreeViewColumn('Date', renderer, text=0)
payee_column = Gtk.TreeViewColumn('Payee', renderer, text=1)
self.transaction_view.append_column(date_column)
self.transaction_view.append_column(payee_column)
# Layout
self.table.attach(self.btn_load_transactions, 0, 1, 0, 1)
self.table.attach(self.spinner, 1, 2, 0, 1)
self.table.attach(self.transaction_view, 0, 2, 1, 3)
# Show
self.show_all()
self.spinner.hide()
def on_button_clicked(self, widget):
def load_transactions():
transactions = self.client.get_register()
GLib.idle_add(self.on_transactions_loaded, transactions)
self.spinner.show()
self.spinner.start()
threading.Thread(target=load_transactions).start()
def on_transactions_loaded(self, transactions):
self.spinner.stop()
self.spinner.hide()
_log.debug('transactions: %s', transactions)
self.transaction_store.clear()
for transaction in transactions:
self.transaction_store.append([
transaction.date.strftime('%Y-%m-%d'),
transaction.payee
])
def main(argv=None):
logging.basicConfig(level=logging.DEBUG)
GObject.threads_init()
accounting_win = Accounting()
accounting_win.connect('delete-event', Gtk.main_quit)
Gtk.main()
if __name__ == '__main__':
sys.exit(main())

View file

@ -15,10 +15,10 @@ class Transaction:
self.generate_id()
def generate_id(self):
self.id = uuid.uuid4()
self.id = str(uuid.uuid4())
def __repr__(self):
return ('<{self.__class__.__name__} {date}' +
return ('<{self.__class__.__name__} {self.id} {date}' +
' {self.payee} {self.postings}').format(
self=self,
date=self.date.strftime('%Y-%m-%d'))

View file

@ -14,7 +14,10 @@ _log = logging.getLogger(__name__)
class Ledger(Storage):
def __init__(self, ledger_file=None, ledger_bin=None):
def __init__(self, app=None, ledger_file=None, ledger_bin=None):
if app:
ledger_file = app.config['LEDGER_FILE']
if ledger_file is None:
raise ValueError('ledger_file cannot be None')
@ -158,11 +161,14 @@ class Ledger(Storage):
:class:`~accounting.models.Transaction` instance in
:data:`transaction`.
'''
if not transaction.metadata.get('Id'):
if transaction.id is None:
_log.debug('No ID found. Generating an ID.')
transaction.generate_id()
transaction.metadata.update({'Id': transaction.id})
transaction_template = ('\n{date} {t.payee}\n'
'{tags}'
'{metadata}'
'{postings}')
metadata_template = ' ;{0}: {1}\n'
@ -178,7 +184,7 @@ class Ledger(Storage):
output += transaction_template.format(
date=transaction.date.strftime('%Y-%m-%d'),
t=transaction,
tags=''.join([
metadata=''.join([
metadata_template.format(k, v)
for k, v in transaction.metadata.items()]),
postings=''.join([posting_template.format(
@ -239,6 +245,9 @@ class Ledger(Storage):
return accounts
def get_transactions(self):
return self.reg()
def reg(self):
output = self.send_command('xml')
@ -301,6 +310,9 @@ class Ledger(Storage):
return entries
def update_transaction(self, transaction):
_log.debug('DUMMY: Updated transaction: %s', transaction)
def main(argv=None):
import argparse

View file

@ -3,6 +3,7 @@ import json
from flask.ext.sqlalchemy import SQLAlchemy
from accounting.exceptions import AccountingException
from accounting.storage import Storage
from accounting.models import Transaction, Posting, Amount
@ -11,8 +12,12 @@ db = None
class SQLStorage(Storage):
def __init__(self, app):
def __init__(self, app=None):
global db
if not app:
raise Exception('Missing app keyword argument')
self.app = app
db = self.db = SQLAlchemy(app)
@ -47,12 +52,19 @@ class SQLStorage(Storage):
return transactions
def update_transaction(self, transaction):
if transaction.id is None:
raise AccountingException('The transaction id must be set for'
' update_transaction calls')
_log.debug('DUMMY: Update transaction: %s', transaction)
def add_transaction(self, transaction):
if transaction.id is None:
transaction.generate_id()
_t = self.Transaction()
_t.uuid = str(transaction.id)
_t.uuid = transaction.id
_t.date = transaction.date
_t.payee = transaction.payee
_t.meta = json.dumps(transaction.metadata)
@ -61,7 +73,7 @@ class SQLStorage(Storage):
for posting in transaction.postings:
_p = self.Posting()
_p.transaction_uuid = str(transaction.id)
_p.transaction_uuid = transaction.id
_p.account = posting.account
_p.meta = json.dumps(posting.metadata)
_p.amount = self.Amount(symbol=posting.amount.symbol,

View file

@ -4,6 +4,7 @@ from flask import json
from accounting.models import Amount, Transaction, Posting, Account
class AccountingEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Account):
@ -16,6 +17,7 @@ class AccountingEncoder(json.JSONEncoder):
elif isinstance(o, Transaction):
return dict(
__type__=o.__class__.__name__,
id=o.id,
date=o.date.strftime('%Y-%m-%d'),
payee=o.payee,
postings=o.postings,
@ -42,6 +44,7 @@ class AccountingEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o)
class AccountingDecoder(json.JSONDecoder):
def __init__(self):
json.JSONDecoder.__init__(self, object_hook=self.dict_to_object)
@ -50,8 +53,8 @@ class AccountingDecoder(json.JSONDecoder):
if '__type__' not in d:
return d
types = {c.__name__ : c for c in [Amount, Transaction, Posting,
Account]}
types = {c.__name__: c for c in [Amount, Transaction, Posting,
Account]}
_type = d.pop('__type__')

View file

@ -21,14 +21,15 @@ from accounting.decorators import jsonify_exceptions
app = Flask('accounting')
app.config.from_pyfile('config.py')
storage = SQLStorage(app)
storage = Ledger(app=app)
# TODO: Move migration stuff into SQLStorage
db = storage.db
migrate = Migrate(app, db)
if isinstance(storage, SQLStorage):
# TODO: Move migration stuff into SQLStorage
db = storage.db
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
@app.before_request
@ -59,6 +60,24 @@ def transaction_get():
'''
return jsonify(transactions=storage.get_transactions())
@app.route('/transaction/<string:transaction_id>', methods=['POST'])
@jsonify_exceptions
def transaction_update(transaction_id=None):
if transaction_id is None:
raise AccountingException('The transaction ID cannot be None.')
transaction = request.json['transaction']
if transaction.id is not None and not transaction.id == transaction_id:
raise AccountingException('The transaction data has an ID attribute and'
' it is not the same ID as in the path')
elif transaction.id is None:
transaction.id = transaction_id
storage.update_transaction(transaction)
return jsonify(status='OK')
@app.route('/transaction', methods=['POST'])
@jsonify_exceptions
@ -118,56 +137,7 @@ def transaction_post():
for transaction in transactions:
storage.add_transaction(transaction)
return jsonify(foo='bar')
@app.route('/parse-json', methods=['POST'])
def parse_json():
r'''
Parses a __type__-annotated JSON payload and debug-logs the decoded version
of it.
Example:
.. code-block:: bash
wget http://127.0.0.1:5000/balance -O balance.json
curl -X POST -H 'Content-Type: application/json' -d @balance.json \
http://127.0.0.1/parse-json
# Logging output (linebreaks added for clarity)
# DEBUG:accounting:json data: {'balance_report':
# [<Account "None" [
# <Amount $ 0>, <Amount KARMA 0>]
# [<Account "Assets" [
# <Amount $ 50>, <Amount KARMA 10>]
# [<Account "Assets:Checking" [
# <Amount $ 50>] []>,
# <Account "Assets:Karma Account" [
# <Amount KARMA 10>] []>]>,
# <Account "Expenses" [
# <Amount $ 500>]
# [<Account "Expenses:Blah" [
# <Amount $ 250>]
# [<Account "Expenses:Blah:Hosting" [
# <Amount $ 250>] []>]>,
# <Account "Expenses:Foo" [
# <Amount $ 250>] [
# <Account "Expenses:Foo:Hosting" [
# <Amount $ 250>] []>]>]>,
# <Account "Income" [
# <Amount $ -550>,
# <Amount KARMA -10>]
# [<Account "Income:Donation" [
# <Amount $ -50>] []>,
# <Account "Income:Foo" [
# <Amount $ -500>]
# [<Account "Income:Foo:Donation" [
# <Amount $ -500>] []>]>,
# <Account "Income:Karma" [
# <Amount KARMA -10>] []>]>]>]}
'''
app.logger.debug('json data: %s', request.json)
return jsonify(foo='bar')
return jsonify(status='OK')
def main(argv=None):