fix: Initial barebones UI and API running

This commit is contained in:
2025-05-08 22:41:14 +12:00
parent a2942f6d7d
commit 5046ac67f1
3 changed files with 443 additions and 168 deletions

View File

@@ -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 ""