"""구조물도 일위대가 → B09 내역 **받는 문** (PLAN 6장 첫 일감 ② · 2026-09-13 브레인 판정). ⭐ 종전엔 구조물도가 세운 일위대가(`B-AX-ST-*`)가 인계로 **한 번도 안 나갔음** — 돌쌓기는 `FP-13-04-05` ㎡ 단가만 가고 모르터·기초잡석은 따로거나 빠졌음. 사슬의 진짜 빈칸. 한 벌로 두는 것 — 인계(B08)와 내역(B09)이 **같은 목록·같은 키**를 봄 양식 일위대가로 셀 장 → `priced_sheets` (장마다 코드 · 제원 키 · 구성원 · 품은 줄) 인계 줄 → `template_row` (코드 AX-ST · 갈래 = 제원 키 · 수량 = 연장 m) 내역 금액 → `price_sheets` (B09 단가표로 B08 일위대가 엔진을 돌림 — 코드 한 벌) ⚠ 호표 코드 `B-AX-ST-<8hex>#<키>` — 키는 **사람이 읽는 제목이 아니라** 양식 제원 칸 값을 칸 이름 차례로 이은 것(연장 L 뺌) + `normalize_variant_key`. 제목 글자가 바뀌어도 코드는 안 바뀜. ⚠ 양식 일위대가가 품은 원단위 줄(돌쌓기·모르터·기초잡석)은 **따로 인계하지 않음** — `covered`. ⚠ 금액 정본은 B09 — 여기 `price_sheets` 는 B09 가 넘긴 단가표로만 셈. 수동 단가도 입력으로 씀. """ from __future__ import annotations from collections.abc import Callable, Iterable from typing import Any #: 인계 줄 갈래 축 이름 — 「구조물도 장 제원」. VARIANT_AXIS = "structure_sheet" def _key_value(value: Any) -> str: if value is None: return "" if isinstance(value, (int, float)) and not isinstance(value, bool): return format(float(value), ".6g") return str(value).strip() def sheet_variant_key(template: dict[str, Any], sheet: dict[str, Any]) -> str: """장의 **제원 키** — 양식 칸 이름 차례로 `이름=값` 을 `|` 로 이음(연장 칸 뺌: m당).""" values = (sheet.get("formula_sheet") or {}).get("vars") or {} specs = template.get("vars") or {} return "|".join( f"{name}={_key_value(values.get(name))}" for name in sorted(specs) if specs[name].get("source") != "length_m" ) def structure_price_code(code: str, key: str) -> str: """호표 코드 `B-AX-ST-<8hex>#<정규화 키>` — 인계·내역이 이것 하나로 만남.""" from B09_Estimation.B09_Estimation_UnitPrice import normalize_variant_key return f"B-{code}#{normalize_variant_key(key)}" def priced_sheets( sheets: Iterable[dict[str, Any]], templates: dict[str, dict[str, Any]], row_edits: dict[str, Any] | None = None, ) -> list[dict[str, Any]]: """양식 일위대가로 셀 장 — 양식(고친 줄 조합 얹음)에 일위대가 줄과 코드가 있는 장만.""" from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import with_rows found = [] for sheet in sheets: type_id = str(sheet.get("type_id") or "") base = templates.get(type_id) if not base or not base.get("code") or not sheet.get("formula_sheet"): continue template = with_rows(base, (row_edits or {}).get(type_id)) rows = template["unit_price"]["rows"] if not rows: continue names = {row.get("seq"): str(row.get("name") or "") for row in template.get("rows") or []} found.append( { "type_id": type_id, "code": str(template["code"]), "name": str(template.get("name") or type_id), "title": str(sheet.get("title") or ""), "key": sheet_variant_key(template, sheet), "structure_ids": [ str(member["structure_id"]) for member in sheet.get("members") or [] if member.get("structure_id") ], # 일위대가가 품은 원단위 줄 이름 — 따로 인계하면 두 번 셈. "covered": sorted( {names[r["from_row"]] for r in rows if r.get("from_row") in names} ), "template": template, "sheet": sheet, } ) return found def by_structure(entries: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any]]: return {sid: entry for entry in entries for sid in entry["structure_ids"]} def template_row(row: dict[str, Any], structure: dict[str, Any], entry: dict[str, Any]) -> dict: """전개 인계 줄(같은 칸 29개 계약)을 **양식 일위대가 줄**로 바꿔 씀 — 코드·갈래·수량·단위만.""" length = float(structure.get("length_m") or 0.0) in_bill = length > 0 return { **row, "work_item_code": entry["code"], "name": entry["name"], "spec": entry["title"], "spec_detail": entry["title"], "unit": "m", "quantity": length, "composite_parts": None, "composite_not_ready": None, "structure_kind": None, "variant_axis": VARIANT_AXIS, "variant_value": entry["key"], "secondary_axes": None, "spec_class": None, "spec_class_basis": f"구조물도 양식 일위대가 — 품은 줄: {'·'.join(entry['covered'])}", # 막힘은 일위대가 표가 정함(B09 가 셈) — 전개 쪽 갈래 사유는 여기 안 실음. "blocked_kind": None if in_bill else row.get("blocked_kind"), "blocked_reason": "" if in_bill else row.get("blocked_reason") or "연장이 0 이라 안 세움", "in_bill": in_bill, "in_bill_reason": "" if in_bill else "연장이 0 이라 내역에 안 세움", } def price_sheets( entries: Iterable[dict[str, Any]], book: Any, find_variant: Callable[[str, str], str | None], library: Iterable[dict[str, Any]], manual_all: dict[str, Any] | None = None, ) -> dict[str, dict[str, Any]]: """B09 내역이 쓸 호표 금액 `{B-AX-ST-…#키: {money, total, blocked, unconfirmed, reasons}}`. ⚠ `money` 는 **호표 표의 성분 소계**(칸마다 0.1원 절사해 더한 뒤 성분마다 1원 절사) · `total` 은 그 합 — 명세 7장 「호표 안 성분 소계는 절사」. 안 자른 값을 쓰면 구조물도 화면(192,429)과 내역 단가(192,430)가 1원 갈림(2026-09-13 검증 프로젝트 실측). """ from decimal import Decimal from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import unit_price_money from B09_Estimation.B09_Estimation_PriceBook import Money3 prices: dict[str, dict[str, Any]] = {} library = list(library) for entry in entries: assembled = unit_price_money( entry["template"], entry["sheet"], book, find_variant, library, (manual_all or {}).get(entry["type_id"]) or {}, ) if assembled is None: continue table, _exact = assembled prices[structure_price_code(entry["code"], entry["key"])] = { "money": Money3(*(Decimal(str(table[k])) for k in ("material", "labor", "expense"))), "total": Decimal(str(table["total"])), "blocked": table["blocked"], "unconfirmed": table["unconfirmed"], # 단위당 구성(단가표 코드, 수량) — B09 집계표가 자원까지 풂. 수동 단가 줄은 못 풂. "parts": [ (str(row["ref_code"]), Decimal(str(row["quantity"]))) for row in table["rows"] if row.get("ref_code") and "total" in row and not row.get("manual") ], "reasons": [ f"{row['name']}: {row['reason']}" for row in table["rows"] if row["reason"] and not row["skipped"] and "total" not in row ], } return prices