· 손료 = 자재가 × 표 수량 곧장(패널 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
106 lines
5.0 KiB
Python
106 lines
5.0 KiB
Python
"""부모 공종 모양 `parent_mode` (명세 2장 · 2026-09-13 축 C Ⓐ).
|
|
|
|
지키는 것
|
|
① 마스터 — 부모는 기본 choose_one, 합산형(9-4·9-5·12-38)만 사람이 적은 단계·가중치를 든다
|
|
② 흙깎기 리핑암 = 9-4 암절취 = 암파쇄(암질별 1/Q) + 집토 — 부모 제목이 잎들의 합으로 선다
|
|
③ 내역 줄의 암 갈래가 그대로 갈래 값으로 가고, 표에 없는 암질은 9-4-1 [주]① 평균으로만 선다
|
|
④ 단계 하나라도 못 서면 부모를 안 세우고 사유 · 갈래 고르기형 부모를 가리키면 오류로 막는다
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
from functools import lru_cache
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
|
|
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
|
|
from B09_Estimation.B09_Estimation_ResourceAxis_Sources import load_work_item_master
|
|
from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _build():
|
|
return build_unit_prices()
|
|
|
|
|
|
def _node(code: str) -> dict:
|
|
return next(n for n in load_work_item_master()["work_items"] if n["work_item_code"] == code)
|
|
|
|
|
|
def test_마스터_부모_모양은_기본_고르기_합산형만_단계를_든다():
|
|
assert _node("FP-10-12")["parent_mode"] == "choose_one"
|
|
rock = _node("FP-09-05")
|
|
assert rock["parent_mode"] == "sum_steps"
|
|
assert [(s["code"], s["weight"]) for s in rock["steps"]] == [
|
|
("FP-09-05-01", "0.1"),
|
|
("FP-09-05-02", "0.9"),
|
|
("FP-09-05-03", "1"),
|
|
]
|
|
assert "parent_mode" not in _node("FP-09-04-01") # 잎
|
|
|
|
|
|
def test_암절취는_암파쇄와_집토의_합으로_선다():
|
|
book = _build().book
|
|
# 층 차례 X → D → B — 잎 일위대가는 D(단가산출)를 1 로 부르고, 기계 1/Q 줄은 D 에 삶.
|
|
assert [d.ref_code for d in book.details["B-FP-09-04-01#연암"]] == ["D-FP-09-04-01#연암"]
|
|
leaf = book.details["D-FP-09-04-01#연암"]
|
|
assert {d.ref_code for d in leaf} == {"X-0201-0070#조합", "X-0230-0007"} # 조합 16 %
|
|
assert all(d.quantity == Decimal(1) / Decimal("5.0") for d in leaf)
|
|
whole = book.resolve("B-FP-09-04#연암").total
|
|
parts = book.resolve("B-FP-09-04-01#연암").total + book.resolve("B-FP-09-04-02").total
|
|
# 28자리 끝의 반올림 차례만 다를 수 있음 — 금액 자리(원)에서 같으면 같은 합.
|
|
assert parts > 0 and abs(whole - parts) < Decimal("1e-15")
|
|
assert "B-FP-09-04#평균" in book.titles # [주]① 평균
|
|
|
|
|
|
def test_단계가_못_서면_부모를_안_세우고_사유를_남긴다():
|
|
build = _build()
|
|
assert not any(code.startswith("B-FP-09-05#") for code in build.book.titles)
|
|
assert "FP-09-05-01" in build.component_gaps["FP-09-05"] # 발파 착암기 층 없음
|
|
assert "FP-12-38-02" in build.component_gaps["FP-12-38"] # 사용수량 — 임대료·손료 단가 안 넣음
|
|
# 깎기 표엔 기계 이름이 없음 — [주] 원문의 기종으로 섬(부모에서 물려받지 않음)
|
|
assert "[주]" in build.book.details["D-FP-09-05-02#경암"][0].note # Q 줄은 D 에
|
|
|
|
|
|
def _bill(rock: str, method: str = "ripping"):
|
|
handoff = build_handoff(
|
|
summary_table={
|
|
"rows": [{"group": "흙깎기", "item": rock, "spec": "", "unit": "㎥", "amount": 10.0}]
|
|
},
|
|
ground_methods={rock: method},
|
|
)
|
|
return handoff["work_items"][0], build_bill(handoff, build=_build())
|
|
|
|
|
|
def test_암_갈래가_그대로_단가를_고른다():
|
|
row, bill = _bill("보통암")
|
|
assert (row["work_item_code"], row["variant_value"]) == ("FP-09-04", "보통암")
|
|
priced = next(r for r in bill.rows if not r.is_group and r.name == "흙깎기")
|
|
assert priced.unit_price_krw and "보통암" in priced.spec
|
|
|
|
|
|
def test_표에_없는_암질은_주1_평균으로만_선다():
|
|
_row, bill = _bill("풍화암")
|
|
priced = next(r for r in bill.rows if not r.is_group and r.name == "흙깎기")
|
|
assert priced.unit_price_krw and "[주]①" in priced.note
|
|
|
|
|
|
def test_발파암은_단계_사유로_막히고_고르기형_부모는_오류():
|
|
_row, bill = _bill("경암", "blasting")
|
|
assert any("단계 합산 미완" in m["reason"] for m in bill.missing)
|
|
handoff = {
|
|
"work_items": [{**_row, "work_item_code": "FP-10-12", "variant_value": None}],
|
|
"materials": [],
|
|
}
|
|
blocked = build_bill(handoff, build=_build())
|
|
assert any("갈래 고르기형 부모" in m["reason"] for m in blocked.missing)
|
|
|
|
|
|
def test_깨기_브레이커는_굴착기_몸체_시간과_함께_붙는다():
|
|
"""9-12·9-13 「깨기 대형브레이커」 — 부착 장비라 몸체도 같은 1/Q 로(2026-09-13 판정)."""
|
|
details = _build().book.details["D-FP-09-13-07"] # 작업량 줄은 D(단가산출)에
|
|
breaker = next(d for d in details if d.ref_code == "X-0230-0007")
|
|
assert breaker.quantity == Decimal(1) / Decimal("3.5") * Decimal("0.9") # 장비 90 % 몫
|
|
breaking = [d for d in details if d.quantity == breaker.quantity]
|
|
assert {d.ref_code for d in breaking} == {"X-0230-0007", "X-0201-0070#조합"}
|