"""B09 집계표 넷(PLAN 145-5) — 재료비 · 노무비 · 경비 수량금액집계표 · 중기시간금액집계표 (서버 단독). 역전개 = 설계내역서 항목 수량 × 호표 구성(산근 · 대가 줄 · 겹 호표 끝까지) → 자원마다 총수량. 호표 구성 = `hopyo_lines(sheets)` — 산근 블록(145-1 `sheets['sg']`) · 대가 블록 (145-2 `sheets['daega_blocks']`) 줄을 `{호표 키: {"lines": [{kind, key, qty}]}}` (호표 1 단위당 · kind = labor · material · expense · machine · hopyo)로 바꿈 · 중기 = 중기목록표 시간당 노 · 재 · 경(144-4 블록) 그대로 · % 줄 · 빈칸 줄 = 자원 아님(안 셈) 4 자재대 · 관급자재대 자재 줄 = 그 자재 그대로(호표 없음) 연결 없는 항목(수량 · 호표 없음 · 호표 구성 없음 — 조합 밖 토공 · 운반 산근 등) = 전개 안 함 · 「연결 없음 n」(`unlinked`) 끝수 = 엑셀 자리 그대로 — 총수량 ROUND 2 · 금액 = ROUND(수량 × 단가, 0)(여기만 반올림) · 중기 = 노무 · 재료 · 경비 각 ROUND(시간 × 시간당, 0) · 합계 = 셋 합. 단가 = 목록표(노무비 · 재료비 · 경비 · 중기목록표) — 없으면 금액 빈칸(수량은 둠). 결과는 GET 때 셈(저장 안 함) · 조작 중엔 다시 안 셈([저장] 뒤 서버 정본). """ from __future__ import annotations from typing import Any from B09_Estimation.B09_Estimation_Calc_Base import num, round_half #: 겹 호표 깊이 한도(엑셀 최대 대가 → 대가 → 산근 → 중기 = 4) — 돌고 도는 구성 막음 MAX_DEPTH = 8 KINDS = ("material", "labor", "expense", "machine") def expand( lines_of: dict[str, Any], key: str, qty: float, out: dict[str, dict[str, float]], depth: int = 0 ) -> None: """호표 하나 × 수량 → 자원 총수량에 더함(겹 호표는 끝까지 · 깊이 한도 넘으면 멈춤).""" if depth > MAX_DEPTH: return for line in (lines_of.get(key) or {}).get("lines") or []: q = num(line.get("qty")) if q is None: continue kind = line.get("kind") if kind == "hopyo": expand(lines_of, str(line.get("key")), qty * q, out, depth + 1) elif kind in KINDS: bucket = out.setdefault(kind, {}) bucket[str(line.get("key"))] = bucket.get(str(line.get("key")), 0.0) + qty * q def totals( sheets: dict[str, Any], lines_of: dict[str, Any] ) -> tuple[dict[str, dict[str, float]], int]: """설계내역서 항목 → (자원 총수량(내역 차례 · 관급 포함), 연결 없음 항목 수).""" out: dict[str, dict[str, float]] = {} unlinked = 0 for row in (sheets.get("bill") or {}).get("rows") or []: if row.get("kind") != "item": continue qty = num(row.get("qty")) ref = row.get("ref") or {} key = str(ref.get("key") or "") if ref.get("tab") == "material" and key and qty is not None: bucket = out.setdefault("material", {}) bucket[key] = bucket.get(key, 0.0) + qty elif qty is not None and key in lines_of: expand(lines_of, key, qty, out) else: unlinked += 1 return out, unlinked def _list(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: return {row["key"]: row for row in rows} def _resource_sheet(used: dict[str, float], listed: dict[str, dict[str, Any]], unit_default: str): rows = [] for no, (key, qty) in enumerate(used.items(), 1): src = listed.get(key) or {} q = round_half(qty, 2) price = num(src.get("price")) rows.append( { "no": no, "key": key, "name": src.get("name") or key, "spec": src.get("spec") or "", "unit": src.get("unit") or unit_default, "qty": q, "price": price, "amount": round_half(q * price, 0) if price is not None else None, "note": "" if src else "목록표에 없음", } ) return rows def _machine_sheet(used: dict[str, float], listed: dict[str, dict[str, Any]]): rows = [] for no, (key, qty) in enumerate(used.items(), 1): src = listed.get(key) or {} q = round_half(qty, 2) parts = {} for part in ("labor", "material", "expense"): unit = num(src.get(part)) parts[part] = round_half(q * unit, 0) if unit is not None else None known = all(v is not None for v in parts.values()) rows.append( { "no": no, "key": key, "name": src.get("name") or key, "spec": src.get("spec") or "", "unit": "시간", "qty": q, **parts, "sum": parts["labor"] + parts["material"] + parts["expense"] if known else None, "note": "" if src else "목록표에 없음", } ) return rows def compute_summary(sheets: dict[str, Any], lines_of: dict[str, Any]) -> dict[str, Any]: """집계표 넷 — `sheets` = 짝 셈 결과(바탕 + 설계내역서).""" used, unlinked = totals(sheets, lines_of) return { "unlinked": unlinked, "sum_material": _resource_sheet(used.get("material", {}), _list(sheets["material"]), ""), "sum_labor": _resource_sheet(used.get("labor", {}), _list(sheets["labor"]), "인"), "sum_expense": _resource_sheet(used.get("expense", {}), _list(sheets["expense"]), ""), "sum_machine": _machine_sheet(used.get("machine", {}), _list(sheets["machine"])), } #: 산근 · 대가 블록 줄 kind → 호표 구성 kind(자원 · 겹 호표) — % 줄 · 빈칸 줄은 빠짐 LINE_KIND = { "labor": "labor", "material": "material", "expense": "expense", "machine": "machine", "sangeun": "hopyo", "daega": "hopyo", } def block_lines(blocks: list[dict[str, Any]] | None) -> dict[str, Any]: """블록 [{key, lines: [{kind, ref: {key}, qty}]}] → {호표 키: {"lines": [{kind, key, qty}]}}.""" out: dict[str, Any] = {} for block in blocks or []: lines = [] for line in block.get("lines") or []: kind = LINE_KIND.get(line.get("kind")) key = (line.get("ref") or {}).get("key") if kind and key and num(line.get("qty")) is not None: lines.append({"kind": kind, "key": str(key), "qty": line["qty"]}) out[str(block["key"])] = {"lines": lines} return out def hopyo_lines(sheets: dict[str, Any]) -> dict[str, Any]: """호표 구성(1 단위당 자원 · 겹 호표) — 산근(145-1) + 대가(145-2 · 들어오면).""" return {**block_lines(sheets.get("sg")), **block_lines(sheets.get("daega_blocks"))}