You've already forked double-entry-accounting
fix: Initial barebones UI and API running
This commit is contained in:
@@ -1,161 +1,416 @@
|
||||
import cherrypy
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from datetime import datetime
|
||||
from accounting.models import Account, BankAccount, BankTransaction, JournalEntry, JournalEntryLine, ReconciliationReport, \
|
||||
from accounting.models import Account, BankAccount, BankTransaction, JournalEntry, JournalEntryLine, \
|
||||
ReconciliationReport, \
|
||||
ReconciliationMatch
|
||||
import simplejson as json
|
||||
|
||||
|
||||
@cherrypy.tools.json_out()
|
||||
class AccountingAPI:
|
||||
exposed = True
|
||||
|
||||
def __init__(self, db_engine):
|
||||
self.db_engine = db_engine
|
||||
Session = sessionmaker(bind=db_engine)
|
||||
self.session = Session()
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def index(self):
|
||||
# Mount resource handlers as attributes - these become URL segments
|
||||
self.accounts = AccountHandler(self.session)
|
||||
self.bank_accounts = BankAccountHandler(self.session)
|
||||
self.journal_entries = JournalEntryHandler(self.session)
|
||||
|
||||
def GET(self):
|
||||
"""Handle GET requests to the API root"""
|
||||
return {"status": "success", "message": "Accounting API is running"}
|
||||
|
||||
# Bank Accounts Endpoints
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def bank_accounts(self):
|
||||
if cherrypy.request.method != 'GET':
|
||||
raise cherrypy.HTTPError(405)
|
||||
def OPTIONS(self):
|
||||
"""Handle OPTIONS requests (CORS preflight) to the API root"""
|
||||
cherrypy.response.headers['Allow'] = 'GET, OPTIONS'
|
||||
return ""
|
||||
|
||||
accounts = self.session.query(BankAccount).all()
|
||||
return [{
|
||||
'id': a.id,
|
||||
'name': a.name,
|
||||
'bank_name': a.bank_name,
|
||||
'account_number': a.account_number,
|
||||
'currency': a.currency
|
||||
} for a in accounts]
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_in()
|
||||
@cherrypy.tools.json_out()
|
||||
def add_bank_account(self):
|
||||
if cherrypy.request.method != 'POST':
|
||||
raise cherrypy.HTTPError(405)
|
||||
@cherrypy.tools.json_out()
|
||||
class AccountHandler:
|
||||
exposed = True
|
||||
|
||||
data = cherrypy.request.json
|
||||
account = BankAccount(
|
||||
name=data['name'],
|
||||
bank_name=data['bank_name'],
|
||||
account_number=data['account_number'],
|
||||
currency=data.get('currency', 'USD')
|
||||
)
|
||||
self.session.add(account)
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': account.id}
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
# Add OPTIONS method handler for CORS preflight
|
||||
@cherrypy.expose
|
||||
def add_bank_account_options(self):
|
||||
cherrypy.response.headers['Allow'] = 'POST, OPTIONS'
|
||||
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
||||
return ''
|
||||
def GET(self, account_id=None):
|
||||
"""
|
||||
GET /api/accounts - List all accounts
|
||||
GET /api/accounts/123 - Get specific account
|
||||
"""
|
||||
if account_id is not None:
|
||||
# Get specific account
|
||||
account = self.session.query(Account).get(account_id)
|
||||
if not account:
|
||||
raise cherrypy.HTTPError(404, "Account not found")
|
||||
|
||||
# Accounts Endpoints
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def accounts(self):
|
||||
if cherrypy.request.method != 'GET':
|
||||
raise cherrypy.HTTPError(405)
|
||||
return {
|
||||
'id': account.id,
|
||||
'name': account.name,
|
||||
'code': account.code,
|
||||
'type': account.account_type,
|
||||
'balance': float(account.balance) if account.balance else 0
|
||||
}
|
||||
else:
|
||||
# List all accounts
|
||||
accounts = self.session.query(Account).all()
|
||||
return [{
|
||||
'id': a.id,
|
||||
'name': a.name,
|
||||
'code': a.code,
|
||||
'type': a.account_type,
|
||||
'balance': float(a.balance) if a.balance else 0
|
||||
} for a in accounts]
|
||||
|
||||
accounts = self.session.query(Account).all()
|
||||
return [{
|
||||
'id': a.id,
|
||||
'name': a.name,
|
||||
'code': a.code,
|
||||
'type': a.account_type,
|
||||
'balance': float(a.balance) if a.balance else 0
|
||||
} for a in accounts]
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_in()
|
||||
@cherrypy.tools.json_out()
|
||||
def add_account(self):
|
||||
if cherrypy.request.method != 'POST':
|
||||
raise cherrypy.HTTPError(405)
|
||||
|
||||
data = cherrypy.request.json
|
||||
account = Account(
|
||||
name=data['name'],
|
||||
account_type=data['type'],
|
||||
code=data['code'],
|
||||
parent_id=data.get('parent_id')
|
||||
)
|
||||
self.session.add(account)
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': account.id}
|
||||
|
||||
# Journal Entries Endpoints
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_in()
|
||||
@cherrypy.tools.json_out()
|
||||
def add_journal_entry(self):
|
||||
if cherrypy.request.method != 'POST':
|
||||
raise cherrypy.HTTPError(405)
|
||||
|
||||
data = cherrypy.request.json
|
||||
total_debit = sum(line['amount'] for line in data['lines'] if line['is_debit'])
|
||||
total_credit = sum(line['amount'] for line in data['lines'] if not line['is_debit'])
|
||||
|
||||
if abs(total_debit - total_credit) > 0.01:
|
||||
return {'status': 'error', 'message': 'Debits and credits must balance'}
|
||||
|
||||
entry = JournalEntry(
|
||||
date=datetime.strptime(data['date'], '%Y-%m-%d').date(),
|
||||
reference=data.get('reference', ''),
|
||||
description=data.get('description', '')
|
||||
)
|
||||
self.session.add(entry)
|
||||
|
||||
for line_data in data['lines']:
|
||||
line = JournalEntryLine(
|
||||
journal_entry=entry,
|
||||
account_id=line_data['account_id'],
|
||||
amount=line_data['amount'],
|
||||
is_debit=line_data['is_debit']
|
||||
def POST(self):
|
||||
"""Handle POST to create a new account"""
|
||||
try:
|
||||
data = cherrypy.request.json
|
||||
account = Account(
|
||||
name=data['name'],
|
||||
account_type=data['type'],
|
||||
code=data['code'],
|
||||
parent_id=data.get('parent_id')
|
||||
)
|
||||
self.session.add(line)
|
||||
self.session.add(account)
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': account.id}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
account = self.session.query(Account).get(line_data['account_id'])
|
||||
if line_data['is_debit']:
|
||||
account.balance += line_data['amount']
|
||||
else:
|
||||
account.balance -= line_data['amount']
|
||||
def PUT(self, account_id):
|
||||
"""Handle PUT to update an existing account"""
|
||||
try:
|
||||
account = self.session.query(Account).get(account_id)
|
||||
if not account:
|
||||
raise cherrypy.HTTPError(404, "Account not found")
|
||||
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': entry.id}
|
||||
data = cherrypy.request.json
|
||||
if 'name' in data:
|
||||
account.name = data['name']
|
||||
if 'type' in data:
|
||||
account.account_type = data['type']
|
||||
if 'code' in data:
|
||||
account.code = data['code']
|
||||
if 'parent_id' in data:
|
||||
account.parent_id = data['parent_id']
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def journal_entries(self):
|
||||
if cherrypy.request.method != 'GET':
|
||||
raise cherrypy.HTTPError(405)
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': account.id}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
entries = self.session.query(JournalEntry).all()
|
||||
return [{
|
||||
'id': e.id,
|
||||
'date': e.date.isoformat(),
|
||||
'description': e.description,
|
||||
'reference': e.reference,
|
||||
'lines': [{
|
||||
'account_id': l.account_id,
|
||||
'amount': float(l.amount),
|
||||
'is_debit': l.is_debit
|
||||
} for l in e.lines]
|
||||
} for e in entries]
|
||||
def DELETE(self, account_id):
|
||||
"""Handle DELETE to remove an account"""
|
||||
try:
|
||||
account = self.session.query(Account).get(account_id)
|
||||
if not account:
|
||||
raise cherrypy.HTTPError(404, "Account not found")
|
||||
|
||||
# Add default OPTIONS handler for all endpoints
|
||||
@cherrypy.expose
|
||||
def default(self, *args, **kwargs):
|
||||
if cherrypy.request.method == 'OPTIONS':
|
||||
# Check for dependencies before deleting
|
||||
if self.session.query(JournalEntryLine).filter_by(account_id=account_id).count() > 0:
|
||||
return {'status': 'error', 'message': 'Cannot delete account with journal entries'}
|
||||
|
||||
self.session.delete(account)
|
||||
self.session.commit()
|
||||
return {'status': 'success'}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
def OPTIONS(self, account_id=None):
|
||||
"""Handle OPTIONS requests (CORS preflight)"""
|
||||
if account_id is not None:
|
||||
cherrypy.response.headers['Allow'] = 'GET, PUT, DELETE, OPTIONS'
|
||||
else:
|
||||
cherrypy.response.headers['Allow'] = 'GET, POST, OPTIONS'
|
||||
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
||||
return ''
|
||||
raise cherrypy.HTTPError(404)
|
||||
return ""
|
||||
|
||||
|
||||
@cherrypy.tools.json_out()
|
||||
class BankAccountHandler:
|
||||
exposed = True
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
# Sub-resource for transactions
|
||||
self.transactions = BankTransactionHandler(session)
|
||||
|
||||
def GET(self, bank_account_id=None):
|
||||
"""
|
||||
GET /api/bank_accounts - List all bank accounts
|
||||
GET /api/bank_accounts/123 - Get specific bank account
|
||||
"""
|
||||
if bank_account_id is not None:
|
||||
# Get specific bank account
|
||||
account = self.session.query(BankAccount).get(bank_account_id)
|
||||
if not account:
|
||||
raise cherrypy.HTTPError(404, "Bank account not found")
|
||||
data = {
|
||||
'id': account.id,
|
||||
'name': account.name,
|
||||
'bank_name': account.bank_name,
|
||||
'account_number': account.account_number,
|
||||
'currency': account.currency
|
||||
}
|
||||
return data
|
||||
else:
|
||||
# List all bank accounts
|
||||
accounts = self.session.query(BankAccount).all()
|
||||
data = [{
|
||||
'id': a.id,
|
||||
'name': a.name,
|
||||
'bank_name': a.bank_name,
|
||||
'account_number': a.account_number,
|
||||
'currency': a.currency
|
||||
} for a in accounts]
|
||||
return data
|
||||
|
||||
def POST(self):
|
||||
"""Handle POST to create a new bank account"""
|
||||
try:
|
||||
data = cherrypy.request.json
|
||||
account = BankAccount(
|
||||
name=data['name'],
|
||||
bank_name=data['bank_name'],
|
||||
account_number=data['account_number'],
|
||||
currency=data.get('currency', 'USD')
|
||||
)
|
||||
self.session.add(account)
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': account.id}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
def PUT(self, bank_account_id):
|
||||
"""Handle PUT to update a bank account"""
|
||||
try:
|
||||
account = self.session.query(BankAccount).get(bank_account_id)
|
||||
if not account:
|
||||
raise cherrypy.HTTPError(404, "Bank account not found")
|
||||
data = cherrypy.request.json
|
||||
if 'name' in data:
|
||||
account.name = data['name']
|
||||
if 'bank_name' in data:
|
||||
account.bank_name = data['bank_name']
|
||||
if 'account_number' in data:
|
||||
account.account_number = data['account_number']
|
||||
if 'currency' in data:
|
||||
account.currency = data['currency']
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': account.id}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
def DELETE(self, bank_account_id):
|
||||
"""Handle DELETE to remove a bank account"""
|
||||
try:
|
||||
account = self.session.query(BankAccount).get(bank_account_id)
|
||||
if not account:
|
||||
raise cherrypy.HTTPError(404, "Bank account not found")
|
||||
# Check for dependencies before deleting
|
||||
if self.session.query(BankTransaction).filter_by(bank_account_id=bank_account_id).count() > 0:
|
||||
return {'status': 'error', 'message': 'Cannot delete bank account with transactions'}
|
||||
self.session.delete(account)
|
||||
self.session.commit()
|
||||
return {'status': 'success'}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
def OPTIONS(self, bank_account_id=None):
|
||||
"""Handle OPTIONS requests (CORS preflight)"""
|
||||
if bank_account_id is not None:
|
||||
cherrypy.response.headers['Allow'] = 'GET, PUT, DELETE, OPTIONS'
|
||||
else:
|
||||
cherrypy.response.headers['Allow'] = 'GET, POST, OPTIONS'
|
||||
return ""
|
||||
|
||||
|
||||
class BankTransactionHandler:
|
||||
exposed = True
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def GET(self, bank_account_id, transaction_id=None):
|
||||
"""
|
||||
GET /api/bank_accounts/123/transactions - List all transactions for bank account
|
||||
GET /api/bank_accounts/123/transactions/456 - Get specific transaction
|
||||
"""
|
||||
if not self.session.query(BankAccount).get(bank_account_id):
|
||||
raise cherrypy.HTTPError(404, "Bank account not found")
|
||||
|
||||
if transaction_id is not None:
|
||||
# Get specific transaction
|
||||
transaction = self.session.query(BankTransaction).get(transaction_id)
|
||||
if not transaction or transaction.bank_account_id != int(bank_account_id):
|
||||
raise cherrypy.HTTPError(404, "Transaction not found")
|
||||
|
||||
return {
|
||||
'id': transaction.id,
|
||||
'date': transaction.date.isoformat(),
|
||||
'description': transaction.description,
|
||||
'amount': float(transaction.amount),
|
||||
'bank_account_id': transaction.bank_account_id
|
||||
}
|
||||
else:
|
||||
# List all transactions for the bank account
|
||||
transactions = self.session.query(BankTransaction).filter_by(bank_account_id=bank_account_id).all()
|
||||
return [{
|
||||
'id': t.id,
|
||||
'date': t.date.isoformat(),
|
||||
'description': t.description,
|
||||
'amount': float(t.amount),
|
||||
'bank_account_id': t.bank_account_id
|
||||
} for t in transactions]
|
||||
|
||||
def POST(self, bank_account_id):
|
||||
"""Handle POST to create a new transaction"""
|
||||
try:
|
||||
if not self.session.query(BankAccount).get(bank_account_id):
|
||||
raise cherrypy.HTTPError(404, "Bank account not found")
|
||||
|
||||
data = cherrypy.request.json
|
||||
transaction = BankTransaction(
|
||||
bank_account_id=bank_account_id,
|
||||
date=datetime.strptime(data['date'], '%Y-%m-%d').date(),
|
||||
description=data['description'],
|
||||
amount=data['amount']
|
||||
)
|
||||
self.session.add(transaction)
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': transaction.id}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
def OPTIONS(self, bank_account_id, transaction_id=None):
|
||||
"""Handle OPTIONS requests (CORS preflight)"""
|
||||
if transaction_id is not None:
|
||||
cherrypy.response.headers['Allow'] = 'GET, OPTIONS'
|
||||
else:
|
||||
cherrypy.response.headers['Allow'] = 'GET, POST, OPTIONS'
|
||||
return ""
|
||||
|
||||
|
||||
@cherrypy.tools.json_out()
|
||||
class JournalEntryHandler:
|
||||
exposed = True
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def GET(self, entry_id=None):
|
||||
"""
|
||||
GET /api/journal_entries - List all journal entries
|
||||
GET /api/journal_entries/123 - Get specific journal entry
|
||||
"""
|
||||
if entry_id is not None:
|
||||
# Get specific journal entry
|
||||
entry = self.session.query(JournalEntry).get(entry_id)
|
||||
if not entry:
|
||||
raise cherrypy.HTTPError(404, "Journal entry not found")
|
||||
|
||||
return {
|
||||
'id': entry.id,
|
||||
'date': entry.date.isoformat(),
|
||||
'description': entry.description,
|
||||
'reference': entry.reference,
|
||||
'lines': [{
|
||||
'account_id': l.account_id,
|
||||
'amount': float(l.amount),
|
||||
'is_debit': l.is_debit
|
||||
} for l in entry.lines]
|
||||
}
|
||||
else:
|
||||
# List all journal entries
|
||||
entries = self.session.query(JournalEntry).all()
|
||||
return [{
|
||||
'id': e.id,
|
||||
'date': e.date.isoformat(),
|
||||
'description': e.description,
|
||||
'reference': e.reference,
|
||||
'lines': [{
|
||||
'account_id': l.account_id,
|
||||
'amount': float(l.amount),
|
||||
'is_debit': l.is_debit
|
||||
} for l in e.lines]
|
||||
} for e in entries]
|
||||
|
||||
def POST(self):
|
||||
"""Handle POST to create a new journal entry"""
|
||||
try:
|
||||
data = cherrypy.request.json
|
||||
total_debit = sum(line['amount'] for line in data['lines'] if line['is_debit'])
|
||||
total_credit = sum(line['amount'] for line in data['lines'] if not line['is_debit'])
|
||||
|
||||
if abs(total_debit - total_credit) > 0.01:
|
||||
return {'status': 'error', 'message': 'Debits and credits must balance'}
|
||||
|
||||
entry = JournalEntry(
|
||||
date=datetime.strptime(data['date'], '%Y-%m-%d').date(),
|
||||
reference=data.get('reference', ''),
|
||||
description=data.get('description', '')
|
||||
)
|
||||
self.session.add(entry)
|
||||
|
||||
for line_data in data['lines']:
|
||||
line = JournalEntryLine(
|
||||
journal_entry=entry,
|
||||
account_id=line_data['account_id'],
|
||||
amount=line_data['amount'],
|
||||
is_debit=line_data['is_debit']
|
||||
)
|
||||
self.session.add(line)
|
||||
|
||||
account = self.session.query(Account).get(line_data['account_id'])
|
||||
if line_data['is_debit']:
|
||||
account.balance += line_data['amount']
|
||||
else:
|
||||
account.balance -= line_data['amount']
|
||||
|
||||
self.session.commit()
|
||||
return {'status': 'success', 'id': entry.id}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
def DELETE(self, entry_id):
|
||||
"""Handle DELETE to remove a journal entry"""
|
||||
try:
|
||||
entry = self.session.query(JournalEntry).get(entry_id)
|
||||
if not entry:
|
||||
raise cherrypy.HTTPError(404, "Journal entry not found")
|
||||
|
||||
# Reverse the effect on account balances
|
||||
for line in entry.lines:
|
||||
account = self.session.query(Account).get(line.account_id)
|
||||
if line.is_debit:
|
||||
account.balance -= line.amount
|
||||
else:
|
||||
account.balance += line.amount
|
||||
|
||||
# Delete the journal entry and its lines (cascade should handle this)
|
||||
self.session.delete(entry)
|
||||
self.session.commit()
|
||||
return {'status': 'success'}
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise cherrypy.HTTPError(400, str(e))
|
||||
|
||||
def OPTIONS(self, entry_id=None):
|
||||
"""Handle OPTIONS requests (CORS preflight)"""
|
||||
if entry_id is not None:
|
||||
cherrypy.response.headers['Allow'] = 'GET, DELETE, OPTIONS'
|
||||
else:
|
||||
cherrypy.response.headers['Allow'] = 'GET, POST, OPTIONS'
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user