Add customer and project management with receipt association

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 23:55:05 -06:00
parent 6e93b7f672
commit be2ac4eaf7
2 changed files with 370 additions and 6 deletions

124
server.py
View File

@@ -25,6 +25,7 @@ NC_DAV_ROOT = "" # set after login, e.g. /remote.php/dav/files/<user>/business-
NC_AUTH = ()
RECEIPTS_FILE = "receipts.json"
SETTINGS_FILE = "settings.json"
AUTH_FILE = "auth.json"
PHOTOS_DIR = "photos"
@@ -109,12 +110,33 @@ def save_receipts(receipts):
r.raise_for_status()
def load_settings():
"""Read settings.json from Nextcloud. Returns dict with customers and projects."""
r = nc_get(SETTINGS_FILE)
if r.status_code == 404:
return {"customers": [], "projects": []}
r.raise_for_status()
return r.json()
def save_settings(data):
"""Write settings.json to Nextcloud."""
payload = json.dumps(data, indent=2).encode()
r = nc_put(SETTINGS_FILE, payload, "application/json")
r.raise_for_status()
def build_excel(receipts):
"""Build an .xlsx file from the receipts list. Returns bytes."""
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Receipts"
# Load settings for project/customer name lookups
settings = load_settings()
project_map = {p["id"]: p for p in settings.get("projects", [])}
customer_map = {c["id"]: c for c in settings.get("customers", [])}
# Styles
header_font = Font(bold=True, color="FFFFFF", size=11)
header_fill = PatternFill("solid", fgColor="1A1A2E")
@@ -125,8 +147,8 @@ def build_excel(receipts):
money_fmt = '#,##0.00'
# Headers
headers = ["Date", "Amount ($)", "Category", "Note"]
col_widths = [14, 14, 14, 40]
headers = ["Date", "Amount ($)", "Category", "Customer", "Project", "Note"]
col_widths = [14, 14, 14, 20, 20, 40]
for col_idx, (label, width) in enumerate(zip(headers, col_widths), 1):
cell = ws.cell(row=1, column=col_idx, value=label)
cell.font = header_font
@@ -143,7 +165,21 @@ def build_excel(receipts):
amt_cell.border = thin_border
ws.cell(row=row_idx, column=3,
value=r.get("category", "").capitalize()).border = thin_border
ws.cell(row=row_idx, column=4,
# Customer and Project columns
project_name = ""
customer_name = ""
pid = r.get("projectId", "")
if pid and pid in project_map:
proj = project_map[pid]
project_name = proj.get("name", "")
cid = proj.get("customerId", "")
if cid and cid in customer_map:
customer_name = customer_map[cid].get("name", "")
ws.cell(row=row_idx, column=4, value=customer_name).border = thin_border
ws.cell(row=row_idx, column=5, value=project_name).border = thin_border
ws.cell(row=row_idx, column=6,
value=r.get("note", "")).border = thin_border
# Total row
@@ -310,6 +346,17 @@ class Handler(BaseHTTPRequestHandler):
self._send_error(500, str(e))
return
# GET /api/settings
if self.path == "/api/settings":
if not self._check_session():
return
try:
settings = load_settings()
self._send_json(settings)
except Exception as e:
self._send_error(502, str(e))
return
# GET /api/photos/<id>
m = re.fullmatch(r"/api/photos/([A-Za-z0-9_-]+)", self.path)
if m:
@@ -385,6 +432,46 @@ class Handler(BaseHTTPRequestHandler):
self._send_error(500, str(e))
return
# POST /api/customers — upsert a customer
if self.path == "/api/customers":
if not self._check_session():
return
try:
data = json.loads(self._read_body())
settings = load_settings()
customers = settings.get("customers", [])
idx = next((i for i, c in enumerate(customers) if c["id"] == data["id"]), None)
if idx is not None:
customers[idx] = data
else:
customers.append(data)
settings["customers"] = customers
save_settings(settings)
self._send_json(data, 200)
except Exception as e:
self._send_error(500, str(e))
return
# POST /api/projects — upsert a project
if self.path == "/api/projects":
if not self._check_session():
return
try:
data = json.loads(self._read_body())
settings = load_settings()
projects = settings.get("projects", [])
idx = next((i for i, p in enumerate(projects) if p["id"] == data["id"]), None)
if idx is not None:
projects[idx] = data
else:
projects.append(data)
settings["projects"] = projects
save_settings(settings)
self._send_json(data, 200)
except Exception as e:
self._send_error(500, str(e))
return
# POST /api/receipts — upsert a receipt
if self.path == "/api/receipts":
if not self._check_session():
@@ -434,6 +521,37 @@ class Handler(BaseHTTPRequestHandler):
# --- DELETE --------------------------------------------------------------
def do_DELETE(self):
# DELETE /api/customers/<id>
m = re.fullmatch(r"/api/customers/([A-Za-z0-9_-]+)", self.path)
if m:
if not self._check_session():
return
cid = m.group(1)
try:
settings = load_settings()
settings["customers"] = [c for c in settings.get("customers", []) if c["id"] != cid]
settings["projects"] = [p for p in settings.get("projects", []) if p.get("customerId") != cid]
save_settings(settings)
self._send_json({"ok": True})
except Exception as e:
self._send_error(500, str(e))
return
# DELETE /api/projects/<id>
m = re.fullmatch(r"/api/projects/([A-Za-z0-9_-]+)", self.path)
if m:
if not self._check_session():
return
pid = m.group(1)
try:
settings = load_settings()
settings["projects"] = [p for p in settings.get("projects", []) if p["id"] != pid]
save_settings(settings)
self._send_json({"ok": True})
except Exception as e:
self._send_error(500, str(e))
return
m = re.fullmatch(r"/api/receipts/([A-Za-z0-9_-]+)", self.path)
if m:
if not self._check_session():