Files
Aislo/B09_Estimation/B09_Estimation_Summary.py
T
eomsangdonandClaude Opus 5.5 49b780e147 feat(B09): 집계표 역전개를 산근 블록에 이음 · 연결 없음 n(145-5)
- hopyo_lines(sheets) = 산근 블록(145-1 sheets['sg']) · 대가 블록(145-2 daega_blocks) 줄 → 호표 구성
- 산근 → 하위 산근 → 중기 겹 전개 · % 줄 · 빈칸 줄은 자원 아님
- 수량 · 호표 · 호표 구성 없는 내역 항목 = 전개 안 함 · summary.unlinked · 화면 안내 「연결 없음 n」
- 시험 = 엑셀 노무 석공 · 보통인부 · 재료 폭약 · 중기 굴삭기+브레카 반올림 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
2026-10-09 15:25:54 +09:00

161 lines
6.7 KiB
Python

"""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"))}