Add customer and project management with receipt association
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
124
server.py
124
server.py
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user