"""B09 원가계산 — **유로폼 사용수량** 12-38-2 (2026-09-14 브레인 301 판정 ①~⑥ · ③′). 정본 = 산림 12-38-2(10㎡당 패널 0.89매 · 내부 패널 0.03매 · 부자재 주자재비 간단 24 · 보통 52 · 복잡 79% · 소모자재 5%) · 건설 공통 6-3-3 은 참고(고시 총칙 「타 부문과 유사한 공종은 본 품셈 우선」). 원문 「자재비는 거래형태 등을 고려하여 **임대료 또는 손료**로 산정」 — 둘을 나란히 주므로 설계자가 고름(제안값 없음). 고르는 자리 「자재 단가」 탭(화약류와 같은 통로) — 넣은 쪽으로 섬 손료 패널·내부 패널 단가 × 표 수량 곧장(실무 넷 — 봉화 「31,500 × 0.89 / 10」) · 부자재·소모자재 = (패널 + 내부 패널) × %(봉화 「2,866.5 × 52%」) 임대료 「유로폼 임대료 ㎡당」 한 줄(설계자가 임대기간 반영해 셈) · % 는 원문이 안 적어 안 걺 둘 다 · 반쯤 · 아무것도 — 안 섬 + 사유(부모 12-38 까지 그대로 올라감) ⚠ 12-38-1 잔존율(12회 25%)은 곱하지 않음 — 표 수량이 이미 그 몫으로 보임. 곱하면 두 번 나눔(패널 몫 약 1/16). ⚠ 가드(이중계상 ③)에서 12-38-02 를 뺀 까닭은 `B09_Estimation_Guards.SURCHARGE_INCLUDED_ITEMS` 곁에. """ from __future__ import annotations import re from decimal import Decimal from typing import Any CODE = "FP-12-38-02" PANEL = "AR-M-10aba455" INNER = "AR-M-5b89b294" RENT = "AR-M-d00a6b1a" QUANTITY_TABLE = "F0393" RATE_TABLE = "F0394" CHOICE_MISSING = ( "자재비 — 임대료 또는 손료 설계자 선택(12-38-2 「거래형태 등을 고려하여 임대료 또는 손료로 산정」)" " · 「자재 단가」 탭에 패널·내부 패널 단가(손료) 또는 유로폼 임대료 ㎡당(임대료) 중 하나를 넣으면 섬" ) CHOICE_BOTH = "자재비 — 손료(패널 단가)와 임대료가 둘 다 들어옴 · 하나만 넣을 것" LOSS_HALF = "자재비 손료 — {name} 단가 없음 · 패널·내부 패널 둘 다 넣어야 섬" REUSE_NOTE = ( "12-38-2 표 수량 곧장(실무 넷 실증) · 12-38-1 잔존율은 안 곱함 — 표 수량이 이미 12회·잔존율 25% 몫으로" " 보임(우리 역산: 10㎡ ÷ 0.72㎡ = 13.9매 × 0.75 ÷ 12 = 0.87 ≈ 0.89 · 원문이 적지는 않음) · 차 2.5% 는" " [주]① 「할증 및 손율 포함」으로 봄(추정) · 「25회 10%」 는 표에 수량이 없어 안 세움" ) RENT_NOTE = "설계자 임대료(12-38-2 「임대료는 시중 물가지 등을 참고하여 결정」) · 부자재·소모자재 % 는 원문이 임대료에 안 적어 안 걺" _RE_PERCENT = re.compile(r"(\d+(?:\.\d+)?)\s*%") def _tight(text: Any) -> str: return re.sub(r"\s", "", str(text or "")) def _table(node: dict[str, Any], table_id: str) -> dict[str, Any]: return next((t for t in node.get("tables") or [] if t.get("pum_table_id") == table_id), {}) def rates(node: dict[str, Any]) -> dict[str, Decimal]: """부자재 요율 표(F0394) — 머리 갈래(원문 표기 「간 단」…) → %. 마스터 갈래 키도 이것을 씀.""" table = _table(node, RATE_TABLE) heads = table.get("condition_note") or [] for row in table.get("raw_row") or []: found = {str(h): _RE_PERCENT.search(str(c)) for h, c in zip(heads[1:], row[1:])} return {h: Decimal(m.group(1)) for h, m in found.items() if m} return {} def _quantities(node: dict[str, Any]) -> tuple[Decimal | None, Decimal | None, Decimal | None]: """(패널 매/㎡, 내부 패널 매/㎡, 소모자재 %) — 표 F0393 10㎡당을 1㎡당으로.""" from B09_Estimation.B09_Estimation_ResourceAxis import parse_amount table = _table(node, QUANTITY_TABLE) per = Decimal(str(table.get("basis_quantity") or 0)) panel = inner = consumable = None for row in table.get("raw_row") or []: name, value = _tight(row[0]), str(row[-1]) if name == "패널" and parse_amount(value) and per: panel = parse_amount(value) / per elif name == "내부패널" and parse_amount(value) and per: inner = parse_amount(value) / per elif name.startswith("소모자재") and _RE_PERCENT.search(value): consumable = Decimal(_RE_PERCENT.search(value).group(1)) return panel, inner, consumable def attach_euroform(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None: """「자재 단가」 칸 셋을 세우고, 넣은 쪽으로 12-38-02 를 세움 — 부모 합산(`attach_parent_steps`) 앞.""" from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle node = nodes_by_code.get(CODE) or {} for material in (PANEL, INNER, RENT): uses = build.material_uses.setdefault(material, []) if CODE not in uses: uses.append(CODE) book = build.book loss = [m for m in (PANEL, INNER) if m in book.titles] panel, inner, consumable = _quantities(node) table_rates = rates(node) reason = "" if loss and RENT in book.titles: reason = CHOICE_BOTH elif RENT in book.titles: title_code = f"B-{CODE}" book.add_title( PriceTitle( code=title_code, kind=PriceKind.UNIT_PRICE, name="유로폼 사용수량", spec="임대료", unit="㎡", ) ) book.add_detail(PriceDetail(title_code, RENT, Decimal(1), note=RENT_NOTE)) elif len(loss) == 2 and panel and inner and consumable is not None and table_rates: for head, rate in table_rates.items(): variant = _tight(head) title_code = f"B-{CODE}#{variant}" book.add_title( PriceTitle( code=title_code, kind=PriceKind.UNIT_PRICE, name=f"유로폼 사용수량 ({variant})", spec=variant, unit="㎡", ) ) book.add_detail(PriceDetail(title_code, PANEL, panel, note=REUSE_NOTE)) book.add_detail( PriceDetail(title_code, INNER, inner, note="12-38-2 내부 패널 표 수량 곧장") ) for label, percent in ( ("부자재(웨지핀·플랫타이·강관파이프·훅)", rate), ("소모자재(박리제 등)", consumable), ): book.add_detail( PriceDetail( title_code, title_code, Decimal(0), note=f"{label} — 주자재비(패널 + 내부 패널)의 {percent}% (12-38-2)", percent_of_material=percent, ) ) build.variants.setdefault(CODE, []).append(variant) elif loss: missing = "내부 패널" if PANEL in loss else "패널" reason = LOSS_HALF.format(name=missing) else: reason = CHOICE_MISSING if reason: build.component_gaps[CODE] = reason build.unattached[CODE] = [reason]