Changeset - f2b9decf2752
[Not reviewed]
0 6 1
Joar Wandborg - 10 years ago 2013-12-16 06:33:56
joar@wandborg.se
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
7 files changed with 180 insertions and 74 deletions:
0 comments (0 inline, 0 general)
accounting/client.py
Show inline comments
...
 
@@ -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']:
accounting/gtkclient.py
Show inline comments
 
new file 100644
 
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())
accounting/models.py
Show inline comments
...
 
@@ -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'))
accounting/storage/ledgercli.py
Show inline comments
...
 
@@ -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
accounting/storage/sql/__init__.py
Show inline comments
...
 
@@ -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,
accounting/transport.py
Show inline comments
...
 
@@ -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__')
 

	
accounting/web.py
Show inline comments
...
 
@@ -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):
0 comments (0 inline, 0 general)