Recalibrate estimate for SMB acceptability
Real-world feedback: list-rate calc overshot SMB-acceptable levels. - scaling: driver properties_total -> subbrands_total (chains share templates), cap x6.5 -> x2.0, applied to On-page only (Technical now fixed site-wide) - add 'smb' entry tier (lean hours @ 0.55 billing); 3-tier auto-select (smb/basic/treatment) by portfolio size; per-tier billing; --baseline smb enabled - docs (findings_to_service.md, SKILL.md) synced to 3-tier model Effect: SHR 25-property chain 71.5M -> 29.5M; SMB single hotel ~3.0M; basic/treatment still reproduce real 10.5M/25.0M at 1 property. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -286,7 +286,8 @@ def main():
|
||||
sc = EST.get("scope", {})
|
||||
note = f"청구율 {int(EST.get('billing_rate', 0.7) * 100)}% · 일8h/월4주 · SOW 기반"
|
||||
if sc.get("hours_multiplier", 1.0) != 1.0:
|
||||
note += f" · 프로퍼티 {sc.get('properties_total')}개 ×{sc['hours_multiplier']:g}"
|
||||
dl = "브랜드/템플릿" if sc.get("driver") == "subbrands_total" else "프로퍼티"
|
||||
note += f" · {dl} {sc.get('driver_count')}개 ×{sc['hours_multiplier']:g}"
|
||||
textbox(s, 0.85, 2.2 + 0.5 * rows, 11.6, 1.3, [
|
||||
[(note, 10, GREY, False)],
|
||||
[(EST.get("disclaimer", ""), 9, GREY, False)],
|
||||
|
||||
@@ -32,28 +32,34 @@ def won(n):
|
||||
return f"{int(round(n)):,}원"
|
||||
|
||||
|
||||
def scope_multiplier(rate, count):
|
||||
def scope_multiplier(rate, f):
|
||||
"""Sub-linear hours multiplier from the configured driver (default subbrands_total)."""
|
||||
sc = rate.get("scaling", {})
|
||||
driver = sc.get("driver", "subbrands_total")
|
||||
bands = sc.get("bands", [[1, 1.0]])
|
||||
c = max(int(count or 0), 1)
|
||||
count = max(int(f.get("entity", {}).get(driver, 0) or 0), 1)
|
||||
for mx, m in bands:
|
||||
if c <= mx:
|
||||
return float(m)
|
||||
return float(bands[-1][1])
|
||||
if count <= mx:
|
||||
return float(m), driver, count
|
||||
return float(bands[-1][1]), driver, count
|
||||
|
||||
|
||||
def pick_baseline(f, override):
|
||||
if override:
|
||||
return override
|
||||
severities = {x.get("severity") for x in f.get("findings", [])}
|
||||
props = f.get("entity", {}).get("properties_total", 0) or 0
|
||||
return "treatment" if ("critical" in severities or props > 3) else "basic"
|
||||
e = f.get("entity", {})
|
||||
props = e.get("properties_total", 0) or 0
|
||||
subs = e.get("subbrands_total", 0) or 0
|
||||
if props > 5 or subs > 3: # multi-brand / chain
|
||||
return "treatment"
|
||||
if props <= 1 and subs == 0: # single-property SMB
|
||||
return "smb"
|
||||
return "basic" # small multi-property / mid
|
||||
|
||||
|
||||
def assemble(f, rate, sow, baseline, billing):
|
||||
roles = rate["role_rates"]
|
||||
props = f.get("entity", {}).get("properties_total", 0)
|
||||
mult = scope_multiplier(rate, props)
|
||||
mult, driver, dcount = scope_multiplier(rate, f)
|
||||
tpl = sow["baselines"][baseline]
|
||||
modules = []
|
||||
grand = 0.0
|
||||
@@ -72,7 +78,7 @@ def assemble(f, rate, sow, baseline, billing):
|
||||
sub += amount
|
||||
modules.append({"name": mod["name"], "subtotal": sub, "tasks": tasks})
|
||||
grand += sub
|
||||
return modules, grand, mult, props, tpl["service"]
|
||||
return modules, grand, mult, driver, dcount, tpl["service"]
|
||||
|
||||
|
||||
ROLE_KO = {
|
||||
@@ -91,7 +97,8 @@ def write_md(path, q):
|
||||
f"- **산정 기준**: SOW 기반 · 청구율 {int(q['billing_rate']*100)}% · 일 8시간/월 4주 · {q['terms']['vat']} · 지급 {q['terms']['payment']}",
|
||||
""]
|
||||
if q["scope"]["hours_multiplier"] != 1.0:
|
||||
L.append(f"> 포트폴리오 규모 반영: 프로퍼티 {q['scope']['properties_total']}개 기준 Technical/On-page 업무시간 ×{q['scope']['hours_multiplier']:g} (서브선형)")
|
||||
dl = "브랜드/템플릿" if q["scope"]["driver"] == "subbrands_total" else "프로퍼티"
|
||||
L.append(f"> 규모 반영: {dl} {q['scope']['driver_count']}개 기준 On-page 업무시간 ×{q['scope']['hours_multiplier']:g} (서브선형)")
|
||||
L.append("")
|
||||
L += ["## 견적 내역", "",
|
||||
"| 구분 | 세부 업무 | 담당 | 시간(h) | 합계 |",
|
||||
@@ -158,7 +165,7 @@ def main():
|
||||
ap.add_argument("--sow", required=True)
|
||||
ap.add_argument("--out-dir", default=".")
|
||||
ap.add_argument("--seq", type=int, default=1)
|
||||
ap.add_argument("--baseline", choices=["basic", "treatment"], default=None)
|
||||
ap.add_argument("--baseline", choices=["smb", "basic", "treatment"], default=None)
|
||||
ap.add_argument("--billing", type=float, default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
@@ -169,9 +176,13 @@ def main():
|
||||
with open(args.sow, encoding="utf-8") as fh:
|
||||
sow = yaml.safe_load(fh)
|
||||
|
||||
billing = args.billing if args.billing is not None else rate["billing_rate"]
|
||||
baseline = pick_baseline(f, args.baseline)
|
||||
modules, grand, mult, props, service = assemble(f, rate, sow, baseline, billing)
|
||||
tpl_billing = sow["baselines"][baseline].get("billing_rate")
|
||||
billing = (args.billing if args.billing is not None
|
||||
else tpl_billing if tpl_billing is not None else rate["billing_rate"])
|
||||
modules, grand, mult, driver, dcount, service = assemble(f, rate, sow, baseline, billing)
|
||||
props = f.get("entity", {}).get("properties_total", 0)
|
||||
subs = f.get("entity", {}).get("subbrands_total", 0)
|
||||
|
||||
rounding = rate["rounding_unit"]
|
||||
proposal = int(math.floor(grand / rounding) * rounding)
|
||||
@@ -190,8 +201,8 @@ def main():
|
||||
"prospect": f.get("prospect", {}).get("name", "(prospect)"),
|
||||
"service": service, "baseline": baseline, "billing_rate": billing,
|
||||
"company": rate["company"], "terms": rate["terms"],
|
||||
"scope": {"properties_total": props,
|
||||
"subbrands_total": f.get("entity", {}).get("subbrands_total", 0),
|
||||
"scope": {"driver": driver, "driver_count": dcount,
|
||||
"properties_total": props, "subbrands_total": subs,
|
||||
"hours_multiplier": mult},
|
||||
"modules": modules, "subtotal_sum": grand, "proposal": proposal,
|
||||
"rounding_unit": rounding,
|
||||
@@ -209,7 +220,7 @@ def main():
|
||||
json.dump(q, fh, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"견적 {quote_no} [{baseline}] 제안가 {won(proposal)} (합계 {won(grand)}) "
|
||||
f"| 프로퍼티 {props} ×{mult:g} | 청구율 {int(billing*100)}%")
|
||||
f"| {driver}={dcount} ×{mult:g} | 청구율 {int(billing*100)}%")
|
||||
for m in modules:
|
||||
print(f" {m['name']:24} {won(m['subtotal'])}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user