Initial commit: Handoff Pro MCP server for Kellow Construction
This commit is contained in:
102
scripts/project_mgmt.py
Normal file
102
scripts/project_mgmt.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""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}"}))
|
||||
Reference in New Issue
Block a user