From 5a016979c40673ade37187f80d342bec0e35b159 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 19:22:08 +0900 Subject: [PATCH] =?UTF-8?q?fix(b09):=20=EC=95=88=EC=A0=84=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EB=B9=84=20=EC=A0=9C3=EC=A1=B0=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=ED=95=98=ED=95=9C=EC=9D=84=20=EC=97=94=EC=A7=84=EC=9D=B4=20?= =?UTF-8?q?=EC=9D=BD=EA=B2=8C=20=ED=95=A8=20=C2=B7=201.2=EB=B0=B0=20?= =?UTF-8?q?=EC=B0=A8=EB=A1=80=EC=99=80=20=EA=B4=80=EA=B8=89=20=C3=B71.1=20?= =?UTF-8?q?=EC=82=AC=EC=9C=A0=20=EB=AA=85=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3. 제3조(적용범위) 「총공사금액 2천만 원 이상」 — 요율 데이터의 minimum_total_construction_amount_krw 를 규모 기준액과 견주고 미달이면 줄 자체를 안 만듦 (A·B 곁줄도 안 섬 · 0 원으로 안 채움). 잴 시험 먼저 빨강 확인한 뒤 고침. ⚠ 견주는 값은 규모 기준액(추정가격 · 없으면 수렴한 총원가) — 고시의 「총공사금액」과 딱 같은 말은 아니나 계산 차례상 안전관리비 앞에 설 수 있는 값이 그것뿐. 보건관리자 문턱과 같은 축. 1. 1.2배 차례 = STmate 쪽 확정(브레인 판정) — 안 고른 갈래의 사유를 코드에 남김. 거창 2025 원본은 버림(밑수 × 율 × 1.2) 로 1원 위지만 STmate 출력이 아니라 채택하지 않음. 4. 관급 ÷1.1 = 실무 따름 — 근거를 정확히 적음. 고시 제4조① 단서는 「해당 재료비를 대상액에 포함」까지만이고 부가세를 말하지 않음 · ÷1.1 은 부가세 제외 환산으로 실무 6건 공통. 전체 시험 1952 통과 · 28 건너뜀 · 1 xfail. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Prk9BCHG1EMAywk9k8wegA --- B09_Estimation/B09_Estimation_Engine_Cost.py | 5 ++- B09_Estimation/B09_Estimation_Statutory.py | 15 +++++++++ resources/tester/test_b09_safety_notice.py | 32 ++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) 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")