"""Estimate generator: parse job description into structured estimate.""" import json, sys, os, re sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from lib.db import get_db, init_db from scripts.pricing_catalog import get_catalog, list_catalogs from scripts.project_mgmt import create_job, create_estimate # Common construction scope patterns → (category, unit, base_hours_per_unit) SCOPE_PATTERNS = { r"cabinet": ("finish_carpentry", "lf", 2.5), r"countertop": ("tile", "sf", 0.5), r"floor": ("flooring", "sf", 0.15), r"tile": ("tile", "sf", 0.25), r"paint": ("painting", "sf", 0.05), r"drywall": ("drywall", "sf", 0.1), r"electric": ("electrical", "ea", 4), r"plumb": ("plumbing", "ea", 6), r"frame|wall": ("framing", "lf", 1.5), r"demo": ("demolition", "sf", 0.08), r"roof": ("roofing", "sf", 0.12), r"concrete|foundation": ("concrete", "cy", 8), r"window": ("finish_carpentry", "ea", 3), r"door": ("finish_carpentry", "ea", 4), r"deck": ("framing", "sf", 0.3), r"bathroom": ("plumbing", "ea", 40), r"kitchen": ("general_labor", "ea", 120), } # Material cost estimates per unit MATERIAL_COSTS = { "cabinet": 250, "countertop": 75, "flooring": 8, "tile": 12, "paint": 0.50, "drywall": 3, "electrical": 150, "plumbing": 200, "framing": 5, "demolition": 2, "roofing": 6, "concrete": 180, "window": 600, "door": 400, "deck": 15, "bathroom": 3000, "kitchen": 8000, } def generate(description, job_id=None, catalog_id=None, markup_percent=20): """Generate estimate from text description.""" # Get pricing catalog if catalog_id: catalog = get_catalog(catalog_id) else: cats = list_catalogs() catalog = cats["catalogs"][0] if cats["catalogs"] else None rates = catalog.get("labor_rates", {}) if catalog else {} markups = catalog.get("material_markups", {}) if catalog else {} # Extract quantities from description sqft_match = re.search(r"(\d+)\s*(?:sq\s*ft|sqft|sf)", description, re.I) sqft = int(sqft_match.group(1)) if sqft_match else 200 # default # Build line items from description line_items = [] desc_lower = description.lower() matched = False for pattern, (category, unit, hours_per) in SCOPE_PATTERNS.items(): if re.search(pattern, desc_lower): matched = True qty = sqft if unit == "sf" else (sqft / 10 if unit == "lf" else 1) labor_rate = rates.get(category, 75) labor_hours = qty * hours_per labor_cost = labor_hours * labor_rate # Material cost mat_key = next((k for k in MATERIAL_COSTS if re.search(pattern, k)), None) mat_cost = MATERIAL_COSTS.get(mat_key, 10) * qty if mat_key else qty * 10 mat_markup = markups.get(category, 1.15) mat_cost *= mat_markup line_items.append({ "description": f"{category.replace('_',' ').title()} — {pattern.replace('|','/')}", "category": "labor", "quantity": round(labor_hours, 1), "unit": "hr", "unit_cost": labor_rate }) line_items.append({ "description": f"Materials — {pattern.replace('|','/')}", "category": "materials", "quantity": round(qty, 1), "unit": unit, "unit_cost": round(mat_cost / qty, 2) }) if not matched: # Generic estimate based on sqft line_items = [ {"description": "General Labor", "category": "labor", "quantity": sqft * 0.2, "unit": "hr", "unit_cost": rates.get("general_labor", 55)}, {"description": "Materials (general)", "category": "materials", "quantity": sqft, "unit": "sf", "unit_cost": 15}, ] # Create job if needed if not job_id: job_result = create_job("TBD", description=description) job_id = job_result["id"] # Create estimate result = create_estimate(job_id, line_items, markup_percent=markup_percent) result["line_items_count"] = len(line_items) result["description"] = description return result if __name__ == "__main__": init_db() if len(sys.argv) < 2: print(json.dumps({"error": "usage: estimate_generator.py [catalog_id] [job_id]"})) else: desc = sys.argv[1] cat_id = sys.argv[2] if len(sys.argv) > 2 else None jid = sys.argv[3] if len(sys.argv) > 3 else None print(json.dumps(generate(desc, job_id=jid, catalog_id=cat_id), indent=2))