· 손료 = 자재가 × 표 수량 곧장(패널 0.089 · 내부 패널 0.003매/㎡) + 부자재 24/52/79% · 소모자재 5%(주자재비 = 패널 + 내부 패널) — 봉화 2024 호표 재료비 보통 4,500 · 간단 3,697 과 일치 · 임대료 = 「유로폼 임대료 ㎡당」 한 줄 · 부자재 % 는 원문이 안 적어 안 걺 · 둘 다 · 한쪽만 반쯤 · 아무것도 — 안 세우고 사유(부모 12-38 까지 그대로) · 12-38-1 잔존율은 안 곱함 — 표 수량이 이미 12회·25% 몫으로 보임(우리 역산 · 차 2.5% 는 추정이라 비고에 밝힘) · 이중계상 ③ 가드에서 12-38-02 만 뺌(패널이 자재총괄에 줄 없음) · 유로폼이 자재총괄로 가면 빨강 시험 · 합산형 부모 — 갈래 벌이 같으면 여러 단계 갈래끼리 합산(12-38 = 사용수량 + 설치·해체) · 작업조 표 7 은 밑수 없음 목록에서 뺌(1단위당 = 인원 ÷ 시공량) — 12-38-3 설치·해체가 막혀 있던 자리 · 마스터 재생성 — 12-38 · 12-38-02 갈래 키만 바뀜 기본 조립 제목 금액 변화 0 · 프로젝트 내역 변화 0 · 잴 시험 일곱 빨강→초록 · 끄기 넷 빨강 · 전체 시험 1677 + 318 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
149 lines
7.1 KiB
Python
149 lines
7.1 KiB
Python
"""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]
|