105 lines
3.3 KiB
Python
105 lines
3.3 KiB
Python
"""Proposal drafter: generate professional proposals from estimates."""
|
|
import json, sys, os
|
|
from datetime import datetime, timedelta
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
from lib.db import get_db, row_to_dict, rows_to_list
|
|
|
|
TEMPLATE = """# Proposal — {client_name}
|
|
## {job_description}
|
|
|
|
**Prepared by:** Kellow Construction
|
|
**Date:** {date}
|
|
**Valid for:** 30 days
|
|
|
|
---
|
|
|
|
## Scope of Work
|
|
|
|
{scope_items}
|
|
|
|
## Investment Summary
|
|
|
|
| Category | Amount |
|
|
|----------|--------|
|
|
| Labor ({labor_hours} hrs @ ${labor_rate}/hr) | ${labor_cost:,.2f} |
|
|
| Materials | ${materials_cost:,.2f} |
|
|
| Subtotal | ${subtotal:,.2f} |
|
|
| Overhead & Profit ({markup_percent}%) | ${markup:,.2f} |
|
|
| **Total** | **${total_cost:,.2f}** |
|
|
|
|
## Timeline
|
|
|
|
Estimated duration: {timeline}
|
|
Projected start: Upon approval and permitting
|
|
|
|
## Payment Terms
|
|
|
|
- 30% deposit upon contract signing
|
|
- Progress payments at milestones
|
|
- Final 10% upon completion and walkthrough
|
|
|
|
## Exclusions
|
|
|
|
- Permits and fees (handled separately)
|
|
- Unforeseen structural issues
|
|
- Owner-supplied materials
|
|
- Landscaping and exterior work (unless specified)
|
|
|
|
## Warranty
|
|
|
|
Kellow Construction provides a 1-year workmanship warranty on all labor performed.
|
|
|
|
---
|
|
|
|
*This proposal is valid for 30 days from the date above. Acceptance constitutes agreement to the terms outlined herein.*
|
|
"""
|
|
|
|
def draft(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"}
|
|
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())
|
|
conn.close()
|
|
|
|
labor_cost = est["labor_hours"] * est["labor_rate"]
|
|
subtotal = labor_cost + est["materials_cost"]
|
|
markup = subtotal * (est["markup_percent"] / 100)
|
|
|
|
# Build scope items
|
|
scope_lines = []
|
|
for item in items:
|
|
scope_lines.append(f"- {item['description']}: {item['quantity']} {item['unit']} @ ${item['unit_cost']:.2f}/{item['unit']}")
|
|
|
|
# Estimate timeline from labor hours
|
|
weeks = max(1, int(est["labor_hours"] / 40))
|
|
timeline = f"{weeks} week{'s' if weeks > 1 else ''}"
|
|
|
|
proposal = TEMPLATE.format(
|
|
client_name=job.get("client_name", "Client") if job else "Client",
|
|
job_description=job.get("description", "Project") if job else "Project",
|
|
date=datetime.now().strftime("%B %d, %Y"),
|
|
scope_items="\n".join(scope_lines) if scope_lines else "- As discussed",
|
|
labor_hours=round(est["labor_hours"], 1),
|
|
labor_rate=est["labor_rate"],
|
|
labor_cost=labor_cost,
|
|
materials_cost=est["materials_cost"],
|
|
subtotal=subtotal,
|
|
markup_percent=est["markup_percent"],
|
|
markup=markup,
|
|
total_cost=est["total_cost"],
|
|
timeline=timeline,
|
|
)
|
|
return {"proposal": proposal, "estimate_id": estimate_id, "total": est["total_cost"]}
|
|
|
|
if __name__ == "__main__":
|
|
from lib.db import init_db; init_db()
|
|
if len(sys.argv) < 2:
|
|
print(json.dumps({"error": "usage: proposal_drafter.py <estimate_id>"}))
|
|
else:
|
|
result = draft(sys.argv[1])
|
|
if "proposal" in result:
|
|
print(result["proposal"])
|
|
else:
|
|
print(json.dumps(result))
|