diff --git a/accounting/__init__.py b/accounting/__init__.py index 17d7bb7a4bbfaa963a5da03bc0dab1684689eea3..eaf52ad422d7ddac6a9661b5f98dd24916195501 100644 --- a/accounting/__init__.py +++ b/accounting/__init__.py @@ -111,6 +111,30 @@ class Ledger: return output + def add_transaction(self, transaction): + transaction_template = ('\n{date} {t.payee}\n' + '{postings}') + + posting_template = (' {account} {p.amount.symbol}' + ' {p.amount.amount}\n') + + output = b'' + + output += transaction_template.format( + date=transaction.date.strftime('%Y-%m-%d'), + t=transaction, + postings=''.join([posting_template.format( + p=p, + account=p.account + ' ' * ( + 80 - (len(p.account) + len(p.amount.symbol) + + len(p.amount.amount) + 1 + 2) + )) for p in transaction.postings])).encode('utf8') + + with open(self.ledger_file, 'ab') as f: + f.write(output) + + _log.debug('written to file: %s', output) + def bal(self): output = self.send_command('xml') diff --git a/accounting/decorators.py b/accounting/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..6e5c1de6e8bc1d9971931872d122afb8a01b963d --- /dev/null +++ b/accounting/decorators.py @@ -0,0 +1,20 @@ +from functools import wraps + +from flask import jsonify + +from accounting.exceptions import AccountingException + + +def jsonify_exceptions(func): + ''' + Wraps a Flask endpoint and catches any AccountingException-based + exceptions which are returned to the client as JSON. + ''' + @wraps(func) + def wrapper(*args, **kw): + try: + return func(*args, **kw) + except AccountingException as exc: + return jsonify(error=exc) + + return wrapper diff --git a/accounting/exceptions.py b/accounting/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..694c6a616cd136ed5d991f07cf34b64298121c7e --- /dev/null +++ b/accounting/exceptions.py @@ -0,0 +1,6 @@ +class AccountingException(Exception): + ''' + Used as a base for exceptions that are returned to the caller via the + jsonify_exceptions decorator + ''' + pass diff --git a/accounting/transport.py b/accounting/transport.py index 091791b14d72f63808b92dc84e0adef860b85d5f..72af3e5bc4928898362dd2c28e7abe60d3da5cfe 100644 --- a/accounting/transport.py +++ b/accounting/transport.py @@ -1,3 +1,5 @@ +from datetime import datetime + from flask import json from accounting import Amount, Transaction, Posting, Account @@ -30,6 +32,11 @@ class AccountingEncoder(json.JSONEncoder): amount=o.amount, symbol=o.symbol ) + elif isinstance(o, Exception): + return dict( + __type__=o.__class__.__name__, + args=o.args + ) return json.JSONEncoder.default(self, o) @@ -46,4 +53,7 @@ class AccountingDecoder(json.JSONDecoder): _type = d.pop('__type__') + if _type == 'Transaction': + d['date'] = datetime.strptime(d['date'], '%Y-%m-%d') + return types[_type](**d) diff --git a/accounting/web.py b/accounting/web.py index 4dd0478c7d0fad60799480592ba12e27c462a9d2..af096de18c5dc68b951820f6ffa38081d8eb18eb 100644 --- a/accounting/web.py +++ b/accounting/web.py @@ -2,10 +2,12 @@ import sys import logging import argparse -from flask import Flask, g, jsonify, json, request +from flask import Flask, jsonify, request -from accounting import Ledger, Account, Posting, Transaction, Amount +from accounting import Ledger from accounting.transport import AccountingEncoder, AccountingDecoder +from accounting.exceptions import AccountingException +from accounting.decorators import jsonify_exceptions app = Flask('accounting') @@ -32,6 +34,67 @@ def balance_report(): return jsonify(balance_report=report_data) +@app.route('/transaction', methods=['POST']) +@jsonify_exceptions +def transaction(): + ''' + REST/JSON endpoint for transactions. + + Current state: + + Takes a POST request with a ``transactions`` JSON payload and writes it to + the ledger file. + + Requires the ``transactions`` payload to be __type__-annotated: + + .. code-block:: json + + { + "transactions": [ + { + "__type__": "Transaction", + "date": "2013-01-01", + "payee": "Kindly T. Donor", + "postings": [ + { + "__type__": "Posting", + "account": "Income:Foo:Donation", + "amount": { + "__type__": "Amount", + "amount": "-100", + "symbol": "$" + } + }, + { + "__type__": "Posting", + "account": "Assets:Checking", + "amount": { + "__type__": "Amount", + "amount": "100", + "symbol": "$" + } + } + ] + } + } + + becomes:: + + 2013-01-01 Kindly T. Donor + Income:Foo:Donation $ -100 + Assets:Checking $ 100 + ''' + transactions = request.json.get('transactions') + + if not transactions: + raise AccountingException('No transaction data provided') + + for transaction in transactions: + ledger.add_transaction(transaction) + + return jsonify(foo='bar') + + @app.route('/parse-json', methods=['POST']) def parse_json(): r'''