"""유로폼 사용수량 12-38-2 — 2026-09-14 브레인 301 판정 ①~⑥ · ③′. ① 산림 12-38-2 정본 ② 「임대료 또는 손료」 설계자 선택 · 제안값 없음 ③′ 손료 = 자재가 × 표 수량 곧장(실무 넷 — 봉화 「31,500 × 0.89 / 10」) · 12-38-1 잔존율은 안 곱함 ④ 이중계상 ③ 가드에서 12-38-02 만 뺌 — 패널이 B08 자재총괄에 줄이 없어서. 되살아날 자리를 시험으로 걺 ⑤ 고르는 자리 = 「자재 단가」 탭(넣은 쪽으로 섬 · 둘 다면 안 섬) ⑥ 임대료 갈래엔 % 안 걺 """ from __future__ import annotations from decimal import Decimal from B09_Estimation.B09_Estimation_UnitPrice import cached_build, detail_of PANEL = "AR-M-10aba455" INNER = "AR-M-5b89b294" RENT = "AR-M-d00a6b1a" LOSS = ((PANEL, "31500", "봉화 2024"), (INNER, "21000", "봉화 2024")) def _titles(build, code: str) -> list[str]: return [t for t in build.book.titles if t == f"B-{code}" or t.startswith(f"B-{code}#")] def test_아무것도_안_넣으면_안_서고_설계자_선택_사유() -> None: build = cached_build() assert not _titles(build, "FP-12-38-02") reason = build.component_gaps.get("FP-12-38-02", "") assert "임대료 또는 손료" in reason, reason assert "임대료 또는 손료" in build.component_gaps.get("FP-12-38", "") # 부모까지 까닭이 올라감 for material in (PANEL, INNER, RENT): assert "FP-12-38-02" in build.material_uses.get(material, []), material def test_손료는_표_수량_곧장_부자재_소모자재는_주자재비의_퍼센트() -> None: """봉화 2024 호표 「유로폼 설치 및 해체」 재료비 보통 4,500 · 간단 3,697 과 같은 값 — 실무 실증.""" build = cached_build(material_prices=LOSS) assert sorted(_titles(build, "FP-12-38-02")) == [ "B-FP-12-38-02#간단", "B-FP-12-38-02#보통", "B-FP-12-38-02#복잡", ] detail = detail_of(build, "B-FP-12-38-02#보통")["rows"] rows = {r["ref_code"]: Decimal(r["quantity"]) for r in detail if r.get("ref_code")} assert rows[PANEL] == Decimal("0.089") and rows[INNER] == Decimal("0.003"), rows assert build.book.resolve("B-FP-12-38-02#보통").material == Decimal(4500) assert build.book.resolve("B-FP-12-38-02#간단").material == Decimal(3697) # 봉화 간단 3,697 # 부모 12-38 = 사용수량 + 설치 및 해체 — 두 단계 갈래가 같으면 갈래끼리 합산 parent = {r["ref_code"] for r in detail_of(build, "B-FP-12-38#보통")["rows"]} assert parent == {"B-FP-12-38-02#보통", "B-FP-12-38-03#보통"}, parent def test_작업조_표는_밑수가_시공량이라_밑수_없음이_아님() -> None: """12-38-3 「(단위: 일 당) 형틀목공 4인 · 시공량 35㎡」 — 1㎡당 = 인원 ÷ 시공량(CrewOutput). 밑수 「N㎡당」 문구가 없다고 `basis_missing` 에 들어 설치·해체가 통째로 막혀 있었음(작업조 표 넷). """ build = cached_build() crew = ("FP-05-28-02", "FP-12-07-02", "FP-12-38-03", "FP-13-16-02") assert not [code for code in crew if code in build.basis_missing] rows = { r["name"]: Decimal(r["quantity"]) for r in detail_of(build, "B-FP-12-38-03#보통")["rows"] } assert rows["형틀목공"] == Decimal(4) / Decimal(35), rows def test_잔존율은_안_곱하고_역산은_추정이라_밝힘() -> None: build = cached_build(material_prices=LOSS) note = next( r["note"] for r in detail_of(build, "B-FP-12-38-02#보통")["rows"] if r.get("ref_code") == PANEL ) assert "우리 역산" in note and "추정" in note and "25회" in note, note def test_임대료는_한_줄만_퍼센트_안_걺() -> None: build = cached_build(material_prices=((RENT, "5000", "견적"),)) assert _titles(build, "FP-12-38-02") == ["B-FP-12-38-02"] rows = detail_of(build, "B-FP-12-38-02")["rows"] assert [r["ref_code"] for r in rows] == [RENT], rows assert build.book.resolve("B-FP-12-38-02").material == Decimal(5000) assert "B-FP-12-38#보통" in build.book.titles def test_둘_다_넣거나_한쪽만_반쯤_넣으면_안_섬() -> None: both = cached_build(material_prices=(*LOSS, (RENT, "5000", "견적"))) assert not _titles(both, "FP-12-38-02") assert "둘 다" in both.component_gaps["FP-12-38-02"] half = cached_build(material_prices=(LOSS[0],)) assert not _titles(half, "FP-12-38-02") assert "내부 패널" in half.component_gaps["FP-12-38-02"] def test_유로폼이_자재총괄에_줄로_서면_가드를_되살릴_것() -> None: """④ 12-38-02 를 가드에서 뺀 전제 — 패널·유로폼이 B08 자재총괄(`material`)에 줄이 없음. 누가 유로폼을 자재총괄로 보내는 날 할증이 두 번 붙음 → 여기서 빨강 · 가드에 12-38-02 를 되돌릴 것. """ import json from pathlib import Path from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import DESTINATION from B09_Estimation.B09_Estimation_Guards import SURCHARGE_INCLUDED_ITEMS assert "FP-12-38-02" not in SURCHARGE_INCLUDED_ITEMS words = ("유로폼", "패널") assert all(DESTINATION[n] != "material" for n in DESTINATION if any(w in n for w in words)) root = Path(__file__).resolve().parents[2] / "resources" / "data_structure_unit" for path in root.glob("*.json"): text = path.read_text(encoding="utf-8") def walk(node): if isinstance(node, dict): name = str(node.get("name") or "") if any(w in name for w in words): assert node.get("destination") != "material", (path.name, node) for value in node.values(): walk(value) elif isinstance(node, list): for value in node: walk(value) walk(json.loads(text))