import cherrypy from sqlalchemy.orm import sessionmaker from datetime import datetime from accounting.models import Account, BankAccount, BankTransaction, JournalEntry, JournalEntryLine, \ ReconciliationReport, \ ReconciliationMatch @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() # 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"} def OPTIONS(self): """Handle OPTIONS requests (CORS preflight) to the API root""" cherrypy.response.headers['Allow'] = 'GET, OPTIONS' return "" @cherrypy.tools.json_out() class AccountHandler: exposed = True def __init__(self, session): self.session = session 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, 'name': a.name, 'code': a.code, 'type': a.account_type, 'balance': float(a.balance) if a.balance else 0 } for a in accounts] 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(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, 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']) 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 ""