SQL, GTK
- 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:
parent
124bd1706d
commit
f2b9decf27
7 changed files with 180 additions and 74 deletions
|
@ -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
93
accounting/gtkclient.py
Normal 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())
|
|
@ -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'))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,7 +53,7 @@ class AccountingDecoder(json.JSONDecoder):
|
|||
if '__type__' not in d:
|
||||
return d
|
||||
|
||||
types = {c.__name__ : c for c in [Amount, Transaction, Posting,
|
||||
types = {c.__name__: c for c in [Amount, Transaction, Posting,
|
||||
Account]}
|
||||
|
||||
_type = d.pop('__type__')
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue