Initial project

This commit is contained in:
2025-05-08 16:12:33 +12:00
commit 5340430298
11 changed files with 942 additions and 0 deletions

Binary file not shown.

BIN
accounting.db Normal file

Binary file not shown.

0
accounting/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

161
accounting/api.py Normal file
View File

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

106
accounting/models.py Normal file
View File

@@ -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"<Account(code={self.code}, name={self.name}, type={self.account_type})>"
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')

View File

@@ -0,0 +1,588 @@
<!DOCTYPE html>
<html>
<head>
<title>Accounting System</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.section { margin-bottom: 30px; padding: 15px; border: 1px solid #eee; border-radius: 5px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.tabs { margin-bottom: 20px; }
.tab { padding: 10px 15px; cursor: pointer; background: #f1f1f1; display: inline-block; margin-right: 5px; }
.tab.active { background: #4CAF50; color: white; }
input, select { padding: 8px; margin: 5px 0; width: 200px; }
button { padding: 8px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; margin: 5px; }
button:hover { background: #45a049; }
button:disabled { background: #cccccc; }
.file-input { margin: 15px 0; }
.score-bar { height: 10px; background-color: #4CAF50; margin: 5px 0; }
.match-quality-filters { margin: 15px 0; }
.match-quality-filters label { margin-right: 15px; }
.report-actions { margin-top: 20px; }
.primary { background-color: #2196F3; }
.danger { background-color: #f44336; }
.bank-section { margin-bottom: 30px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<h1>Accounting System</h1>
<div class="tabs">
<div class="tab" :class="{active: activeTab === 'accounts'}" @click="activeTab = 'accounts'">Accounts</div>
<div class="tab" :class="{active: activeTab === 'trial-balance'}" @click="activeTab = 'trial-balance'">Trial Balance</div>
<div class="tab" :class="{active: activeTab === 'banking'}" @click="activeTab = 'banking'">Bank Reconciliation</div>
<div class="tab" :class="{active: activeTab === 'reports'}" @click="activeTab = 'reports'">Reports</div>
</div>
<!-- Accounts Tab -->
<div class="section" v-if="activeTab === 'accounts'">
<h2>Accounts</h2>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Type</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
<tr v-for="account in accounts">
<td>{{ account.code }}</td>
<td>{{ account.name }}</td>
<td>{{ account.type }}</td>
<td>{{ formatCurrency(account.balance) }}</td>
</tr>
</tbody>
</table>
<h3>Add New Account</h3>
<div class="form-group">
<label>Account Name</label>
<input v-model="newAccount.name" placeholder="Account Name">
</div>
<div class="form-group">
<label>Account Code</label>
<input v-model="newAccount.code" placeholder="Account Code">
</div>
<div class="form-group">
<label>Account Type</label>
<select v-model="newAccount.type">
<option value="Asset">Asset</option>
<option value="Liability">Liability</option>
<option value="Equity">Equity</option>
<option value="Revenue">Revenue</option>
<option value="Expense">Expense</option>
</select>
</div>
<button @click="addAccount">Add Account</button>
</div>
<!-- Trial Balance Tab -->
<div class="section" v-if="activeTab === 'trial-balance'">
<h2>Trial Balance</h2>
<table>
<thead>
<tr>
<th>Account</th>
<th>Debit</th>
<th>Credit</th>
</tr>
</thead>
<tbody>
<tr v-for="item in trialBalance">
<td>{{ item.account }}</td>
<td>{{ formatCurrency(item.debit) }}</td>
<td>{{ formatCurrency(item.credit) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Banking Tab -->
<div class="section" v-if="activeTab === 'banking'">
<h2>Bank Reconciliation</h2>
<div class="bank-section">
<h3>Bank Accounts</h3>
<table>
<thead>
<tr>
<th>Bank</th>
<th>Account Number</th>
<th>Name</th>
<th>Currency</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="account in bankAccounts">
<td>{{ account.bank_name }}</td>
<td>{{ account.account_number }}</td>
<td>{{ account.name }}</td>
<td>{{ account.currency }}</td>
<td>
<button @click="viewReconciliation(account.id)">Reconcile</button>
</td>
</tr>
</tbody>
</table>
<h3>Add New Bank Account</h3>
<div class="form-group">
<label>Account Name</label>
<input v-model="newBankAccount.name" placeholder="Account Name">
</div>
<div class="form-group">
<label>Bank Name</label>
<input v-model="newBankAccount.bank_name" placeholder="Bank Name">
</div>
<div class="form-group">
<label>Account Number</label>
<input v-model="newBankAccount.account_number" placeholder="Account Number">
</div>
<div class="form-group">
<label>Currency</label>
<input v-model="newBankAccount.currency" placeholder="Currency" value="USD">
</div>
<button @click="addBankAccount">Add Bank Account</button>
</div>
<div class="bank-section" v-if="selectedBankAccount">
<h3>Import Transactions</h3>
<div class="form-group">
<label>Select Bank Account</label>
<select v-model="selectedBankAccount">
<option value="">Select Bank Account</option>
<option v-for="account in bankAccounts" :value="account.id">
{{ account.bank_name }} - {{ account.account_number }}
</option>
</select>
</div>
<div class="form-group">
<label>CSV File</label>
<input type="file" @change="handleFileUpload" accept=".csv">
</div>
<button @click="importTransactions" :disabled="!selectedBankAccount || !fileData">Import</button>
<div v-if="importResult">
<p>Imported {{ importResult.imported.length }} transactions</p>
</div>
</div>
<div class="bank-section" v-if="selectedBankAccountForReconciliation">
<h3>Unreconciled Transactions</h3>
<div class="form-group">
<label>Select Bank Account</label>
<select v-model="selectedBankAccountForReconciliation" @change="fetchUnreconciledTransactions">
<option value="">Select Bank Account</option>
<option v-for="account in bankAccounts" :value="account.id">
{{ account.bank_name }} - {{ account.account_number }}
</option>
</select>
</div>
<table v-if="unreconciledTransactions.length > 0">
<thead>
<tr>
<th>Date</th>
<th>Description</th>
<th>Amount</th>
<th>Match With Journal Entry</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="txn in unreconciledTransactions">
<td>{{ txn.date }}</td>
<td>{{ txn.description }}</td>
<td>{{ formatCurrency(txn.amount) }}</td>
<td>
<select v-model="selectedJournalEntries[txn.id]">
<option value="">Select matching entry</option>
<option v-for="entry in potentialMatches(txn)" :value="entry.id">
{{ entry.date }} - {{ entry.description }} ({{ formatCurrency(entry.amount) }})
</option>
</select>
</td>
<td>
<button @click="reconcile(txn.id)" :disabled="!selectedJournalEntries[txn.id]">Reconcile</button>
</td>
</tr>
</tbody>
</table>
<p v-else>No unreconciled transactions found</p>
</div>
</div>
<!-- Reports Tab -->
<div class="section" v-if="activeTab === 'reports'">
<h2>Reconciliation Reports</h2>
<div v-if="!activeReport">
<h3>Create New Report</h3>
<div class="form-group">
<label>Bank Account</label>
<select v-model="newReport.bank_account_id">
<option value="">Select Bank Account</option>
<option v-for="account in bankAccounts" :value="account.id">
{{ account.bank_name }} - {{ account.account_number }}
</option>
</select>
</div>
<div class="form-group">
<label>Start Date</label>
<input type="date" v-model="newReport.start_date">
</div>
<div class="form-group">
<label>End Date</label>
<input type="date" v-model="newReport.end_date">
</div>
<button @click="createReconciliationReport"
:disabled="!newReport.bank_account_id || !newReport.start_date || !newReport.end_date">
Generate Report
</button>
<h3>Recent Reports</h3>
<table v-if="reconciliationReports.length > 0">
<thead>
<tr>
<th>Bank Account</th>
<th>Date Range</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="report in reconciliationReports">
<td>{{ getBankAccountName(report.bank_account_id) }}</td>
<td>{{ report.start_date }} to {{ report.end_date }}</td>
<td>{{ report.status }}</td>
<td>{{ report.created_at }}</td>
<td>
<button @click="loadReport(report.id)">View</button>
</td>
</tr>
</tbody>
</table>
<p v-else>No reconciliation reports found</p>
</div>
<div v-if="activeReport">
<h3>Reconciliation Report - {{ activeReport.start_date }} to {{ activeReport.end_date }}</h3>
<p>Bank Account: {{ getBankAccountName(activeReport.bank_account_id) }}</p>
<p>Status: {{ activeReport.status }}</p>
<div class="match-quality-filters">
<label>
<input type="checkbox" v-model="filterMatches" value="high"> High Confidence (>80%)
</label>
<label>
<input type="checkbox" v-model="filterMatches" value="medium"> Medium Confidence (50-80%)
</label>
</div>
<table>
<thead>
<tr>
<th>Bank Transaction</th>
<th>Match Score</th>
<th>Journal Entry</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="match in filteredMatches">
<td>
{{ match.transaction_date }}<br>
{{ match.transaction_description }}<br>
<strong>{{ formatCurrency(match.transaction_amount) }}</strong>
</td>
<td>
<div class="score-bar" :style="{width: match.match_score + '%'}"></div>
{{ match.match_score }}%
</td>
<td>
{{ match.journal_entry_date }}<br>
{{ match.journal_entry_description }}
</td>
<td>
<button @click="acceptMatch(match)">Accept</button>
<button @click="rejectMatch(match)" class="danger">Reject</button>
</td>
</tr>
</tbody>
</table>
<div class="report-actions">
<button @click="finalizeReconciliation" class="primary" v-if="activeReport.status === 'draft'">Finalize Reconciliation</button>
<button @click="cancelReconciliation">Back to Reports</button>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
activeTab: 'accounts',
accounts: [],
trialBalance: [],
newAccount: {
name: '',
code: '',
type: 'Asset'
},
bankAccounts: [],
newBankAccount: {
name: '',
bank_name: '',
account_number: '',
currency: 'USD'
},
selectedBankAccount: null,
selectedBankAccountForReconciliation: null,
fileData: null,
importResult: null,
unreconciledTransactions: [],
selectedJournalEntries: {},
journalEntries: [],
reconciliationReports: [],
newReport: {
bank_account_id: null,
start_date: '',
end_date: ''
},
activeReport: null,
filterMatches: ['high', 'medium']
}
},
computed: {
filteredMatches() {
if (!this.activeReport) return [];
return this.activeReport.matches.filter(match => {
if (this.filterMatches.includes('high') && match.match_score >= 80) return true;
if (this.filterMatches.includes('medium') && match.match_score >= 50 && match.match_score < 80) return true;
return false;
});
}
},
mounted() {
this.fetchAccounts();
this.fetchTrialBalance();
this.fetchBankAccounts();
this.fetchJournalEntries();
this.fetchReconciliationReports();
},
methods: {
formatCurrency(value) {
if (value === undefined || value === null) return '$0.00';
return '$' + parseFloat(value).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
},
fetchAccounts() {
axios.get('/api/accounts')
.then(response => {
this.accounts = response.data;
});
},
fetchTrialBalance() {
axios.get('/api/trial_balance')
.then(response => {
this.trialBalance = response.data;
});
},
addAccount() {
axios.post('/api/add_account', this.newAccount)
.then(response => {
if (response.data.status === 'success') {
this.fetchAccounts();
this.newAccount = { name: '', code: '', type: 'Asset' };
}
});
},
fetchBankAccounts() {
axios.get('/api/bank_accounts')
.then(response => {
this.bankAccounts = response.data;
});
},
addBankAccount() {
axios.post('/api/add_bank_account', this.newBankAccount)
.then(response => {
if (response.data.status === 'success') {
this.fetchBankAccounts();
this.newBankAccount = { name: '', bank_name: '', account_number: '', currency: 'USD' };
}
});
},
handleFileUpload(event) {
this.fileData = event.target.files[0];
},
importTransactions() {
if (!this.fileData || !this.selectedBankAccount) return;
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
const lines = content.split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const transactions = [];
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const values = lines[i].split(',');
const txn = {};
for (let j = 0; j < headers.length; j++) {
txn[headers[j]] = values[j] ? values[j].trim() : '';
}
// Convert amount to number
if (txn.amount) {
txn.amount = parseFloat(txn.amount);
}
transactions.push(txn);
}
axios.post('/api/import_bank_transactions', {
bank_account_id: this.selectedBankAccount,
transactions: transactions
}).then(response => {
this.importResult = response.data;
this.fileData = null;
document.querySelector('input[type="file"]').value = '';
if (this.selectedBankAccount === this.selectedBankAccountForReconciliation) {
this.fetchUnreconciledTransactions();
}
});
};
reader.readAsText(this.fileData);
},
fetchUnreconciledTransactions() {
if (this.selectedBankAccountForReconciliation) {
axios.get(`/api/get_unreconciled_transactions?bank_account_id=${this.selectedBankAccountForReconciliation}`)
.then(response => {
this.unreconciledTransactions = response.data;
});
}
},
fetchJournalEntries() {
axios.get('/api/journal_entries')
.then(response => {
this.journalEntries = response.data.map(entry => {
const debitLines = entry.lines.filter(line => line.is_debit);
const creditLines = entry.lines.filter(line => !line.is_debit);
return {
id: entry.id,
date: entry.date,
description: entry.description,
amount: debitLines.reduce((sum, line) => sum + line.amount, 0),
lines: entry.lines
};
});
});
},
potentialMatches(txn) {
// Simple matching by amount and date
const txnDate = new Date(txn.date);
return this.journalEntries.filter(entry => {
const entryDate = new Date(entry.date);
return Math.abs(entry.amount - txn.amount) < 0.01 &&
txnDate.toDateString() === entryDate.toDateString();
});
},
reconcile(txnId) {
const journalEntryId = this.selectedJournalEntries[txnId];
if (!journalEntryId) return;
axios.post('/api/reconcile_transaction', {
transaction_id: txnId,
journal_entry_id: journalEntryId
}).then(response => {
if (response.data.status === 'success') {
this.fetchUnreconciledTransactions();
this.selectedJournalEntries[txnId] = null;
} else {
alert(response.data.message);
}
});
},
fetchReconciliationReports() {
axios.get('/api/reconciliation_reports')
.then(response => {
this.reconciliationReports = response.data;
});
},
createReconciliationReport() {
axios.post('/api/create_reconciliation_report', this.newReport)
.then(response => {
if (response.data.status === 'success') {
this.loadReport(response.data.report_id);
this.fetchReconciliationReports();
}
});
},
loadReport(reportId) {
axios.get(`/api/get_reconciliation_report?report_id=${reportId}`)
.then(response => {
if (response.data.status === 'success') {
this.activeReport = response.data.report;
this.activeTab = 'reports';
}
});
},
acceptMatch(match) {
axios.post('/api/reconcile_transaction', {
transaction_id: match.transaction_id,
journal_entry_id: match.journal_entry_id
}).then(response => {
if (response.data.status === 'success') {
this.loadReport(this.activeReport.id);
}
});
},
rejectMatch(match) {
axios.delete(`/api/reconciliation_match/${match.id}`)
.then(() => {
this.loadReport(this.activeReport.id);
});
},
finalizeReconciliation() {
axios.post('/api/finalize_reconciliation', {
report_id: this.activeReport.id
}).then(response => {
if (response.data.status === 'success') {
this.activeReport = null;
this.fetchReconciliationReports();
this.fetchUnreconciledTransactions();
}
});
},
cancelReconciliation() {
this.activeReport = null;
},
getBankAccountName(accountId) {
const account = this.bankAccounts.find(a => a.id === accountId);
return account ? `${account.bank_name} - ${account.account_number}` : '';
},
viewReconciliation(accountId) {
this.selectedBankAccountForReconciliation = accountId;
this.fetchUnreconciledTransactions();
this.activeTab = 'banking';
}
}
});
</script>
</body>
</html>

2
config.py Normal file
View File

@@ -0,0 +1,2 @@
# Configuration settings
DATABASE_URI = "sqlite:///accounting.db"

85
server.py Normal file
View File

@@ -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()