diff --git a/accounting/__init__.py b/accounting/__init__.py index eaf52ad..d9cbaa2 100644 --- a/accounting/__init__.py +++ b/accounting/__init__.py @@ -7,6 +7,8 @@ from datetime import datetime from xml.etree import ElementTree from contextlib import contextmanager +from accounting.models import Account, Transaction, Posting, Amount + _log = logging.getLogger(__name__) class Ledger: @@ -112,6 +114,11 @@ class Ledger: return output def add_transaction(self, transaction): + ''' + Writes a transaction to the ledger file by opening it in 'ab' mode and + writing a ledger transaction based on the Transaction instance in + ``transaction``. + ''' transaction_template = ('\n{date} {t.payee}\n' '{postings}') @@ -127,7 +134,7 @@ class Ledger: p=p, account=p.account + ' ' * ( 80 - (len(p.account) + len(p.amount.symbol) + - len(p.amount.amount) + 1 + 2) + len(str(p.amount.amount)) + 1 + 2) )) for p in transaction.postings])).encode('utf8') with open(self.ledger_file, 'ab') as f: @@ -212,50 +219,6 @@ class Ledger: 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.strftime('%Y-%m-%d')) - - -class Posting: - def __init__(self, account=None, amount=None): - self.account = account - self.amount = amount - - def __repr__(self): - return ('<{self.__class__.__name__} "{self.account}"' + - ' {self.amount}>').format(self=self) - - -class Amount: - def __init__(self, amount=None, symbol=None): - self.amount = amount - self.symbol = symbol - - def __repr__(self): - return ('<{self.__class__.__name__} {self.symbol}' + - ' {self.amount}>').format(self=self) - - -class Account: - def __init__(self, name=None, amounts=None, accounts=None): - self.name = name - self.amounts = amounts - self.accounts = accounts - - def __repr__(self): - return ('<{self.__class__.__name__} "{self.name}" {self.amounts}' + - ' {self.accounts}>').format(self=self) - - def main(argv=None): import argparse if argv is None: diff --git a/accounting/client.py b/accounting/client.py new file mode 100644 index 0000000..ef5d62b --- /dev/null +++ b/accounting/client.py @@ -0,0 +1,83 @@ +import sys +import argparse +import json +import logging + +from datetime import datetime +from decimal import Decimal + +import requests + +from accounting.models import Transaction, Posting, Amount +from accounting.transport import AccountingDecoder, AccountingEncoder + +# TODO: Client should be a class + +HOST = None + + +def insert_paypal_transaction(amount): + t = Transaction( + date=datetime.today(), + payee='PayPal donation', + postings=[ + Posting(account='Income:Donations:PayPal', + amount=Amount(symbol='$', amount=-amount)), + Posting(account='Assets:Checking', + amount=Amount(symbol='$', amount=amount)) + ] + ) + + response = requests.post(HOST + '/transaction', + headers={'Content-Type': 'application/json'}, + data=json.dumps({'transactions': [t]}, + cls=AccountingEncoder)) + + print(response.json(cls=AccountingDecoder)) + + +def get_balance(): + response = requests.get(HOST + '/balance') + + balance = response.json(cls=AccountingDecoder) + + _recurse_accounts(balance['balance_report']) + + +def _recurse_accounts(accounts, level=0): + for account in accounts: + print(' ' * level + ' + {account.name}'.format(account=account) + + ' ' + '-' * (80 - len(str(account.name)) - level)) + for amount in account.amounts: + print(' ' * level + ' {amount.symbol} {amount.amount}'.format( + amount=amount)) + _recurse_accounts(account.accounts, level+1) + + +def main(argv=None, prog=None): + global HOST + if argv is None: + prog = sys.argv[0] + argv = sys.argv[1:] + + parser = argparse.ArgumentParser(prog=prog) + parser.add_argument('-p', '--paypal', type=Decimal) + parser.add_argument('-v', '--verbosity', + default='WARNING', + help=('Filter logging output. Possible values:' + + ' CRITICAL, ERROR, WARNING, INFO, DEBUG')) + parser.add_argument('-b', '--balance', action='store_true') + parser.add_argument('--host', default='http://localhost:5000') + args = parser.parse_args(argv) + + HOST = args.host + + logging.basicConfig(level=getattr(logging, args.verbosity)) + + if args.paypal: + insert_paypal_transaction(args.paypal) + elif args.balance: + get_balance() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/accounting/models.py b/accounting/models.py new file mode 100644 index 0000000..f4aa9f2 --- /dev/null +++ b/accounting/models.py @@ -0,0 +1,45 @@ +from decimal import Decimal + + +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.strftime('%Y-%m-%d')) + + +class Posting: + def __init__(self, account=None, amount=None): + self.account = account + self.amount = amount + + def __repr__(self): + return ('<{self.__class__.__name__} "{self.account}"' + + ' {self.amount}>').format(self=self) + + +class Amount: + def __init__(self, amount=None, symbol=None): + self.amount = Decimal(amount) + self.symbol = symbol + + def __repr__(self): + return ('<{self.__class__.__name__} {self.symbol}' + + ' {self.amount}>').format(self=self) + + +class Account: + def __init__(self, name=None, amounts=None, accounts=None): + self.name = name + self.amounts = amounts + self.accounts = accounts + + def __repr__(self): + return ('<{self.__class__.__name__} "{self.name}" {self.amounts}' + + ' {self.accounts}>').format(self=self) diff --git a/accounting/transport.py b/accounting/transport.py index 72af3e5..0ddb5c1 100644 --- a/accounting/transport.py +++ b/accounting/transport.py @@ -2,7 +2,7 @@ from datetime import datetime from flask import json -from accounting import Amount, Transaction, Posting, Account +from accounting.models import Amount, Transaction, Posting, Account class AccountingEncoder(json.JSONEncoder): def default(self, o): @@ -29,7 +29,7 @@ class AccountingEncoder(json.JSONEncoder): elif isinstance(o, Amount): return dict( __type__=o.__class__.__name__, - amount=o.amount, + amount=str(o.amount), symbol=o.symbol ) elif isinstance(o, Exception): diff --git a/bin/client b/bin/client new file mode 100755 index 0000000..bbf097b --- /dev/null +++ b/bin/client @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import sys + +sys.path.append('./') + +from accounting.client import main + +if __name__ == '__main__': + sys.exit(main())