Cybrkyd's Git Repositories

python-budget - commit: 7337e40

commit 7337e40d275fa9efc5c68ba32d567a41dcc541c6145fce7a5c272afff6e10181
author Cybrkyd <git@cybrkyd.com> 2026-05-30 10:34:37 +0100
committer Cybrkyd <git@cybrkyd.com> 2026-05-30 10:34:37 +0100

Commit Message

Budget table

- New database table plus table on UI with delete action
- Column name desc_2 changed to details

📊 Diffstat

budget.py 184
1 files changed, 145 insertions(+), 39 deletions(-)

Diff

diff --git a/budget.py b/budget.py
index a16bd03..74a4c53 100644
--- a/budget.py
+++ b/budget.py
@@ -16,10 +16,17 @@ def init_db():
debit REAL DEFAULT 0,
amount REAL NOT NULL,
description TEXT,
- desc_2 TEXT,
+ details TEXT,
created_at TEXT NOT NULL
)
""")
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS budget (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ category TEXT NOT NULL UNIQUE,
+ planned_amount REAL NOT NULL
+ )
+ """)
def get_balance():
with sqlite3.connect(DB) as conn:
@@ -39,6 +46,24 @@ def get_month_total():
""", (start.isoformat(), end.isoformat()))
return cur.fetchone()[0]
+ def get_budget_items():
+ with sqlite3.connect(DB) as conn:
+ conn.row_factory = sqlite3.Row
+ cur = conn.execute("SELECT id, category, planned_amount FROM budget ORDER BY category")
+ return cur.fetchall()
+
+ def upsert_budget(category, planned_amount):
+ with sqlite3.connect(DB) as conn:
+ conn.execute("""
+ INSERT INTO budget (category, planned_amount)
+ VALUES (?, ?)
+ ON CONFLICT(category) DO UPDATE SET planned_amount = excluded.planned_amount
+ """, (category, planned_amount))
+
+ def delete_budget(category):
+ with sqlite3.connect(DB) as conn:
+ conn.execute("DELETE FROM budget WHERE category = ?", (category,))
+
HTML = """
<!doctype html>
<html>
@@ -59,9 +84,28 @@ input, select, button {{
.card {{
border: 1px solid #ccc;
- padding: 16px;
+ padding: 10px;
margin-top: 20px;
- max-width: 50%;
+ }}
+
+ table {{
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 15px;
+ }}
+
+ th, td {{
+ text-align: left;
+ padding: 8px;
+ border-bottom: 1px solid #eee;
+ }}
+
+ th {{
+ border-bottom: 2px solid #ccc;
+ }}
+
+ .actions {{
+ display: inline;
}}
</style>
<script>
@@ -104,12 +148,12 @@ function autoFillAmount() {{
<option value="Virgin Media">Virgin Media</option>
<option value="Water">Water</option>
</select>
- <input name="desc_2" type="text" placeholder="2nd description (Optional)">
+ <input name="details" type="text" placeholder="Details (Optional)">
<button type="submit">Add</button>
</form>
<div class="card">
- <h2>All Time Balance</h2>
+ <h2>Current Balance</h2>
<p>{balance:.2f}</p>
</div>
@@ -118,56 +162,118 @@ function autoFillAmount() {{
<p>{month:.2f}</p>
</div>
+ <div class="card">
+ <h2>Monthly Budget Targets</h2>
+ <form method="POST" action="/budget/add">
+ <input name="category" type="text" placeholder="Item" required>
+ <input type="text" name="planned_amount" placeholder="Planned amount" required>
+ <button type="submit">Save Budget</button>
+ </form>
+
+ <table>
+ <tr>
+ <th>Category</th>
+ <th>Planned</th>
+ <th>Actions</th>
+ </tr>
+ {budget_rows}
+ </table>
+ </div>
+
</body>
</html>
"""
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
- if self.path != "/":
- self.send_error(404)
- return
+ if self.path == "/":
+ balance = get_balance()
+ month = get_month_total()
+ budget_items = get_budget_items()
- balance = get_balance()
- month = get_month_total()
+ budget_rows = ""
+ for item in budget_items:
+ budget_rows += f"""
+ <tr>
+ <td>{item['category']}</td>
+ <td>{item['planned_amount']:.2f}</td>
+ <td>
+ <form method="POST" action="/budget/delete" class="actions">
+ <input type="hidden" name="category" value="{item['category']}">
+ <button type="submit" onclick="return confirm('Delete budget for {item['category']}?')">Delete</button>
+ </form>
+ </td>
+ </tr>
+ """
- page = HTML.format(balance=balance, month=month)
+ page = HTML.format(balance=balance, month=month, budget_rows=budget_rows)
- self.send_response(200)
- self.send_header("Content-type", "text/html")
- self.end_headers()
- self.wfile.write(page.encode())
+ self.send_response(200)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+ self.wfile.write(page.encode())
+ else:
+ self.send_error(404)
def do_POST(self):
- if self.path != "/add":
- self.send_error(404)
- return
+ if self.path == "/add":
+ length = int(self.headers["Content-Length"])
+ body = self.rfile.read(length).decode()
+ data = parse_qs(body)
- length = int(self.headers["Content-Length"])
- body = self.rfile.read(length).decode()
- data = parse_qs(body)
+ amount = float(data["amount"][0])
+ description = data.get("description", [""])[0]
+ details = data.get("details", [""])[0]
- amount = float(data["amount"][0])
- description = data.get("description", [""])[0]
- desc_2 = data.get("desc_2", [""])[0]
+ # Determine if the amount should be credit or debit
+ if amount > 0:
+ credit = amount
+ debit = 0
+ else:
+ credit = 0
+ debit = abs(amount)
- # Determine if the amount should be credit or debit
- if amount > 0:
- credit = amount
- debit = 0
- else:
- credit = 0
- debit = abs(amount)
+ with sqlite3.connect(DB) as conn:
+ conn.execute(
+ "INSERT INTO entries(credit, debit, amount, description, details, created_at) VALUES (?, ?, ?, ?, ?, ?)",
+ (credit, debit, amount, description, details, datetime.now().date().isoformat())
+ )
- with sqlite3.connect(DB) as conn:
- conn.execute(
- "INSERT INTO entries(credit, debit, amount, description, desc_2, created_at) VALUES (?, ?, ?, ?, ?, ?)",
- (credit, debit, amount, description, desc_2, datetime.now().date().isoformat())
- )
+ self.send_response(303)
+ self.send_header("Location", "/")
+ self.end_headers()
- self.send_response(303)
- self.send_header("Location", "/")
- self.end_headers()
+ elif self.path == "/budget/add":
+ length = int(self.headers["Content-Length"])
+ body = self.rfile.read(length).decode()
+ data = parse_qs(body)
+
+ category = data.get("category", [""])[0]
+ planned_amount = float(data.get("planned_amount", [0])[0])
+
+ if category and planned_amount:
+ upsert_budget(category, planned_amount)
+
+ self.send_response(303)
+ self.send_header("Location", "/")
+ self.end_headers()
+
+ elif self.path == "/budget/delete":
+ length = int(self.headers["Content-Length"])
+ body = self.rfile.read(length).decode()
+ data = parse_qs(body)
+
+ category = data.get("category", [""])[0]
+
+ if category:
+ delete_budget(category)
+
+ self.send_response(303)
+ self.send_header("Location", "/")
+ self.end_headers()
+
+ else:
+ self.send_error(404)
def open_browser():
webbrowser.open("http://localhost:8080")