From 53404302988ab0a932a9c9de1e4a814cbbfbf781 Mon Sep 17 00:00:00 2001 From: Aaron Guise Date: Thu, 8 May 2025 16:12:33 +1200 Subject: [PATCH] Initial project --- __pycache__/config.cpython-311.pyc | Bin 0 -> 215 bytes accounting.db | Bin 0 -> 36864 bytes accounting/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 189 bytes accounting/__pycache__/api.cpython-311.pyc | Bin 0 -> 10565 bytes accounting/__pycache__/models.cpython-311.pyc | Bin 0 -> 8406 bytes accounting/api.py | 161 +++++ accounting/models.py | 106 ++++ accounting/templates/index.html | 588 ++++++++++++++++++ config.py | 2 + server.py | 85 +++ 11 files changed, 942 insertions(+) create mode 100644 __pycache__/config.cpython-311.pyc create mode 100644 accounting.db create mode 100644 accounting/__init__.py create mode 100644 accounting/__pycache__/__init__.cpython-311.pyc create mode 100644 accounting/__pycache__/api.cpython-311.pyc create mode 100644 accounting/__pycache__/models.cpython-311.pyc create mode 100644 accounting/api.py create mode 100644 accounting/models.py create mode 100644 accounting/templates/index.html create mode 100644 config.py create mode 100644 server.py diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d96951ddf11129506d2b333f35688623a1e465b0 GIT binary patch literal 215 zcmZ3^%ge<81i^wb8IC~uF^B^Lj8MjBkdo;PDGX5zDU87knoL#V#f3SUC8<{W`ud5< z$@!&uC7F5YdMQbMnvA!2TpU9jog9N*<3ocyS2BDC8S=|nKO;XkRX@Envp6+TKcFZ- zD>b>KSihjMBqKjhKe3>oSU)AdG$|)lH#M)Ms8SbUs(x~QURq|lUP0wA4x8Nkl+v73 fyCOEAnIP8{^8twu%#4hT5BN12ctEg-1tx9$(V3U=#h3a+|ToKHr%$v6bVarX5i7+VxJ2fK;WMk}OM4 z2$3X7Vb214Mi2gdDf+_R<=E%EkBW4E|LfX~Kho;*FX{TX8$Z{+U%OrWUAU+GU=4Uc z00Izzz<&@pqpM13cUS&G1Ebfc9yMLZwEMOZ*si0q)!uwd{p{94T~livm2_%Pn<~lP zhLnp1+airtM}4kpq}A?_R=3&QBE^D72d)?BIlX~rIKE+qXOXkXJ9p?gMqj5+;EnRe z2_w*b)AcAj@`0)~>g9)9q_V+J!EpoXlb1EE{-UOpD-Rxot%q9Upr*Yd`|2xFwym&j zudS(#=Plk=o}Tyy(bPRvQ(JZQI2r$VBR_KhF*ae8p5dJ6vHt7QbU9EN0=raN?Uv+V~4n+le}t}&kRDHWe5)Uk4U zOpmfXES+7F@Vz>X1C=~e_iEi{ht#_oOH4=SjmMqZ!6DB#J4`T)C;U?RP6Qxe$NM7c zv&Sn+X?t7#vJq0JQkrT|1mINnoEnNBKCc3@RgF`gk7-Hlgf$_!Thuo_d%!C&RU#gJ z&6Wzsq|=_AN%3S8$LD0cRC!V^Dz`+87x42DCvdOmGm<5shtJ9S*YU$=EIUws#fLCrBE$SJJS2ML;Z}VWkE1wgE@}k*0abmIrF+N ztODEDE!wjK^0eJz9DQ5bV161`s$qKsPS%%)UP35lKz zpa-MDtae#V3LBZOHS4S-ItKQA`~O8vFXDm#1Rwwb2tWV=5P$##AOHafgaY{g9}@ur z5P$##AOHafKmY;|fB*y_u=oPF|6lwZBSHv300Izz00bZa0SG_<0uX=z?*B0dAOHaf zKmY;|fB*y_009U<00N6IfcyW&&oLr|00bZa0SG_<0uX=z1Rwwb2;lx7a{vMmfB*y_ r009U<00Izz00bbg_yV~9U;G>+LI^+r0uX=z1Rwwb2tWV=5P-m6#;_sa literal 0 HcmV?d00001 diff --git a/accounting/__init__.py b/accounting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounting/__pycache__/__init__.cpython-311.pyc b/accounting/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fe5bd8e366051eb7aaaf99792450bc0e8fc815c GIT binary patch literal 189 zcmZ3^%ge<81p0h386f&Gh=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09tD?mRZKQ~oB zy)?5pHBmpHC_gJTxujUXpt2+*KTkigprBYkCBHN&Csj8!ucW9_H!(RmzcjBTGcR2q yi5VZCnU`4-AFo$X`HRCQH$SB`C)KWq6=(s-3B~+C;sY}yBjX1K7*WIw6axSh1TVS( literal 0 HcmV?d00001 diff --git a/accounting/__pycache__/api.cpython-311.pyc b/accounting/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3363c6ebd03d281ed1dcb2be2802442779ea66df GIT binary patch literal 10565 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 literal 0 HcmV?d00001 diff --git a/accounting/api.py b/accounting/api.py new file mode 100644 index 0000000..0aa5b61 --- /dev/null +++ b/accounting/api.py @@ -0,0 +1,161 @@ +import cherrypy +from sqlalchemy.orm import sessionmaker +from datetime import datetime +from accounting.models import Account, BankAccount, BankTransaction, JournalEntry, JournalEntryLine, ReconciliationReport, \ + ReconciliationMatch +import simplejson as json + + +class AccountingAPI: + 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): + 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) + + 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) + + 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} + + # 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) + + 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'] + ) + 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} + + @cherrypy.expose + @cherrypy.tools.json_out() + def journal_entries(self): + if cherrypy.request.method != 'GET': + raise cherrypy.HTTPError(405) + + 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] + + # Add default OPTIONS handler for all endpoints + @cherrypy.expose + def default(self, *args, **kwargs): + if cherrypy.request.method == 'OPTIONS': + cherrypy.response.headers['Allow'] = 'GET, POST, OPTIONS' + cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + return '' + raise cherrypy.HTTPError(404) \ No newline at end of file diff --git a/accounting/models.py b/accounting/models.py new file mode 100644 index 0000000..e1a8f21 --- /dev/null +++ b/accounting/models.py @@ -0,0 +1,106 @@ +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship + +Base = declarative_base() + + +class Account(Base): + __tablename__ = 'accounts' + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(100), nullable=False) + account_type = sa.Column(sa.String(50), nullable=False) # Asset, Liability, Equity, Revenue, Expense + code = sa.Column(sa.String(20), unique=True, nullable=False) + parent_id = sa.Column(sa.Integer, sa.ForeignKey('accounts.id')) + balance = sa.Column(sa.Numeric(15, 2), default=0) + + parent = relationship('Account', remote_side=[id]) + entries = relationship('JournalEntryLine', back_populates='account') + + def __repr__(self): + return f"" + + +class JournalEntry(Base): + __tablename__ = 'journal_entries' + + id = sa.Column(sa.Integer, primary_key=True) + date = sa.Column(sa.Date, nullable=False) + reference = sa.Column(sa.String(100)) + description = sa.Column(sa.String(200)) + created_at = sa.Column(sa.DateTime, server_default=sa.func.now()) + + lines = relationship('JournalEntryLine', back_populates='journal_entry') + + +class JournalEntryLine(Base): + __tablename__ = 'journal_entry_lines' + + id = sa.Column(sa.Integer, primary_key=True) + journal_entry_id = sa.Column(sa.Integer, sa.ForeignKey('journal_entries.id'), nullable=False) + account_id = sa.Column(sa.Integer, sa.ForeignKey('accounts.id'), nullable=False) + amount = sa.Column(sa.Numeric(15, 2), nullable=False) + is_debit = sa.Column(sa.Boolean, nullable=False) # True for debit, False for credit + + journal_entry = relationship('JournalEntry', back_populates='lines') + account = relationship('Account', back_populates='entries') + + +class BankAccount(Base): + __tablename__ = 'bank_accounts' + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(100), nullable=False) + account_number = sa.Column(sa.String(50)) + bank_name = sa.Column(sa.String(100)) + currency = sa.Column(sa.String(3), default='USD') + ledger_account_id = sa.Column(sa.Integer, sa.ForeignKey('accounts.id')) + + ledger_account = relationship('Account') + transactions = relationship('BankTransaction', back_populates='bank_account') + + +class BankTransaction(Base): + __tablename__ = 'bank_transactions' + + id = sa.Column(sa.Integer, primary_key=True) + bank_account_id = sa.Column(sa.Integer, sa.ForeignKey('bank_accounts.id'), nullable=False) + date = sa.Column(sa.Date, nullable=False) + amount = sa.Column(sa.Numeric(15, 2), nullable=False) + description = sa.Column(sa.String(200)) + reference = sa.Column(sa.String(100)) + status = sa.Column(sa.String(20), default='unreconciled') # unreconciled, reconciled + journal_entry_id = sa.Column(sa.Integer, sa.ForeignKey('journal_entries.id')) + + bank_account = relationship('BankAccount', back_populates='transactions') + journal_entry = relationship('JournalEntry') + + +class ReconciliationReport(Base): + __tablename__ = 'reconciliation_reports' + + id = sa.Column(sa.Integer, primary_key=True) + bank_account_id = sa.Column(sa.Integer, sa.ForeignKey('bank_accounts.id'), nullable=False) + start_date = sa.Column(sa.Date, nullable=False) + end_date = sa.Column(sa.Date, nullable=False) + created_at = sa.Column(sa.DateTime, server_default=sa.func.now()) + status = sa.Column(sa.String(20), default='draft') # draft, completed + + bank_account = relationship('BankAccount') + matches = relationship('ReconciliationMatch', back_populates='report') + + +class ReconciliationMatch(Base): + __tablename__ = 'reconciliation_matches' + + id = sa.Column(sa.Integer, primary_key=True) + report_id = sa.Column(sa.Integer, sa.ForeignKey('reconciliation_reports.id'), nullable=False) + transaction_id = sa.Column(sa.Integer, sa.ForeignKey('bank_transactions.id'), nullable=False) + journal_entry_id = sa.Column(sa.Integer, sa.ForeignKey('journal_entries.id'), nullable=False) + match_score = sa.Column(sa.Numeric(5, 2)) # 0-100 score for match quality + notes = sa.Column(sa.String(200)) + + report = relationship('ReconciliationReport', back_populates='matches') + transaction = relationship('BankTransaction') + journal_entry = relationship('JournalEntry') \ No newline at end of file diff --git a/accounting/templates/index.html b/accounting/templates/index.html new file mode 100644 index 0000000..ab6aeaf --- /dev/null +++ b/accounting/templates/index.html @@ -0,0 +1,588 @@ + + + + Accounting System + + + + + +
+

Accounting System

+ +
+
Accounts
+
Trial Balance
+
Bank Reconciliation
+
Reports
+
+ + +
+

Accounts

+ + + + + + + + + + + + + + + + + +
CodeNameTypeBalance
{{ account.code }}{{ account.name }}{{ account.type }}{{ formatCurrency(account.balance) }}
+ +

Add New Account

+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+

Trial Balance

+ + + + + + + + + + + + + + + +
AccountDebitCredit
{{ item.account }}{{ formatCurrency(item.debit) }}{{ formatCurrency(item.credit) }}
+
+ + +
+

Bank Reconciliation

+ +
+

Bank Accounts

+ + + + + + + + + + + + + + + + + + + +
BankAccount NumberNameCurrencyActions
{{ account.bank_name }}{{ account.account_number }}{{ account.name }}{{ account.currency }} + +
+ +

Add New Bank Account

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+

Import Transactions

+
+ + +
+ +
+ + +
+ + + +
+

Imported {{ importResult.imported.length }} transactions

+
+
+ +
+

Unreconciled Transactions

+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
DateDescriptionAmountMatch With Journal EntryAction
{{ txn.date }}{{ txn.description }}{{ formatCurrency(txn.amount) }} + + + +
+

No unreconciled transactions found

+
+
+ + +
+

Reconciliation Reports

+ +
+

Create New Report

+
+ + +
+
+ + +
+
+ + +
+ + +

Recent Reports

+ + + + + + + + + + + + + + + + + + + +
Bank AccountDate RangeStatusCreatedActions
{{ getBankAccountName(report.bank_account_id) }}{{ report.start_date }} to {{ report.end_date }}{{ report.status }}{{ report.created_at }} + +
+

No reconciliation reports found

+
+ +
+

Reconciliation Report - {{ activeReport.start_date }} to {{ activeReport.end_date }}

+

Bank Account: {{ getBankAccountName(activeReport.bank_account_id) }}

+

Status: {{ activeReport.status }}

+ +
+ + +
+ + + + + + + + + + + + + + + + + + +
Bank TransactionMatch ScoreJournal EntryAction
+ {{ match.transaction_date }}
+ {{ match.transaction_description }}
+ {{ formatCurrency(match.transaction_amount) }} +
+
+ {{ match.match_score }}% +
+ {{ match.journal_entry_date }}
+ {{ match.journal_entry_description }} +
+ + +
+ +
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..caaf8b6 --- /dev/null +++ b/config.py @@ -0,0 +1,2 @@ +# Configuration settings +DATABASE_URI = "sqlite:///accounting.db" \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..987a21e --- /dev/null +++ b/server.py @@ -0,0 +1,85 @@ +import cherrypy +from sqlalchemy import create_engine +from accounting.models import Base +from accounting.api import AccountingAPI +import os +from config import DATABASE_URI + + +def CORS(): + cherrypy.response.headers["Access-Control-Allow-Origin"] = "*" + cherrypy.response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + cherrypy.response.headers["Access-Control-Allow-Headers"] = "Content-Type" + + +class Root: + @cherrypy.expose + def index(self): + return open(os.path.join(os.path.dirname(__file__), 'accounting/templates/index.html')).read() + + @cherrypy.expose + def favicon_ico(self): + return cherrypy.lib.static.serve_file( + os.path.join(os.path.dirname(__file__), 'static/favicon.ico'), + content_type='image/x-icon' + ) + + +def setup_database(): + engine = create_engine(DATABASE_URI) + Base.metadata.create_all(engine) + return engine + + +def main(): + # Database setup + db_engine = setup_database() + + # Create static directory if it doesn't exist + static_path = os.path.join(os.path.dirname(__file__), 'static') + if not os.path.exists(static_path): + os.makedirs(static_path) + + # CherryPy configuration + conf = { + '/': { + 'tools.sessions.on': True, + 'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)), + 'tools.CORS.on': True + }, + '/static': { + 'tools.staticdir.on': True, + '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 + cherrypy.tools.CORS = cherrypy.Tool('before_handler', CORS) + + # Create application + root = Root() + root.api = AccountingAPI(db_engine) + + cherrypy.tree.mount(root, '/', conf) + + # Start server + cherrypy.config.update({ + 'server.socket_host': '0.0.0.0', + 'server.socket_port': 8080, + 'log.screen': True, + 'engine.autoreload.on': True + }) + + print("Starting accounting system...") + cherrypy.engine.start() + cherrypy.engine.block() + + +if __name__ == '__main__': + main() \ No newline at end of file