Files
Aislo/B09_Estimation/B09_Estimation_Summary.py
T
eomsangdonandClaude Opus 5.5 a24e0d622c feat(M02·B09 고정 줄): 일위대가 줄 「고정」 갈래 · B09 대가 fixed 줄 · 이음 단가 · 집계표 빠짐 수 (PLAN 705-1)
- M02 일위대가 줄 {고정: 이름 · 규격 · 단위 · 단가 · 이음(노임 · 자재 · 기계) · 출처} — 정리 · 셈(새 파일 M02_Labor_Fixed) · 줄 가름 짝 TS lineId
- 사본 단가 묶음 · B09 단가 책 수요에 이음 키
- B09 대가 줄 fixed — 이은 비목만 단가 책 값 · 못 이음 = 줄 단가 · 덮개 먹음 · 빈칸 안 됨(파이썬 · TS 짝)
- 집계표 — 이은 고정 줄 = 자원 셈 · 이음 없는 줄 = 빠짐 수(summary.fixed · 안내 「고정 줄 n」)
- B09 일위대가표 줄 꼬리표 「고정」 값 넘김
- 시험 test_705_1 12 · test_145_5 응답 칸 목록에 fixed(낡은 단정 고침)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:05:47 +09:00

202 lines
8.8 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`)
고정 줄(705-1) = 이은 노임 · 자재 · 기계 키 자원으로 셈 · 이음 없는 줄 = 집계에서 빠짐 ·
「고정 줄 n」(`fixed` · 빠진 줄 수)
끝수 = 엑셀 자리 그대로 — 총수량 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, "fixed"):
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,
"fixed": len(used.get("fixed", {})),
"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 []:
if line.get("kind") == "fixed" and num(line.get("qty")) is not None:
lines += _fixed_lines(block, line)
continue
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 _fixed_lines(block: dict[str, Any], line: dict[str, Any]) -> list[dict[str, Any]]:
"""고정 줄(705-1) → 이은 자원 줄(기계 = 중기 하나) · 이음 없음 = `fixed` 줄(빠짐 셈)."""
link = line.get("link") or {}
pairs = [("machine", link.get("machine"))] if link.get("machine") else []
pairs = pairs or [(k, link.get(k)) for k in ("labor", "material") if link.get(k)]
if not pairs:
pairs = [("fixed", f"{block['key']}|{line.get('cid') or line.get('name')}")]
return [{"kind": k, "key": str(v), "qty": line["qty"]} for k, v in pairs]
def used_materials(sheets: dict[str, Any], inputs: dict[str, Any], user: dict[str, Any]) -> set:
"""재료비목록표에 남길 자재 키(183-1) — 대가 · 산근 · 중기 블록 재료 줄(수량 빈칸 · 0 도 =
집계표 역전개와 같은 줄) · B08 총괄 줄(수량 있는 입력) · 관급 · 별도 표시 줄.
그 밖(고르기 후보 줄) = 뺌."""
blocks = [*(sheets.get("sg") or []), *(sheets.get("daega_blocks") or [])]
blocks += sheets.get("machine_blocks") or []
keep = {
str((line.get("ref") or {}).get("key"))
for block in blocks
for line in block.get("lines") or []
if line.get("kind") == "material" and (line.get("ref") or {}).get("key")
}
keep |= { # 705-1 고정 줄이 이은 자재
str(line["link"]["material"])
for block in blocks
for line in block.get("lines") or []
if line.get("kind") == "fixed" and (line.get("link") or {}).get("material")
}
marks = user.get("materials") or {}
for m in inputs.get("materials") or []:
if "qty" in m or m.get("supply") or (marks.get(m["key"]) or {}).get("supply"):
keep.add(m["key"])
return keep
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"))}