diff --git a/B09_Estimation/B09_Estimation_Engine_Cost.py b/B09_Estimation/B09_Estimation_Engine_Cost.py index 47a46469..f58eb6ab 100644 --- a/B09_Estimation/B09_Estimation_Engine_Cost.py +++ b/B09_Estimation/B09_Estimation_Engine_Cost.py @@ -87,7 +87,10 @@ class CostInput: #: 안전관리비 대상액에 들어가는 **도급자설치 관급금액**. None 이면 관급 전액. owner_supplied_for_safety_krw: Decimal | None = None - #: 그 금액이 부가세 포함인가 — 포함이면 1.1 로 나눈다(규정: 부가세 제외 기준). + #: 그 금액이 부가세 포함인가 — 포함이면 1.1 로 나눈다. + #: ⚠ 근거는 **실무**다 — 고시(산업안전보건관리비 계상 및 사용기준) 제4조① 단서는 「해당 + #: 재료비를 **대상액에 포함**」까지만 적고 부가세를 말하지 않는다. ÷1.1 은 부가세 제외 + #: 환산이며, 실무 원가계산서 **6건이 모두** 「관급재/1.1」로 적었다(2026-09-14 전수 확인). owner_supplied_includes_vat: bool = True #: 규모 구간 판정에 쓸 **추정가격**. 주면 그 값으로 한 번만 판정한다. #: 없으면 직접공사비를 씨앗으로 **반복 수렴**한다 (`calculate_cost` 참조). diff --git a/B09_Estimation/B09_Estimation_Statutory.py b/B09_Estimation/B09_Estimation_Statutory.py index f8712276..0ef4d11c 100644 --- a/B09_Estimation/B09_Estimation_Statutory.py +++ b/B09_Estimation/B09_Estimation_Statutory.py @@ -199,10 +199,22 @@ def safety_management_cost( variable = dataset.variable("rate_safety_pct") brackets = variable["brackets"] + # 제3조(적용범위) — 「총공사금액 2천만 원 이상인 공사에 적용」. 하한은 요율 데이터가 든다. + # ⚠ 견주는 값은 **규모 기준액**(설계자가 준 추정가격 · 없으면 수렴한 총원가)이다. 고시의 + # 「총공사금액」과 딱 같은 말은 아니나(관급·부가세 자리가 다름) 계산 차례상 안전관리비 + # 앞에 설 수 있는 값이 그것뿐이라 같은 축으로 쓴다 — 보건관리자 문턱도 같은 축이다. + if not _threshold_met( + dataset, "rate_safety_pct", "minimum_total_construction_amount_krw", ctx.scale_reference + ): + return _ZERO # 대상 아님 — 줄 자체를 만들지 않는다(0 원으로 채우지 않음) + owner_supplied = data.owner_supplied_for_safety_krw if owner_supplied is None: owner_supplied = data.owner_supplied_material_krw if data.owner_supplied_includes_vat: + # 제4조① 단서는 「해당 재료비를 **대상액에 포함**」까지만 적고 부가세를 말하지 않는다. + # ÷1.1 은 **부가세 제외 환산**이며 근거는 실무다 — 실무 원가계산서 **6건이 모두** + # 「(직노+직재+간재+관급재/1.1) × 율」로 적었다(2026-09-14 골든셋 전수 확인). owner_supplied = owner_supplied / _VAT_DIVISOR base_with = ctx.material_cost + ctx.direct_labor_cost + owner_supplied @@ -255,6 +267,9 @@ def safety_management_cost( # 1.2배의 대상은 1·2호로 **산정이 끝난 금액**이다. 종전엔 1.2 를 곱한 뒤 한 번만 버려 # 영월 2024 B 줄이 20,330,639 로 원본(20,330,638)보다 1원 컸다(골든셋 실증). # A(배수 1)는 어느 차례로 해도 같은 값이다. + # ⚠ 안 고른 갈래 — 거창 2025 원본은 `버림(밑수 × 율 × 1.2)` 로 1원 위다. 그 서류는 + # 시트 이름·줄 차례가 달라 **STmate 출력이 아니며**, 우리 기준은 STmate 재현이라 + # 사유로만 남기고 채택하지 않는다(브레인 판정 2026-09-14). return floor_won(base * percent / _HUNDRED + flat) * multiplier, percent, flat raw_a, percent_a, flat_a = evaluate(base_with, Decimal(1), "안전관리비 A(관급 포함)") diff --git a/resources/tester/test_b09_safety_notice.py b/resources/tester/test_b09_safety_notice.py index 1efb0942..b749fe02 100644 --- a/resources/tester/test_b09_safety_notice.py +++ b/resources/tester/test_b09_safety_notice.py @@ -172,3 +172,35 @@ def test_대상액은_직재_간재_직노_그리고_발주자_제공_재료다( assert owner.line("safety_management_cost_a").base_amount_krw == base + 30_000_000 # 관급 제외 밑수(B)는 관급이 들어도 안 움직임. assert owner.line("safety_management_cost_b").base_amount_krw == base + + +def _scaled(estimated_price: Decimal) -> object: + """총공사금액(규모 기준액)만 갈아 끼워 돌림 — 대상액은 구간이 안 갈리게 작게 둠.""" + return calculate_cost( + CostInput( + direct_material_krw=Decimal(5_000_000), + direct_labor_krw=Decimal(5_000_000), + direct_expense_krw=Decimal(0), + enabled_items=("safety_management_cost",), + estimated_price_krw=estimated_price, + cut_basis="none", + ) + ) + + +def test_제3조_총공사금액_2천만원_미만이면_줄이_안_선다() -> None: + """제3조 — 「총공사금액 2천만 원 이상인 공사에 적용」. 하한은 요율 데이터가 들고 있음. + + ⚠ 우리가 견주는 값은 **규모 기준액**(설계자가 준 추정가격 · 없으면 수렴한 총원가)임. + 고시의 「총공사금액」과 딱 같은 말은 아니나(관급·부가세 자리가 다름) 계산 차례상 + 안전관리비 앞에 설 수 있는 값이 그것뿐이라 같은 축으로 씀 — 보건관리자 문턱도 같은 축. + """ + minimum = json.load(io.open(RATES, encoding="utf-8"))["variables"]["rate_safety_pct"].get( + "minimum_total_construction_amount_krw" + ) + assert minimum == 20_000_000, minimum + assert not _scaled(Decimal(minimum) - 1).has("safety_management_cost") + assert _scaled(Decimal(minimum)).has("safety_management_cost") + # 안 서면 A·B 곁줄도 안 선다 — 0 원으로 채우지 않음. + below = _scaled(Decimal(minimum) - 1) + assert not below.has("safety_management_cost_a") and not below.has("safety_management_cost_b")