103 lines
5.2 KiB
Python
103 lines
5.2 KiB
Python
|
|
"""Project management: jobs and estimates CRUD."""
|
||
|
|
import json, sys, os
|
||
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||
|
|
from lib.db import get_db, new_id, now, row_to_dict, rows_to_list
|
||
|
|
|
||
|
|
def create_job(client_name, address=None, description=None, jobtread_job_id=None, jobtread_customer_id=None):
|
||
|
|
jid = new_id()
|
||
|
|
conn = get_db()
|
||
|
|
conn.execute("""INSERT INTO jobs (id, client_name, address, description, jobtread_job_id, jobtread_customer_id, status, created_at, updated_at)
|
||
|
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||
|
|
(jid, client_name, address, description, jobtread_job_id, jobtread_customer_id, "proposal", now(), now()))
|
||
|
|
conn.commit(); conn.close()
|
||
|
|
return {"id": jid, "client_name": client_name, "status": "proposal"}
|
||
|
|
|
||
|
|
def list_jobs(status=None):
|
||
|
|
conn = get_db()
|
||
|
|
if status:
|
||
|
|
rows = conn.execute("SELECT * FROM jobs WHERE status=? ORDER BY created_at DESC", (status,)).fetchall()
|
||
|
|
else:
|
||
|
|
rows = conn.execute("SELECT * FROM jobs ORDER BY created_at DESC").fetchall()
|
||
|
|
conn.close()
|
||
|
|
return {"jobs": rows_to_list(rows)}
|
||
|
|
|
||
|
|
def get_job(job_id):
|
||
|
|
conn = get_db()
|
||
|
|
job = row_to_dict(conn.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone())
|
||
|
|
if not job: conn.close(); return {"error": "job not found"}
|
||
|
|
job["estimates"] = rows_to_list(conn.execute("SELECT * FROM estimates WHERE job_id=?", (job_id,)).fetchall())
|
||
|
|
job["invoices"] = rows_to_list(conn.execute("SELECT * FROM invoices WHERE job_id=?", (job_id,)).fetchall())
|
||
|
|
conn.close()
|
||
|
|
return job
|
||
|
|
|
||
|
|
def update_job(job_id, status=None, client_name=None, address=None, description=None):
|
||
|
|
conn = get_db()
|
||
|
|
updates = []
|
||
|
|
params = []
|
||
|
|
for field, val in [("status", status), ("client_name", client_name), ("address", address), ("description", description)]:
|
||
|
|
if val is not None:
|
||
|
|
updates.append(f"{field}=?"); params.append(val)
|
||
|
|
if updates:
|
||
|
|
updates.append("updated_at=?"); params.append(now())
|
||
|
|
params.append(job_id)
|
||
|
|
conn.execute(f"UPDATE jobs SET {','.join(updates)} WHERE id=?", params)
|
||
|
|
conn.commit()
|
||
|
|
conn.close()
|
||
|
|
return {"ok": True, "id": job_id}
|
||
|
|
|
||
|
|
def create_estimate(job_id, line_items, labor_hours=None, labor_rate=None, markup_percent=20):
|
||
|
|
eid = new_id()
|
||
|
|
conn = get_db()
|
||
|
|
materials_cost = 0
|
||
|
|
total_labor_hours = labor_hours or 0
|
||
|
|
# Calculate totals first
|
||
|
|
item_rows = []
|
||
|
|
for item in line_items:
|
||
|
|
lid = new_id()
|
||
|
|
total = item.get("quantity", 1) * item.get("unit_cost", 0)
|
||
|
|
item_rows.append((lid, eid, item.get("description",""), item.get("category",""), item.get("quantity",1), item.get("unit","ea"), item.get("unit_cost",0), total))
|
||
|
|
if item.get("category") == "labor":
|
||
|
|
total_labor_hours += item.get("quantity", 0)
|
||
|
|
else:
|
||
|
|
materials_cost += total
|
||
|
|
effective_rate = labor_rate or 85
|
||
|
|
labor_cost = total_labor_hours * effective_rate
|
||
|
|
subtotal = labor_cost + materials_cost
|
||
|
|
markup = subtotal * (markup_percent / 100)
|
||
|
|
total_cost = subtotal + markup
|
||
|
|
# Insert estimate first, then line items
|
||
|
|
conn.execute("""INSERT INTO estimates (id, job_id, labor_hours, labor_rate, materials_cost, markup_percent, total_cost, status, created_at)
|
||
|
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||
|
|
(eid, job_id, total_labor_hours, effective_rate, materials_cost, markup_percent, total_cost, "draft", now()))
|
||
|
|
for row in item_rows:
|
||
|
|
conn.execute("INSERT INTO line_items (id, estimate_id, description, category, quantity, unit, unit_cost, total) VALUES (?,?,?,?,?,?,?,?)", row)
|
||
|
|
conn.commit(); conn.close()
|
||
|
|
return {"id": eid, "job_id": job_id, "labor_hours": total_labor_hours, "labor_rate": effective_rate,
|
||
|
|
"materials_cost": materials_cost, "markup_percent": markup_percent, "total_cost": total_cost}
|
||
|
|
|
||
|
|
def get_estimate(estimate_id):
|
||
|
|
conn = get_db()
|
||
|
|
est = row_to_dict(conn.execute("SELECT * FROM estimates WHERE id=?", (estimate_id,)).fetchone())
|
||
|
|
if not est: conn.close(); return {"error": "estimate not found"}
|
||
|
|
est["line_items"] = rows_to_list(conn.execute("SELECT * FROM line_items WHERE estimate_id=?", (estimate_id,)).fetchall())
|
||
|
|
job = row_to_dict(conn.execute("SELECT * FROM jobs WHERE id=?", (est["job_id"],)).fetchone())
|
||
|
|
est["job"] = job
|
||
|
|
conn.close()
|
||
|
|
return est
|
||
|
|
|
||
|
|
def update_estimate_status(estimate_id, status):
|
||
|
|
conn = get_db()
|
||
|
|
conn.execute("UPDATE estimates SET status=? WHERE id=?", (status, estimate_id))
|
||
|
|
conn.commit(); conn.close()
|
||
|
|
return {"ok": True, "id": estimate_id, "status": status}
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
from lib.db import init_db; init_db()
|
||
|
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "list-jobs"
|
||
|
|
if cmd == "create-job": print(json.dumps(create_job(sys.argv[2], sys.argv[3] if len(sys.argv)>3 else None, sys.argv[4] if len(sys.argv)>4 else None)))
|
||
|
|
elif cmd == "list-jobs": print(json.dumps(list_jobs(sys.argv[2] if len(sys.argv)>2 else None), indent=2))
|
||
|
|
elif cmd == "get-job": print(json.dumps(get_job(sys.argv[2]), indent=2))
|
||
|
|
elif cmd == "get-estimate": print(json.dumps(get_estimate(sys.argv[2]), indent=2))
|
||
|
|
elif cmd == "create-estimate": print(json.dumps(create_estimate(sys.argv[2], json.loads(sys.argv[3]), markup_percent=float(sys.argv[4]) if len(sys.argv)>4 else 20)))
|
||
|
|
else: print(json.dumps({"error": f"unknown: {cmd}"}))
|