41 lines
2.0 KiB
Python
41 lines
2.0 KiB
Python
"""Budget tracker: compare estimated vs actual costs."""
|
|
import json, sys, os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
from lib.db import get_db, row_to_dict, rows_to_list
|
|
|
|
def report(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"}
|
|
estimates = rows_to_list(conn.execute("SELECT * FROM estimates WHERE job_id=?", (job_id,)).fetchall())
|
|
logs = rows_to_list(conn.execute("SELECT * FROM daily_logs WHERE job_id=?", (job_id,)).fetchall())
|
|
invoices = rows_to_list(conn.execute("SELECT * FROM invoices WHERE job_id=?", (job_id,)).fetchall())
|
|
conn.close()
|
|
|
|
budgeted = sum(e["total_cost"] for e in estimates) if estimates else 0
|
|
actual_hours = sum(l["hours_worked"] for l in logs)
|
|
budgeted_hours = sum(e["labor_hours"] for e in estimates) if estimates else 0
|
|
invoiced = sum(i["amount_due"] for i in invoices)
|
|
paid = sum(i["amount_paid"] or 0 for i in invoices)
|
|
|
|
hours_variance = actual_hours - budgeted_hours
|
|
hours_pct = (hours_variance / budgeted_hours * 100) if budgeted_hours else 0
|
|
|
|
flags = []
|
|
if hours_pct > 10: flags.append(f"⚠️ Labor {hours_pct:.0f}% over budget")
|
|
if paid < invoiced * 0.5 and invoices: flags.append("⚠️ Less than 50% collected")
|
|
|
|
return {
|
|
"job_id": job_id, "client": job.get("client_name"),
|
|
"budget": {"total_estimated": budgeted, "budgeted_hours": budgeted_hours},
|
|
"actual": {"hours_worked": actual_hours, "log_entries": len(logs)},
|
|
"financial": {"invoiced": invoiced, "collected": paid, "outstanding": invoiced - paid},
|
|
"variance": {"hours": hours_variance, "hours_pct": round(hours_pct, 1)},
|
|
"flags": flags
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
from lib.db import init_db; init_db()
|
|
if len(sys.argv) < 2: print(json.dumps({"error": "usage: budget_tracker.py <job_id>"}))
|
|
else: print(json.dumps(report(sys.argv[1]), indent=2))
|