Added client, other fixes
- Switched from int to decimal.Decimal - Moved models to accounting.models
This commit is contained in:
parent
6f2c875c7b
commit
9656c39e91
5 changed files with 147 additions and 47 deletions
|
@ -7,6 +7,8 @@ from datetime import datetime
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from accounting.models import Account, Transaction, Posting, Amount
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Ledger:
|
class Ledger:
|
||||||
|
@ -112,6 +114,11 @@ class Ledger:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def add_transaction(self, transaction):
|
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'
|
transaction_template = ('\n{date} {t.payee}\n'
|
||||||
'{postings}')
|
'{postings}')
|
||||||
|
|
||||||
|
@ -127,7 +134,7 @@ class Ledger:
|
||||||
p=p,
|
p=p,
|
||||||
account=p.account + ' ' * (
|
account=p.account + ' ' * (
|
||||||
80 - (len(p.account) + len(p.amount.symbol) +
|
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')
|
)) for p in transaction.postings])).encode('utf8')
|
||||||
|
|
||||||
with open(self.ledger_file, 'ab') as f:
|
with open(self.ledger_file, 'ab') as f:
|
||||||
|
@ -212,50 +219,6 @@ class Ledger:
|
||||||
return entries
|
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):
|
def main(argv=None):
|
||||||
import argparse
|
import argparse
|
||||||
if argv is None:
|
if argv is None:
|
||||||
|
|
83
accounting/client.py
Normal file
83
accounting/client.py
Normal file
|
@ -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())
|
45
accounting/models.py
Normal file
45
accounting/models.py
Normal file
|
@ -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)
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
||||||
|
|
||||||
from flask import json
|
from flask import json
|
||||||
|
|
||||||
from accounting import Amount, Transaction, Posting, Account
|
from accounting.models import Amount, Transaction, Posting, Account
|
||||||
|
|
||||||
class AccountingEncoder(json.JSONEncoder):
|
class AccountingEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
|
@ -29,7 +29,7 @@ class AccountingEncoder(json.JSONEncoder):
|
||||||
elif isinstance(o, Amount):
|
elif isinstance(o, Amount):
|
||||||
return dict(
|
return dict(
|
||||||
__type__=o.__class__.__name__,
|
__type__=o.__class__.__name__,
|
||||||
amount=o.amount,
|
amount=str(o.amount),
|
||||||
symbol=o.symbol
|
symbol=o.symbol
|
||||||
)
|
)
|
||||||
elif isinstance(o, Exception):
|
elif isinstance(o, Exception):
|
||||||
|
|
9
bin/client
Executable file
9
bin/client
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('./')
|
||||||
|
|
||||||
|
from accounting.client import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
Loading…
Reference in a new issue