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
|
import cherrypy
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from datetime import datetime
|
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
|
ReconciliationMatch
|
||||||
import simplejson as json
|
|
||||||
|
|
||||||
|
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
class AccountingAPI:
|
class AccountingAPI:
|
||||||
|
exposed = True
|
||||||
|
|
||||||
def __init__(self, db_engine):
|
def __init__(self, db_engine):
|
||||||
self.db_engine = db_engine
|
self.db_engine = db_engine
|
||||||
Session = sessionmaker(bind=db_engine)
|
Session = sessionmaker(bind=db_engine)
|
||||||
self.session = Session()
|
self.session = Session()
|
||||||
|
|
||||||
@cherrypy.expose
|
# Mount resource handlers as attributes - these become URL segments
|
||||||
@cherrypy.tools.json_out()
|
self.accounts = AccountHandler(self.session)
|
||||||
def index(self):
|
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"}
|
return {"status": "success", "message": "Accounting API is running"}
|
||||||
|
|
||||||
# Bank Accounts Endpoints
|
def OPTIONS(self):
|
||||||
@cherrypy.expose
|
"""Handle OPTIONS requests (CORS preflight) to the API root"""
|
||||||
@cherrypy.tools.json_out()
|
cherrypy.response.headers['Allow'] = 'GET, OPTIONS'
|
||||||
def bank_accounts(self):
|
return ""
|
||||||
if cherrypy.request.method != 'GET':
|
|
||||||
raise cherrypy.HTTPError(405)
|
|
||||||
|
|
||||||
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_out()
|
||||||
@cherrypy.tools.json_in()
|
class AccountHandler:
|
||||||
@cherrypy.tools.json_out()
|
exposed = True
|
||||||
def add_bank_account(self):
|
|
||||||
if cherrypy.request.method != 'POST':
|
|
||||||
raise cherrypy.HTTPError(405)
|
|
||||||
|
|
||||||
data = cherrypy.request.json
|
def __init__(self, session):
|
||||||
account = BankAccount(
|
self.session = session
|
||||||
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}
|
|
||||||
|
|
||||||
# Add OPTIONS method handler for CORS preflight
|
def GET(self, account_id=None):
|
||||||
@cherrypy.expose
|
"""
|
||||||
def add_bank_account_options(self):
|
GET /api/accounts - List all accounts
|
||||||
cherrypy.response.headers['Allow'] = 'POST, OPTIONS'
|
GET /api/accounts/123 - Get specific account
|
||||||
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
"""
|
||||||
return ''
|
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
|
return {
|
||||||
@cherrypy.expose
|
'id': account.id,
|
||||||
@cherrypy.tools.json_out()
|
'name': account.name,
|
||||||
def accounts(self):
|
'code': account.code,
|
||||||
if cherrypy.request.method != 'GET':
|
'type': account.account_type,
|
||||||
raise cherrypy.HTTPError(405)
|
'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()
|
def POST(self):
|
||||||
return [{
|
"""Handle POST to create a new account"""
|
||||||
'id': a.id,
|
try:
|
||||||
'name': a.name,
|
data = cherrypy.request.json
|
||||||
'code': a.code,
|
account = Account(
|
||||||
'type': a.account_type,
|
name=data['name'],
|
||||||
'balance': float(a.balance) if a.balance else 0
|
account_type=data['type'],
|
||||||
} for a in accounts]
|
code=data['code'],
|
||||||
|
parent_id=data.get('parent_id')
|
||||||
@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']
|
|
||||||
)
|
)
|
||||||
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'])
|
def PUT(self, account_id):
|
||||||
if line_data['is_debit']:
|
"""Handle PUT to update an existing account"""
|
||||||
account.balance += line_data['amount']
|
try:
|
||||||
else:
|
account = self.session.query(Account).get(account_id)
|
||||||
account.balance -= line_data['amount']
|
if not account:
|
||||||
|
raise cherrypy.HTTPError(404, "Account not found")
|
||||||
|
|
||||||
self.session.commit()
|
data = cherrypy.request.json
|
||||||
return {'status': 'success', 'id': entry.id}
|
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
|
self.session.commit()
|
||||||
@cherrypy.tools.json_out()
|
return {'status': 'success', 'id': account.id}
|
||||||
def journal_entries(self):
|
except Exception as e:
|
||||||
if cherrypy.request.method != 'GET':
|
self.session.rollback()
|
||||||
raise cherrypy.HTTPError(405)
|
raise cherrypy.HTTPError(400, str(e))
|
||||||
|
|
||||||
entries = self.session.query(JournalEntry).all()
|
def DELETE(self, account_id):
|
||||||
return [{
|
"""Handle DELETE to remove an account"""
|
||||||
'id': e.id,
|
try:
|
||||||
'date': e.date.isoformat(),
|
account = self.session.query(Account).get(account_id)
|
||||||
'description': e.description,
|
if not account:
|
||||||
'reference': e.reference,
|
raise cherrypy.HTTPError(404, "Account not found")
|
||||||
'lines': [{
|
|
||||||
'account_id': l.account_id,
|
|
||||||
'amount': float(l.amount),
|
|
||||||
'is_debit': l.is_debit
|
|
||||||
} for l in e.lines]
|
|
||||||
} for e in entries]
|
|
||||||
|
|
||||||
# Add default OPTIONS handler for all endpoints
|
# Check for dependencies before deleting
|
||||||
@cherrypy.expose
|
if self.session.query(JournalEntryLine).filter_by(account_id=account_id).count() > 0:
|
||||||
def default(self, *args, **kwargs):
|
return {'status': 'error', 'message': 'Cannot delete account with journal entries'}
|
||||||
if cherrypy.request.method == 'OPTIONS':
|
|
||||||
|
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['Allow'] = 'GET, POST, OPTIONS'
|
||||||
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
return ""
|
||||||
return ''
|
|
||||||
raise cherrypy.HTTPError(404)
|
|
||||||
|
@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 ""
|
||||||
|
|||||||
@@ -404,7 +404,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
addAccount() {
|
addAccount() {
|
||||||
axios.post('/api/add_account', this.newAccount)
|
axios.post('/api/accounts', this.newAccount)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.status === 'success') {
|
if (response.data.status === 'success') {
|
||||||
this.fetchAccounts();
|
this.fetchAccounts();
|
||||||
@@ -419,7 +419,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
addBankAccount() {
|
addBankAccount() {
|
||||||
axios.post('/api/add_bank_account', this.newBankAccount)
|
axios.post('/api/bank_accounts', this.newBankAccount)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.status === 'success') {
|
if (response.data.status === 'success') {
|
||||||
this.fetchBankAccounts();
|
this.fetchBankAccounts();
|
||||||
@@ -455,8 +455,7 @@
|
|||||||
transactions.push(txn);
|
transactions.push(txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.post('/api/import_bank_transactions', {
|
axios.post(`/api/bank_accounts/${this.selectedBankAccount}/transactions`, {
|
||||||
bank_account_id: this.selectedBankAccount,
|
|
||||||
transactions: transactions
|
transactions: transactions
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
this.importResult = response.data;
|
this.importResult = response.data;
|
||||||
@@ -471,7 +470,7 @@
|
|||||||
},
|
},
|
||||||
fetchUnreconciledTransactions() {
|
fetchUnreconciledTransactions() {
|
||||||
if (this.selectedBankAccountForReconciliation) {
|
if (this.selectedBankAccountForReconciliation) {
|
||||||
axios.get(`/api/get_unreconciled_transactions?bank_account_id=${this.selectedBankAccountForReconciliation}`)
|
axios.get(`/api/bank_accounts/${this.selectedBankAccountForReconciliation}/unreconciled`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.unreconciledTransactions = response.data;
|
this.unreconciledTransactions = response.data;
|
||||||
});
|
});
|
||||||
@@ -506,7 +505,7 @@
|
|||||||
const journalEntryId = this.selectedJournalEntries[txnId];
|
const journalEntryId = this.selectedJournalEntries[txnId];
|
||||||
if (!journalEntryId) return;
|
if (!journalEntryId) return;
|
||||||
|
|
||||||
axios.post('/api/reconcile_transaction', {
|
axios.post('/api/reconciliation', {
|
||||||
transaction_id: txnId,
|
transaction_id: txnId,
|
||||||
journal_entry_id: journalEntryId
|
journal_entry_id: journalEntryId
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
@@ -519,13 +518,13 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchReconciliationReports() {
|
fetchReconciliationReports() {
|
||||||
axios.get('/api/reconciliation_reports')
|
axios.get('/api/reconciliation/reports')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.reconciliationReports = response.data;
|
this.reconciliationReports = response.data;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createReconciliationReport() {
|
createReconciliationReport() {
|
||||||
axios.post('/api/create_reconciliation_report', this.newReport)
|
axios.post('/api/reconciliation/reports', this.newReport)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.status === 'success') {
|
if (response.data.status === 'success') {
|
||||||
this.loadReport(response.data.report_id);
|
this.loadReport(response.data.report_id);
|
||||||
@@ -534,7 +533,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadReport(reportId) {
|
loadReport(reportId) {
|
||||||
axios.get(`/api/get_reconciliation_report?report_id=${reportId}`)
|
axios.get(`/api/reconciliation/reports/${reportId}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.status === 'success') {
|
if (response.data.status === 'success') {
|
||||||
this.activeReport = response.data.report;
|
this.activeReport = response.data.report;
|
||||||
@@ -543,7 +542,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
acceptMatch(match) {
|
acceptMatch(match) {
|
||||||
axios.post('/api/reconcile_transaction', {
|
axios.post('/api/reconciliation', {
|
||||||
transaction_id: match.transaction_id,
|
transaction_id: match.transaction_id,
|
||||||
journal_entry_id: match.journal_entry_id
|
journal_entry_id: match.journal_entry_id
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
@@ -553,21 +552,20 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
rejectMatch(match) {
|
rejectMatch(match) {
|
||||||
axios.delete(`/api/reconciliation_match/${match.id}`)
|
axios.delete(`/api/reconciliation/matches/${match.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.loadReport(this.activeReport.id);
|
this.loadReport(this.activeReport.id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
finalizeReconciliation() {
|
finalizeReconciliation() {
|
||||||
axios.post('/api/finalize_reconciliation', {
|
axios.post(`/api/reconciliation/reports/${this.activeReport.id}/finalize`)
|
||||||
report_id: this.activeReport.id
|
.then(response => {
|
||||||
}).then(response => {
|
if (response.data.status === 'success') {
|
||||||
if (response.data.status === 'success') {
|
this.activeReport = null;
|
||||||
this.activeReport = null;
|
this.fetchReconciliationReports();
|
||||||
this.fetchReconciliationReports();
|
this.fetchUnreconciledTransactions();
|
||||||
this.fetchUnreconciledTransactions();
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
},
|
},
|
||||||
cancelReconciliation() {
|
cancelReconciliation() {
|
||||||
this.activeReport = null;
|
this.activeReport = null;
|
||||||
|
|||||||
50
server.py
50
server.py
@@ -3,6 +3,7 @@ from sqlalchemy import create_engine
|
|||||||
from accounting.models import Base
|
from accounting.models import Base
|
||||||
from accounting.api import AccountingAPI
|
from accounting.api import AccountingAPI
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
from config import DATABASE_URI
|
from config import DATABASE_URI
|
||||||
|
|
||||||
|
|
||||||
@@ -12,6 +13,16 @@ def CORS():
|
|||||||
cherrypy.response.headers["Access-Control-Allow-Headers"] = "Content-Type"
|
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:
|
class Root:
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def index(self):
|
def index(self):
|
||||||
@@ -40,8 +51,11 @@ def main():
|
|||||||
if not os.path.exists(static_path):
|
if not os.path.exists(static_path):
|
||||||
os.makedirs(static_path)
|
os.makedirs(static_path)
|
||||||
|
|
||||||
# CherryPy configuration
|
# Register tools
|
||||||
conf = {
|
cherrypy.tools.CORS = cherrypy.Tool('before_handler', CORS)
|
||||||
|
|
||||||
|
# Root application config
|
||||||
|
root_conf = {
|
||||||
'/': {
|
'/': {
|
||||||
'tools.sessions.on': True,
|
'tools.sessions.on': True,
|
||||||
'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)),
|
'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)),
|
||||||
@@ -50,23 +64,31 @@ def main():
|
|||||||
'/static': {
|
'/static': {
|
||||||
'tools.staticdir.on': True,
|
'tools.staticdir.on': True,
|
||||||
'tools.staticdir.dir': 'static',
|
'tools.staticdir.dir': 'static',
|
||||||
},
|
|
||||||
'/api': {
|
|
||||||
'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
|
|
||||||
'tools.CORS.on': True,
|
|
||||||
'tools.response_headers.on': True,
|
|
||||||
'tools.response_headers.headers': [('Content-Type', 'application/json')],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Register CORS tool
|
# API application config
|
||||||
cherrypy.tools.CORS = cherrypy.Tool('before_handler', CORS)
|
api_conf = {
|
||||||
|
'/': {
|
||||||
|
'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
|
||||||
|
'tools.CORS.on': True,
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Create application
|
# Create and mount Root application
|
||||||
root = Root()
|
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
|
# Start server
|
||||||
cherrypy.config.update({
|
cherrypy.config.update({
|
||||||
@@ -82,4 +104,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user