Files
our-claude-skills/custom-skills/96-ourdigital-estimate-engine/scripts/render.py
Andrew Yim c9bdbb57f7 Extract ourdigital-estimate-engine; presales-seo now calls it
New skill 96-ourdigital-estimate-engine: method-aware quoting engine
(effort / coaching / procurement) with universal rate_card + per-service
catalog. Real catalogs: seo (effort), education (coaching); stubs:
digital_ads, digital_branding. Validated to reproduce real quotes —
SEO basic ₩10.5M / treatment ₩25.0M, SHR chain ₩29.5M, L'Escape basic
₩10.5M, GA4/GTM coaching ₩1,570,000, procurement +15%.

Refactor 95-ourdigital-presales-seo: remove rate_card.yaml, sow_templates.yaml,
estimate.py (migrated to engine); add findings_to_scope.py; Stage 5 now maps
findings→scope.json and calls the engine CLI. build_deck/kg_query unchanged;
end-to-end validated on SHR (29.5M) + deck renders engine estimate.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:54:11 +09:00

124 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Render an enriched quote dict to 05_estimate_ko.md, 05_estimate.xlsx, data/estimate.json.
Handles all kinds: effort (modules), coaching (lessons), procurement (items)."""
import json
import os
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill
ROLE_KO = {"ceo": "대표", "evp": "전무", "svp": "상무", "technical_advisor": "기술고문",
"director": "이사", "senior_manager": "부장", "deputy_manager": "차장",
"manager": "과장", "assistant_manager": "대리", "junior": "주임",
"associate": "사원", "intern": "인턴"}
NAVY = "11243D"
def won(n):
return f"{int(round(n)):,}"
def _header_md(q):
svc = q.get("label", q["service"])
if q["kind"] == "effort":
svc += f" ({q['tier']})"
L = [f"# 견적서 — {q['prospect'].get('name', '(prospect)')}", "",
f"- **제공 서비스**: {svc}",
f"- **견적번호**: {q['quote_no']} · **작성일**: {q['date']} · **유효기간**: ~{q['valid_until']}",
f"- **공급자**: {q['company']['legal_name']} (대표 {q['company']['ceo']}, {q['company']['contact']})"]
if q.get("stub"):
L.append("- ⚠️ **STUB 카탈로그** — 실제 단가 미반영(placeholder). 실제 견적 자료로 교체 필요.")
L.append("")
return L
def _md(q, path):
L = _header_md(q)
if q["kind"] == "effort":
if q["scope"]["hours_multiplier"] != 1.0:
dl = "브랜드/템플릿" if q["scope"]["driver"] == "subbrands_total" else "프로퍼티"
L += [f"> 규모 반영: {dl} {q['scope']['driver_count']}개 기준 On-page 업무시간 ×{q['scope']['hours_multiplier']:g}", ""]
L += ["## 견적 내역", "", "| 구분 | 세부 업무 | 담당 | 시간(h) | 합계 |", "|---|---|:--:|--:|--:|"]
for m in q["modules"]:
for i, t in enumerate(m["tasks"]):
grp = m["name"] if i == 0 else ""
mark = " *" if t["scaled"] else ""
L.append(f"| {grp} | {t['task']}{mark} | {ROLE_KO.get(t['role'], t['role'])} | {t['hours']:g} | {won(t['amount'])} |")
L.append(f"| | **{m['name']} 소계** | | | **{won(m['subtotal'])}** |")
L += ["", f"- 합계: **{won(q['subtotal_sum'])}** · 청구율 {int(q['billing_rate']*100)}% · 일8h/월4주",
f"- **제안가(절사 적용): {won(q['proposal'])}** ({q['terms']['vat']})"]
elif q["kind"] == "coaching":
L += ["## 견적 내역", "", "| 구분 | 세부 | 방식 | 시간 | 단가 | 합계 |", "|---|---|:--:|--:|--:|--:|"]
for it in q["lessons"]:
L.append(f"| {it['subject']} | {it['title']} | {it['type']} | {it['hours']:g} | {won(it['unit_price'])} | {won(it['amount'])} |")
L += ["", f"- 합계: **{won(q['subtotal_sum'])}** ({q['pricing_mode']} 단가, 수강생 {q['students']}명)"]
if q["discount_rate"]:
L.append(f"- 할인({int(q['discount_rate']*100)}%): -{won(q['discount_amount'])}")
L.append(f"- **제안가: {won(q['proposal'])}** ({q['terms']['vat']})")
elif q["kind"] == "procurement":
L += ["## 조달 내역", "", "| 항목 | 단가 | 수량 | 수수료 | 합계 |", "|---|--:|--:|:--:|--:|"]
for it in q["items"]:
L.append(f"| {it['label']} | {it['unit_cost']:,}{it['currency']} | {it['qty']:g} | {int(it['markup']*100)}% | {won(it['amount'])} |")
L += ["", f"- **합계: {won(q['proposal'])}** ({q['terms']['vat']})"]
L += ["", "---", f"> {q['disclaimer']}"]
if q["kind"] == "effort" and any(t["scaled"] for m in q["modules"] for t in m["tasks"]):
L.append("> \\* 포트폴리오 규모에 따라 업무시간이 스케일된 항목.")
with open(path, "w", encoding="utf-8") as fh:
fh.write("\n".join(L) + "\n")
def _xlsx(q, path):
wb = Workbook()
ws = wb.active
ws.title = "견적"
ws.append([f"견적서 — {q['prospect'].get('name', '(prospect)')} ({q.get('label', q['service'])})"])
ws.append([f"견적번호 {q['quote_no']}", f"작성일 {q['date']}", f"유효 ~{q['valid_until']}", q["terms"]["vat"]])
ws.append([])
def hdr(cols):
ws.append(cols)
for c in range(1, len(cols) + 1):
cell = ws.cell(row=ws.max_row, column=c)
cell.fill = PatternFill("solid", fgColor=NAVY)
cell.font = Font(color="FFFFFF", bold=True)
if q["kind"] == "effort":
hdr(["구분", "세부 업무", "담당", "시간(h)", "합계(원)"])
for m in q["modules"]:
for i, t in enumerate(m["tasks"]):
ws.append([m["name"] if i == 0 else "", t["task"], ROLE_KO.get(t["role"], t["role"]), t["hours"], int(round(t["amount"]))])
ws.append(["", f"{m['name']} 소계", "", "", int(round(m["subtotal"]))])
ws.append([])
ws.append(["", "제안가(절사 적용)", "", "", int(q["proposal"])])
widths = [22, 40, 8, 8, 16]
elif q["kind"] == "coaching":
hdr(["구분", "세부", "방식", "시간", "단가", "합계(원)"])
for it in q["lessons"]:
ws.append([it["subject"], it["title"], it["type"], it["hours"], int(it["unit_price"]), int(round(it["amount"]))])
ws.append([])
ws.append(["", "제안가", "", "", "", int(q["proposal"])])
widths = [18, 34, 8, 6, 12, 14]
else: # procurement
hdr(["항목", "단가", "수량", "수수료", "합계(원)"])
for it in q["items"]:
ws.append([it["label"], it["unit_cost"], it["qty"], it["markup"], int(round(it["amount"]))])
ws.append([])
ws.append(["", "합계", "", "", int(q["proposal"])])
widths = [34, 14, 8, 10, 16]
ws.cell(row=ws.max_row, column=2).font = Font(bold=True)
ws.cell(row=ws.max_row, column=len(widths)).font = Font(bold=True, color="C0392B")
ws.append([])
ws.append([q["disclaimer"]])
for idx, w in enumerate(widths, 1):
ws.column_dimensions[chr(64 + idx)].width = w
wb.save(path)
def write_all(q, out_dir):
os.makedirs(out_dir, exist_ok=True)
ddir = os.path.join(out_dir, "data")
os.makedirs(ddir, exist_ok=True)
with open(os.path.join(ddir, "estimate.json"), "w", encoding="utf-8") as fh:
json.dump(q, fh, ensure_ascii=False, indent=2)
_md(q, os.path.join(out_dir, "05_estimate_ko.md"))
_xlsx(q, os.path.join(out_dir, "05_estimate.xlsx"))