Add real digital_ads + digital_branding catalogs
Built from real D.intelligence docs (not the 견적 자료 folder, which has only GA4/GTM + education — confirmed): - digital_branding: TNS 유학원 디지털 브랜딩 진단 컨설팅 → ₩9,000,000 (5 fixed stages) - digital_ads: 디하이브 디지털 광고·퍼포먼스 마케팅 대행 계약 → ₩6,000,000/월 retainer (media-spend commission % is per-deal, kept as a parameter — not invented) effort method now supports fixed-amount tasks (Unit Cost) and monthly retainers (unit: monthly); render shows 고정/—//월 and retainer/commission notes. Validated: branding ₩9.0M, ads ₩6.0M/월; no regression (SEO 25.0M, coaching 1.57M). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,16 +57,26 @@ def build(scope, rate, catalog):
|
||||
mult, driver, dcount = _scope_mult(rate, signals)
|
||||
roles = rate["role_rates"]
|
||||
|
||||
modules, grand = [], 0.0
|
||||
modules, grand, recurring = [], 0.0, False
|
||||
for mod in t["modules"]:
|
||||
tasks, sub = [], 0.0
|
||||
for task in mod["tasks"]:
|
||||
applied = mult if (task.get("scale") and mult != 1.0) else 1.0
|
||||
hours = round(task["hours"] * applied, 1)
|
||||
rr = roles[task["role"]]
|
||||
amt = rr * billing * hours
|
||||
tasks.append({"task": task["task"], "desc": task.get("desc", ""), "role": task["role"],
|
||||
"role_rate": rr, "hours": hours, "amount": amt, "scaled": applied != 1.0})
|
||||
unit = task.get("unit", "one_time")
|
||||
if unit == "monthly":
|
||||
recurring = True
|
||||
if "fixed" in task: # quoted lump sum / Unit Cost (no labor-hour basis)
|
||||
amt = task["fixed"] * applied
|
||||
tasks.append({"task": task["task"], "desc": task.get("desc", ""), "role": "fixed",
|
||||
"role_rate": None, "hours": None, "amount": amt,
|
||||
"scaled": applied != 1.0, "unit": unit})
|
||||
else:
|
||||
hours = round(task["hours"] * applied, 1)
|
||||
rr = roles[task["role"]]
|
||||
amt = rr * billing * hours
|
||||
tasks.append({"task": task["task"], "desc": task.get("desc", ""), "role": task["role"],
|
||||
"role_rate": rr, "hours": hours, "amount": amt,
|
||||
"scaled": applied != 1.0, "unit": unit})
|
||||
sub += amt
|
||||
modules.append({"name": mod["name"], "subtotal": sub, "tasks": tasks})
|
||||
grand += sub
|
||||
@@ -75,7 +85,8 @@ def build(scope, rate, catalog):
|
||||
proposal = int(math.floor(grand / rounding) * rounding)
|
||||
return {
|
||||
"kind": "effort", "service": catalog["service"], "label": t.get("label", catalog["service"]),
|
||||
"tier": tier, "billing_rate": billing,
|
||||
"tier": tier, "billing_rate": billing, "recurring": recurring,
|
||||
"notes": t.get("notes", []),
|
||||
"scope": {"driver": driver, "driver_count": dcount,
|
||||
"properties_total": signals.get("properties_total", 0),
|
||||
"subbrands_total": signals.get("subbrands_total", 0), "hours_multiplier": mult},
|
||||
|
||||
@@ -42,10 +42,16 @@ def _md(q, path):
|
||||
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'])} |")
|
||||
role_lbl = "고정" if t["role"] == "fixed" else ROLE_KO.get(t["role"], t["role"])
|
||||
hrs = f"{t['hours']:g}" if t.get("hours") is not None else "—"
|
||||
umark = " /월" if t.get("unit") == "monthly" else ""
|
||||
L.append(f"| {grp} | {t['task']}{mark} | {role_lbl} | {hrs} | {won(t['amount'])}{umark} |")
|
||||
L.append(f"| | **{m['name']} 소계** | | | **{won(m['subtotal'])}** |")
|
||||
plabel = "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)"
|
||||
L += ["", f"- 합계: **{won(q['subtotal_sum'])}** · 청구율 {int(q['billing_rate']*100)}% · 일8h/월4주",
|
||||
f"- **제안가(절사 적용): {won(q['proposal'])}** ({q['terms']['vat']})"]
|
||||
f"- **{plabel}: {won(q['proposal'])}** ({q['terms']['vat']})"]
|
||||
if q.get("recurring"):
|
||||
L.append("- ※ '/월' 항목은 월 정기(retainer) 비용 — 계약 기간에 따라 합산.")
|
||||
elif q["kind"] == "coaching":
|
||||
L += ["## 견적 내역", "", "| 구분 | 세부 | 방식 | 시간 | 단가 | 합계 |", "|---|---|:--:|--:|--:|--:|"]
|
||||
for it in q["lessons"]:
|
||||
@@ -62,6 +68,8 @@ def _md(q, path):
|
||||
L += ["", "---", f"> {q['disclaimer']}"]
|
||||
if q["kind"] == "effort" and any(t["scaled"] for m in q["modules"] for t in m["tasks"]):
|
||||
L.append("> \\* 포트폴리오 규모에 따라 업무시간이 스케일된 항목.")
|
||||
for n in q.get("notes", []):
|
||||
L.append(f"> {n}")
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write("\n".join(L) + "\n")
|
||||
|
||||
@@ -85,10 +93,12 @@ def _xlsx(q, path):
|
||||
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"]))])
|
||||
role_lbl = "고정" if t["role"] == "fixed" else ROLE_KO.get(t["role"], t["role"])
|
||||
hrs = t["hours"] if t.get("hours") is not None else "—"
|
||||
ws.append([m["name"] if i == 0 else "", t["task"], role_lbl, hrs, int(round(t["amount"]))])
|
||||
ws.append(["", f"{m['name']} 소계", "", "", int(round(m["subtotal"]))])
|
||||
ws.append([])
|
||||
ws.append(["", "제안가(절사 적용)", "", "", int(q["proposal"])])
|
||||
ws.append(["", "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)", "", "", int(q["proposal"])])
|
||||
widths = [22, 40, 8, 8, 16]
|
||||
elif q["kind"] == "coaching":
|
||||
hdr(["구분", "세부", "방식", "시간", "단가", "합계(원)"])
|
||||
|
||||
Reference in New Issue
Block a user