fix(b09): 산업안전 50억 이상 구간 겹침 해소 · 하도급대금 지급보증수수료 칸(기본 꺼짐)

- 산업안전보건관리비: 고시 별표1 「보건관리자 선임 대상 건설공사의 적용비율」 열을 대상액이 아니라
  공사가 선임 대상인가(요율 데이터 manager_thresholds 800억 · 토목 1,000억)로 가름 — 두 줄이 겹쳐 멈추던 자리
- 하도급대금 지급보증수수료: 기본 꺼짐 — 실무 원가계산서 6건에 줄 없음 · 건설산업기본법 제34조 대상이면 켬
  (칸 밑에 「대상 아님인지 안 적음인지 못 가름」 사유)
- 시험: 50억 이상 2.60 / 선임 대상 2.73 · 하도급 꺼짐·켬 · 골든셋 초록

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-14 04:07:45 +09:00
co-authored by Claude Opus 5
parent 8693cf0012
commit 8046205039
5 changed files with 84 additions and 12 deletions
@@ -52,6 +52,7 @@ CHOICE_FIELDS: tuple[tuple[str, str], ...] = (
("vat_mode", "12.부 가 가 치 세"),
("private_material_position", "13.사급비 위치"),
("bid_method", "14. 낙 찰 방 식"),
("subcontract_guarantee", "하도급대금 지급보증수수료"),
("performance_guarantee_mode", "15. 이행보증 방식"),
("contract_law_basis", "16. 적용기준"),
("waste_placement", "폐기물처리비 자리"),
@@ -79,6 +80,11 @@ FIELD_HINTS: dict[str, str] = {
" 종합심사 대상 = 300억 이상 줄"
" · 턴키(대안) = 0.084% · 설계 단계 금액엔 이 한 줄뿐, 계약 단계 단가 적용률로 이어질 칸"
),
"subcontract_guarantee": (
"기본 꺼짐 — 실무 원가계산서 6건에 이 줄이 없음. 건설산업기본법 제34조 대상"
"(하도급대금 지급보증서 교부)이면 켤 것 · 실무에 없는 것이 「대상 아님」인지"
" 「안 적음」인지는 못 가름"
),
"performance_guarantee_mode": (
"일반계약(기본) = 추정가격 300억 이상일 때만 공사이행보증수수료를 셈 · 최저가·기술제안 ="
" 규모와 무관하게 셈 — 근거: 예정가격작성기준 제19조③20호(시행령 제52조제1항 단서) ·"
@@ -132,6 +138,10 @@ def choice_options() -> dict[str, list[dict[str, str]]]:
{"value": "comprehensive", "label": "종합심사 대상"},
{"value": "turnkey", "label": "턴키(대안)"},
],
"subcontract_guarantee": [
{"value": "off", "label": "계상 안 함(기본)"},
{"value": "on", "label": "계상"},
],
"performance_guarantee_mode": [
{"value": "general", "label": "일반계약"},
{"value": "lowest_price_tech", "label": "최저가,기술제안"},
@@ -139,6 +139,9 @@ class CostInput:
#: 14. 낙찰방식 — `not_comprehensive`(종합심사 외) · `comprehensive`(종합심사 대상) · `turnkey`.
#: 하도급대금 지급보증 요율 줄만 고름.
bid_method: str = "not_comprehensive"
#: 하도급대금 지급보증수수료 — `off`(기본) · `on`. 실무 여섯 건에 줄 없음 · 건설산업기본법
#: 제34조 대상이면 켬(「대상 아님」인지 「안 적음」인지는 실무로 못 가름).
subcontract_guarantee: str = "off"
#: 15. 이행보증 — `general`(일반계약: 추정가격 300억 이상만) ·
#: `lowest_price_tech`(최저가·기술제안: 규모 무관).
performance_guarantee_mode: str = "general"
+43 -7
View File
@@ -206,16 +206,46 @@ def safety_management_cost(
base_with = ctx.material_cost + ctx.direct_labor_cost + owner_supplied
base_without = ctx.material_cost + ctx.direct_labor_cost
# ⚠ 2026-09-14 고침 — 대상액 50억 이상에서 두 줄(선임 대상 아님/대상)이 겹쳐 멈추던 자리.
# 고시 별표1 은 「대상액 50억원 이상」 열과 **따로** 「영 별표5에 따른 보건관리자 선임 대상
# 건설공사의 적용비율」 열을 둔다 — 대상액이 아니라 **공사가 선임 대상인가**로 가르는 열.
# 선임 대상 금액은 요율 데이터 `manager_thresholds`(800억 · 토목 1,000억, 산업안전보건법
# 시행령 별표5 — 원문 미보유). 추정금액 = 규모 기준액으로 견줌.
thresholds = variable.get("manager_thresholds") or {}
limit = thresholds.get(
"civil_main_work_estimated_amount_krw"
if data.work_type_safety == "civil"
else "default_estimated_amount_krw"
)
manager_target = limit is not None and ctx.scale_reference >= Decimal(str(limit))
def evaluate(
base: Decimal, multiplier: Decimal, label: str
) -> tuple[Decimal, Decimal, Decimal]:
row = select_bracket(
brackets,
amount_field="target_amount_bracket",
amount=base,
equals={"work_type": data.work_type_safety},
label=label,
)
if manager_target:
row = next(
(
r
for r in brackets
if str(r["target_amount_bracket"]).endswith("at_or_above_manager_threshold")
and r.get("work_type") == data.work_type_safety
),
None,
)
if row is None:
raise RateLookupError(f"{label}: 보건관리자 선임 대상 요율이 없습니다")
else:
row = select_bracket(
[
r
for r in brackets
if not str(r["target_amount_bracket"]).endswith("at_or_above_manager_threshold")
],
amount_field="target_amount_bracket",
amount=base,
equals={"work_type": data.work_type_safety},
label=label,
)
percent = rate_percent(row, label=label)
flat = base_amount(row)
return (base * percent / _HUNDRED + flat) * multiplier, percent, flat
@@ -309,6 +339,12 @@ def statutory_expenses(
if key == "performance_guarantee_fee" and not _performance_guarantee_applies(data, ctx):
continue # 15. 일반계약·추정가격 300억 미만 — 줄 자체를 만들지 않는다
if key == "subcontract_payment_guarantee" and (
str(getattr(data, "subcontract_guarantee", "off") or "off") != "on"
):
# 기본 꺼짐(2026-09-14) — 실무 원가계산서 여섯 건에 이 줄이 없음. 건설산업기본법
# 제34조 대상(하도급대금 지급보증서 교부)이면 설계자가 켬.
continue
base, percent, note = _base_and_rate(dataset, data, ctx, item)
if base is None:
@@ -234,6 +234,7 @@ function drawPanel(ctx: B09TabContext, sheet: CostSheetDto, reload: () => void):
// 13~16 — 사급비 위치·적용기준은 칸만 받음(계산에 안 씀 · 까닭은 칸 밑 안내).
typeSelect("private_material_position");
typeSelect("bid_method");
typeSelect("subcontract_guarantee");
typeSelect("performance_guarantee_mode");
typeSelect("contract_law_basis");
typeSelect("waste_placement");
+27 -5
View File
@@ -183,12 +183,11 @@ def test_이행보증_70억_이상은_기초액과_기준액을_쓴다() -> None
def test_낙찰방식이_하도급대금_요율_줄을_고른다() -> None:
assert calculate_cost(BASE).line("subcontract_payment_guarantee").rate_percent == Decimal(
"0.081"
)
turnkey = calculate_cost(replace(BASE, bid_method="turnkey"))
on = replace(BASE, subcontract_guarantee="on")
assert calculate_cost(on).line("subcontract_payment_guarantee").rate_percent == Decimal("0.081")
turnkey = calculate_cost(replace(on, bid_method="turnkey"))
assert turnkey.line("subcontract_payment_guarantee").rate_percent == Decimal("0.084")
comp = calculate_cost(replace(BASE, bid_method="comprehensive"))
comp = calculate_cost(replace(on, bid_method="comprehensive"))
assert comp.line("subcontract_payment_guarantee").rate_percent == Decimal("0.071")
@@ -197,3 +196,26 @@ def test_사급비_위치_적용기준은_계산에_안_쓴다() -> None:
replace(BASE, private_material_position="material", contract_law_basis="local")
)
assert same.totals == calculate_cost(BASE).totals
def test_산업안전_50억_이상은_선임_대상으로_가른다() -> None:
"""고시 별표1 — 「50억 이상」 열과 「보건관리자 선임 대상」 열은 공사가 선임 대상인가로 갈림."""
big = replace(
BASE,
direct_material_krw=Decimal(4_000_000_000),
direct_labor_krw=Decimal(3_000_000_000),
estimated_price_krw=Decimal(20_000_000_000), # 토목 선임 기준 1,000억 미만
)
assert calculate_cost(big).line("safety_management_cost_b").rate_percent == Decimal("2.6")
target = calculate_cost(replace(big, estimated_price_krw=Decimal(120_000_000_000)))
assert target.line("safety_management_cost_b").rate_percent == Decimal("2.73")
def test_하도급대금_지급보증은_기본_꺼짐() -> None:
assert not calculate_cost(BASE).has("subcontract_payment_guarantee")
assert (
calculate_cost(replace(BASE, subcontract_guarantee="on")).amount(
"subcontract_payment_guarantee"
)
> 0
)