"""구조물도 일위대가 → B09 내역 받는 문 (2026-09-13, PLAN 6장 첫 일감 ② · 브레인 판정). 겨누는 것 ① 양식 일위대가로 셀 구조물은 인계 줄 하나 = 코드 AX-ST · 갈래 = 제원 키 · 수량 = 연장 m ② 양식이 품은 기초잡석은 따로 인계 안 함(이중계상) — 양식 장이 없으면 종전대로 섬 ③ 제원 키는 제목 글이 아니라 제원 값 — 연장이 달라도 같은 키 · 뒷길이가 다르면 다른 키 ④ B09 내역 — 호표 금액 × 연장 · 금액 못 받으면 사유 · 수동 단가는 미확정 """ from __future__ import annotations from decimal import Decimal from functools import lru_cache from B05_Profile.B05_Profile_Structures_Schema import StructureInstance from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff from B08_Quantity.B08_Quantity_Engine_StructurePriceLink import ( price_sheets, priced_sheets, structure_price_code, ) from B08_Quantity.B08_Quantity_Engine_StructureSheet import build_standard_sheets from B08_Quantity.B08_Quantity_Engine_StructureTemplate import apply_templates, load_template from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill from B09_Estimation.B09_Estimation_Rounding import OutputPlace, round_at from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices, find_variant_code @lru_cache(maxsize=1) def _build(): return build_unit_prices() def _wall(sid: str, start: float, end: float, back: int = 45) -> dict: return StructureInstance.model_validate( { "structure_id": sid, "type_id": "masonry_wet", "placement": "interval", "start_m": start, "end_m": end, "options": {"height_m": 2.5, "back_len_cm": back}, } ).model_dump() def _chain(walls: list[dict]) -> tuple[dict, list[dict]]: table = build_table(walls, {"masonry_wet": "돌쌓기(찰)"}, {}, {}, None) payload = build_standard_sheets(table, {}) apply_templates(payload, {}, {}) entries = priced_sheets(payload["sheets"], {"masonry_wet": load_template("masonry_wet")}) return table, entries def _rows(handoff: dict, prefix: str) -> list[dict]: return [ r for r in handoff["work_items"] if str(r.get("work_item_code") or "").startswith(prefix) ] def test_양식_장_구조물은_호표_줄_하나로_가고_기초잡석은_따로_안_간다() -> None: table, entries = _chain([_wall("a", 0, 10), _wall("b", 50, 56)]) assert len(entries) == 1 and entries[0]["structure_ids"] == ["a", "b"] assert "기초잡석" in entries[0]["covered"] and "모르터" in entries[0]["covered"] before = build_handoff(unit_quantity_table=table) assert _rows(before, "FP-13-04-05") and _rows(before, "FP-12-25") # 종전 — 돌쌓기 ㎡ + 잡석 after = build_handoff(unit_quantity_table=table, priced_sheets=entries) ax = _rows(after, "AX-ST-") assert [(r["unit"], r["quantity"]) for r in ax] == [("m", 10.0), ("m", 6.0)] assert {r["variant_axis"] for r in ax} == {"structure_sheet"} assert not _rows(after, "FP-13-04-05") and not _rows(after, "FP-12-25") assert set(ax[0]) == set(before["work_items"][0]) # 인계 칸 계약 그대로 def test_제원_키는_제원_값이고_연장과_제목은_안_든다() -> None: _table, entries = _chain([_wall("a", 0, 10), _wall("c", 20, 30, back=55)]) keys = {e["structure_ids"][0]: e["key"] for e in entries} assert "L3=45" in keys["a"] and "L3=55" in keys["c"] and "|L=" not in keys["a"] assert keys["a"] != keys["c"] assert structure_price_code("AX-ST-56e81a2c", "A = 1 | B~2").endswith("#A=1|B~2") def _bill(manual: dict | None = None): build = _build() table, entries = _chain([_wall("a", 0, 10)]) handoff = build_handoff(unit_quantity_table=table, priced_sheets=entries) prices = price_sheets( entries, build.book, lambda code, value: find_variant_code(code, value, build), [load_template("masonry_wet")], manual, ) return entries, prices, build_bill(handoff, build=build, structure_prices=prices) def test_내역은_호표_금액에_연장을_곱한다() -> None: entries, prices, bill = _bill() ref = structure_price_code(entries[0]["code"], entries[0]["key"]) entry = prices[ref] row = next(r for r in bill.rows if r.code == entries[0]["code"]) # 모르터 공종 코드가 선 뒤(랩탑 메인 AX-WK) 찰쌓기 호표 세 줄이 다 섬. assert not entry["blocked"], entry["reasons"] # 단가 = 구조물도 일위대가 표 계금 그대로(화면과 한 값) · 금액 = 성분 소계 × 연장. from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import unit_price_table table = unit_price_table( entries[0]["template"], entries[0]["sheet"], _build().book, lambda code, value: find_variant_code(code, value, _build()), [load_template("masonry_wet")], ) assert row.unit_price_krw == Decimal(str(table["total"])) cells = sum(Decimal(str(table[k])) for k in ("material", "labor", "expense")) assert row.amount_krw == round_at(cells * Decimal(10), OutputPlace.BOQ_ROW) assert row.quantity == Decimal(10) and f"호표 {ref}" in row.note def test_금액을_못_받으면_사유로_서고_수동_단가는_미확정으로_센다() -> None: build = _build() table, entries = _chain([_wall("a", 0, 10)]) handoff = build_handoff(unit_quantity_table=table, priced_sheets=entries) missing = build_bill(handoff, build=build, structure_prices={}) row = next(r for r in missing.rows if r.code == entries[0]["code"]) assert row.amount_krw is None and "못 받음" in row.note # 모든 일위대가 줄에 수동 단가 — 막힘 없이 서고 미확정 3건. manual = { "masonry_wet": {str(s): {"material": 1000, "labor": 0, "expense": 0} for s in (1, 2, 3)} } _entries, _prices, bill = _bill(manual) row = next(r for r in bill.rows if r.code == entries[0]["code"]) assert row.amount_krw is not None and "미확정" in row.note summary = bill_summary(bill) assert summary["unconfirmed_count"] == 3 and summary["unconfirmed"][0]["count"] == 3