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,70 +1,62 @@
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 ''
# Accounts Endpoints
@cherrypy.expose
@cherrypy.tools.json_out()
def accounts(self):
if cherrypy.request.method != 'GET':
raise cherrypy.HTTPError(405)
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")
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,
@@ -74,13 +66,9 @@ class AccountingAPI:
'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)
def POST(self):
"""Handle POST to create a new account"""
try:
data = cherrypy.request.json
account = Account(
name=data['name'],
@@ -91,15 +79,276 @@ class AccountingAPI:
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))
# 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)
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")
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']
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, 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")
# 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'
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'])
@@ -131,31 +380,37 @@ class AccountingAPI:
self.session.commit()
return {'status': 'success', 'id': entry.id}
except Exception as e:
self.session.rollback()
raise cherrypy.HTTPError(400, str(e))
@cherrypy.expose
@cherrypy.tools.json_out()
def journal_entries(self):
if cherrypy.request.method != 'GET':
raise cherrypy.HTTPError(405)
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")
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]
# 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
# Add default OPTIONS handler for all endpoints
@cherrypy.expose
def default(self, *args, **kwargs):
if cherrypy.request.method == 'OPTIONS':
# 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'
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
return ''
raise cherrypy.HTTPError(404)
return ""

View File

@@ -404,7 +404,7 @@
});
},
addAccount() {
axios.post('/api/add_account', this.newAccount)
axios.post('/api/accounts', this.newAccount)
.then(response => {
if (response.data.status === 'success') {
this.fetchAccounts();
@@ -419,7 +419,7 @@
});
},
addBankAccount() {
axios.post('/api/add_bank_account', this.newBankAccount)
axios.post('/api/bank_accounts', this.newBankAccount)
.then(response => {
if (response.data.status === 'success') {
this.fetchBankAccounts();
@@ -455,8 +455,7 @@
transactions.push(txn);
}
axios.post('/api/import_bank_transactions', {
bank_account_id: this.selectedBankAccount,
axios.post(`/api/bank_accounts/${this.selectedBankAccount}/transactions`, {
transactions: transactions
}).then(response => {
this.importResult = response.data;
@@ -471,7 +470,7 @@
},
fetchUnreconciledTransactions() {
if (this.selectedBankAccountForReconciliation) {
axios.get(`/api/get_unreconciled_transactions?bank_account_id=${this.selectedBankAccountForReconciliation}`)
axios.get(`/api/bank_accounts/${this.selectedBankAccountForReconciliation}/unreconciled`)
.then(response => {
this.unreconciledTransactions = response.data;
});
@@ -506,7 +505,7 @@
const journalEntryId = this.selectedJournalEntries[txnId];
if (!journalEntryId) return;
axios.post('/api/reconcile_transaction', {
axios.post('/api/reconciliation', {
transaction_id: txnId,
journal_entry_id: journalEntryId
}).then(response => {
@@ -519,13 +518,13 @@
});
},
fetchReconciliationReports() {
axios.get('/api/reconciliation_reports')
axios.get('/api/reconciliation/reports')
.then(response => {
this.reconciliationReports = response.data;
});
},
createReconciliationReport() {
axios.post('/api/create_reconciliation_report', this.newReport)
axios.post('/api/reconciliation/reports', this.newReport)
.then(response => {
if (response.data.status === 'success') {
this.loadReport(response.data.report_id);
@@ -534,7 +533,7 @@
});
},
loadReport(reportId) {
axios.get(`/api/get_reconciliation_report?report_id=${reportId}`)
axios.get(`/api/reconciliation/reports/${reportId}`)
.then(response => {
if (response.data.status === 'success') {
this.activeReport = response.data.report;
@@ -543,7 +542,7 @@
});
},
acceptMatch(match) {
axios.post('/api/reconcile_transaction', {
axios.post('/api/reconciliation', {
transaction_id: match.transaction_id,
journal_entry_id: match.journal_entry_id
}).then(response => {
@@ -553,15 +552,14 @@
});
},
rejectMatch(match) {
axios.delete(`/api/reconciliation_match/${match.id}`)
axios.delete(`/api/reconciliation/matches/${match.id}`)
.then(() => {
this.loadReport(this.activeReport.id);
});
},
finalizeReconciliation() {
axios.post('/api/finalize_reconciliation', {
report_id: this.activeReport.id
}).then(response => {
axios.post(`/api/reconciliation/reports/${this.activeReport.id}/finalize`)
.then(response => {
if (response.data.status === 'success') {
this.activeReport = null;
this.fetchReconciliationReports();

View File

@@ -3,6 +3,7 @@ from sqlalchemy import create_engine
from accounting.models import Base
from accounting.api import AccountingAPI
import os
import json
from config import DATABASE_URI
@@ -12,6 +13,16 @@ def CORS():
cherrypy.response.headers["Access-Control-Allow-Headers"] = "Content-Type"
# JSON Tools for response serialization
class JSONEncoder(object):
def __init__(self):
self.json_encoder = json.JSONEncoder()
def __call__(self, value):
# Convert the Python object to a JSON string then to bytes
return json.dumps(value).encode('utf-8')
class Root:
@cherrypy.expose
def index(self):
@@ -40,8 +51,11 @@ def main():
if not os.path.exists(static_path):
os.makedirs(static_path)
# CherryPy configuration
conf = {
# Register tools
cherrypy.tools.CORS = cherrypy.Tool('before_handler', CORS)
# Root application config
root_conf = {
'/': {
'tools.sessions.on': True,
'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)),
@@ -50,23 +64,31 @@ def main():
'/static': {
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
},
'/api': {
}
}
# API application config
api_conf = {
'/': {
'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
'tools.CORS.on': True,
'tools.response_headers.on': True,
'tools.response_headers.headers': [('Content-Type', 'application/json')],
'tools.json_out.on': True, # Use the built-in JSON serializer
'tools.encode.on': True,
'tools.encode.encoding': 'utf-8',
# Process JSON request bodies
'request.body.processors': {
'application/json': cherrypy.lib.jsontools.json_processor
}
}
}
# Register CORS tool
cherrypy.tools.CORS = cherrypy.Tool('before_handler', CORS)
# Create application
# Create and mount Root application
root = Root()
root.api = AccountingAPI(db_engine)
cherrypy.tree.mount(root, '/', root_conf)
cherrypy.tree.mount(root, '/', conf)
# Create and mount API as a separate application
api = AccountingAPI(db_engine)
cherrypy.tree.mount(api, '/api', api_conf)
# Start server
cherrypy.config.update({