"""B09 원가계산 — 사급 자재총괄 줄을 **내역 본체 「자재」 줄**로 (PLAN 1장 Ⓐ-2 · 브레인 판정). 셈부터 함(배정 프로젝트 자재총괄 9줄): ㉠ 일위대가를 거쳐 이미 내역에 선 자재 0 · ㉡ 일위대가에 안 드는 자재 9 · ㉢ ㉠ 의 할증 0 — 품셈 표에 재료 줄이 없는 공종(규준틀·찰쌓기·타설)의 자재라 따로 계상하는 것이 실무 모양. 본체 줄로 서므로 도급 재료비·계약·기성·집계가 고칠 곳 없이 그 줄을 셈. 올리는 줄 사급 · 수동 단가가 선 줄만(미확정 1건) — 관급은 수량표에만(도급 금액 밖) ⚠ 이중계상 가드 — 같은 구조물 내역 줄의 일위대가 자재(붙은 것·못 붙은 것)와 겹치면 안 올리고 사유로 멈춤. **코드가 있으면 코드로, 없을 때만 이름으로**(명세 2장). 막는 쪽이라 틀려도 과소 (줄이 안 서고 사유가 뜸) — 양식이 늘어 ㉠ 이 생기는 날 금액이 조용히 부풀지 않게. """ from __future__ import annotations from decimal import Decimal from typing import Any, Callable from B09_Estimation.B09_Estimation_BillOfQuantities import BillResult, BillRow from B09_Estimation.B09_Estimation_PriceBook import Money3, PriceKind from B09_Estimation.B09_Estimation_UnitPrice import FUEL_CODE_PREFIX, UnitPriceBuild MATERIAL_GROUP_NAME = "자재(사급)" DOUBLE_COUNT_SUSPECT = "double_count_suspect" _ZERO = Decimal(0) def _flat(text: Any) -> str: return "".join(str(text or "").split()) def unit_price_materials(row: BillRow, unit_prices: UnitPriceBuild) -> list[tuple[str, str]]: """그 내역 줄의 일위대가가 밟는 자재 — 붙은 것 (코드, 이름) · 못 붙은 줄 (「」, 글).""" book = unit_prices.book stack = [code for code in (row.price_code, *(code for code, _ in row.parts)) if code] seen: set[str] = set() found: list[tuple[str, str]] = [] while stack: code = stack.pop() if code in seen: continue seen.add(code) title = book.titles.get(code) if title is not None and title.kind is PriceKind.MATERIAL: if not code.startswith(FUEL_CODE_PREFIX): # 기계 연료는 자재총괄 자재가 아님 found.append((code, title.name)) if code.startswith("B-"): labels = unit_prices.unattached.get(code[2:].split("#", 1)[0]) or [] found.extend(("", label) for label in labels) stack.extend(detail.ref_code for detail in book.details.get(code, [])) return found def overlap(name: str, code: str, inside: list[tuple[str, str]]) -> str: """겹친 일위대가 자재 글 — 코드가 있으면 코드로, 없으면 이름(공백 무시 앞머리)으로.""" if code: return next((label for found, label in inside if found == code), "") wanted = _flat(name) return next((label for _, label in inside if wanted and _flat(label).startswith(wanted)), "") def raise_material_rows( result: BillResult, unit_prices: UnitPriceBuild, next_number: Callable[[str], str], bill_line: Callable[[Money3, Decimal], Money3], ) -> None: """자재대 표의 사급·수동 단가 줄 → 본체 「자재(사급)」 묶음 줄. 가드에 걸린 줄은 사유만.""" from B09_Estimation.B09_Estimation_BillOfQuantities_Rows import settle_quantity sheet = result.material_sheet picked = [] for item in getattr(sheet, "contractor_rows", []): if item.amount_krw is None or not item.manual: continue inside = [ pair for row in result.rows if not row.is_group and row.name in item.source_structure for pair in unit_price_materials(row, unit_prices) ] hit = overlap(item.name, getattr(item, "code", ""), inside) if hit: reason = f"두 번 셀 수 있어 내역 줄로 안 올림 — 같은 구조물 일위대가에 「{hit}」" item.note = f"{item.note} · {reason}" result.missing.append( { "name": item.key, "unit": item.unit, "quantity": str(item.total_amount), "reason": reason, "blocked_kind": DOUBLE_COUNT_SUSPECT, } ) continue picked.append(item) if not picked: return group_no = next_number("") result.rows.append( BillRow(item_no=group_no, level=1, code=None, name=MATERIAL_GROUP_NAME, is_group=True) ) for index, item in enumerate(picked, start=1): price = item.unit_price_krw row = BillRow( item_no=f"{group_no}-{index}", level=2, code=None, name=item.name, spec=item.spec, unit=item.unit, quantity=item.total_amount, ) line = bill_line(Money3(material=price), settle_quantity(row)) # 수량 확정 뒤 금액 row.unit_material_krw, row.unit_labor_krw, row.unit_expense_krw = price, _ZERO, _ZERO row.unit_price_krw = price row.amount_krw = line.total row.material_krw = line.material row.unconfirmed = 1 row.add_note("quantity", f"자재총괄 할증 뒤 수량 — {', '.join(item.source_structure)}") row.add_note("unit_price_krw", item.note) result.unconfirmed.append({"name": row.name, "code": item.key, "count": 1}) result.rows.append(row)