"""단가 층 차례 `X → D → B` (2026-09-13, PLAN 6장 · 명세 3장 정정 · 브레인 판정). 겨누는 것 ① 시공능력 Q 로 선 기계 줄은 D(단가산출)에 살고 B 는 D 를 수량 1 로 부름 — D 는 X 만 부름 ② 층 차례만 바꿈 — 금액 불변(420 제목 전후 대조는 스크립트, 여기선 B = D 합) ③ 자원 집계가 가운데 층(B→B 잎 · B→D)을 **끝까지 풀어** 기계 사용료에 닿음(종전 조용히 버림) ④ 「단산 N」은 D 를 품은 줄에만 — 사람 품만 있는 일위대가엔 산출서가 없음 """ from __future__ import annotations from decimal import Decimal from functools import lru_cache from B09_Estimation.B09_Estimation_Lists import resource_summary from B09_Estimation.B09_Estimation_PriceBasis import build_price_basis, price_basis_detail from B09_Estimation.B09_Estimation_PriceBook import PriceKind from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices @lru_cache(maxsize=1) def _build(): return build_unit_prices() def test_D_는_X_만_부르고_B_가_D_를_수량_1로_부른다() -> None: book = _build().book bases = [code for code, t in book.titles.items() if t.kind is PriceKind.PRICE_BASIS] assert len(bases) >= 40, len(bases) # 2026-09-13 기본 벌 45장 for basis in bases: refs = {book.titles[d.ref_code].kind for d in book.details[basis]} assert refs == {PriceKind.MACHINE_HOURLY}, (basis, refs) parent = f"B-{basis[2:]}" calls = [d for d in book.details[parent] if d.ref_code == basis] assert len(calls) == 1 and calls[0].quantity == Decimal(1), basis # 금액은 B 에 D 한 줄이 그대로 — 사람 품 등 나머지 줄과 더해짐(층만 바뀜). others = [d for d in book.details[parent] if d.ref_code != basis] if not others: assert book.resolve(parent) == book.resolve(basis) # 굴착기 공식 공종 — Q 줄이 B 에 바로 안 붙음. assert all( book.titles[d.ref_code].kind is not PriceKind.MACHINE_HOURLY for d in book.details["B-FP-09-03-02"] ) def test_자원_집계가_가운데_층을_끝까지_풀어_기계에_닿는다() -> None: build = _build() summary = resource_summary({"B-FP-09-04#연암": Decimal(1)}, build) machines = {row["code"] for row in summary["groups"]["machine"]} # 암절취 부모 → 잎 B(암파쇄·집토) → D → X — 종전엔 부모의 B→B 참조를 조용히 버렸음. assert {"X-0230-0007", "X-0201-0070#조합"} <= machines, machines assert not summary["missing"] def test_단산_번호는_D_를_품은_줄에만_붙는다() -> None: build = _build() book = build.book labor_only = next( code for code, t in sorted(book.titles.items()) if t.kind is PriceKind.UNIT_PRICE and book.details.get(code) and all(book.titles[d.ref_code].kind is PriceKind.LABOR for d in book.details[code]) ) sheet = build_price_basis(["B-FP-09-03-02", labor_only, "B-FP-09-04#연암"], build) assert [e.code for e in sheet.entries] == [ "D-FP-09-03-02", "D-FP-09-04-01#연암", "D-FP-09-04-02", ] assert sheet.label_for("B-FP-09-03-02") == "단산 1 참조" assert sheet.label_for(labor_only) == "" # 없는 근거를 가리키지 않음 assert sheet.label_for("B-FP-09-04#연암") == "단산 2·3 참조" # 단계 합산 부모 detail = price_basis_detail("D-FP-09-03-02", build) assert detail["kind"] == PriceKind.PRICE_BASIS.value assert detail["rows"] and all(r["ref_code"].startswith("X-") for r in detail["rows"])