- 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
161 lines
6.7 KiB
Python
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"))}
|