Files
Aislo/B09_Estimation/B09_Estimation_Bill_Items.py
T
eomsangdonandClaude Opus 5.5 b751f29b01 fix(b09): 운반공 줄 이름 = 거리이정표 항목명 그대로 · 행마다 개별 줄 (261-4)
- 「운반」 덧붙임 없앰 · 빈 항목명 = 거래처 · 그것도 없으면 묶음 이름 · 「운반」
- 자동 상자 둘째 행부터 `haul:{묶음}.n` 개별 줄(묶음 줄 바로 뒤 · 수량 빈칸)
- 시험 기대값 고침(253_1 · 251_1 · 145_3) · 새 시험 test_261_4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
2026-10-11 15:19:06 +09:00

170 lines
7.8 KiB
Python

"""B09 설계내역서 항목 짜기(PLAN 145-3) — 서버 단독 · B08 수량 → 내역 줄(수량 · 단가 자리).
공종 나무 = 엑셀 기번3 공통 꼴 고정(`B09_Estimation_Calc_Bill.SECTIONS`) · 공구 안 나눔.
토공(1.1~1.6) = B08 토공집계표 줄 짝(`EARTH` — 엑셀 항목 이름 ↔ 토공집계표 구분 · 종류 · 규격) ·
단가 = 조합 밖 토공 산근 · 준비공 대가 — 원천 없음 → 빈칸 「연결 없음」(지시)
구조물(2.1~2.3) = B08 구조물 집계표 열(사본 있을 때만 · 143) ·
구조물군 → 공종 짝(`STRUCTURE_SECTION`)
· 커플링 열은 자재(관급자재대)라 뺌 · 단가 = 대가 호표(145-2 가 `hopyo` 로 줌 · 그전엔 빈칸)
운반(3) = 엑셀 운반공 줄 · 수량 = B08 총괄자재 짝 · 거리 · 단가 = 운반 산근(원천 없음 → 빈칸)
자재대 · 관급자재대 = 화면 짝이 대비표 「별도」 · 「관급」 표시로 짬(사용자 조작 즉시)
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Haul import bill_key, haul_groups
from B08_Quantity.B08_Quantity_Material_Calc import _structure_row
WHY_EARTH = "조합 밖 토공 산근 — 연결 없음"
WHY_READY = "준비공 대가 — 연결 없음"
WHY_HAUL = "운반 산근 — 연결 없음(운반거리 원천 없음)"
WHY_LOGIC = "새 산근 — [저장] 뒤 단가(서버 셈)"
WHY_STRUCTURE = "대가 호표 — 연결 없음(일위대가 145-2)"
#: 토공 항목 — (공종, 엑셀 명칭, 단위, 토공집계표 (구분, 종류, 규격 | None) | None)
EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [
("1.1", "규준틀설치 종단규준틀", "개소", None),
("1.1", "규준틀설치 횡단규준틀", "개소", None),
("1.1", "제근", "㎡", None),
("1.2", "절토(토사)", "㎥", ("흙깍기", "토사", None)),
("1.2", "절토(연암)", "㎥", ("흙깍기", "연암", None)),
("1.2", "암발파", "㎥", ("흙깍기", "경암", None)),
("1.3", "구조물터파기(토사)", "㎥", ("구조물터파기", "토사", None)),
("1.3", "구조물터파기(연암)", "㎥", ("구조물터파기", "연암", None)),
("1.3", "구조물 되메우기", "㎥", ("되메우기", "", None)),
("1.4", "측구파기(토사)", "㎥", ("측구 터파기", "토사", None)),
("1.4", "측구파기(연암)", "㎥", ("측구 터파기", "연암", None)),
("1.5", "유용토운반", "㎥", ("유용토 소계", "", None)),
("1.5", "도자운반", "㎥", ("토사운반", "", "19 TON B/D")),
("1.5", "덤프운반", "㎥", ("토사운반", "", "BH+DT+BH")),
("1.6", "사토처리", "㎥", ("잔토처리", "", None)),
("1.6", "성토사면다짐", "㎡", ("성토면다짐", "", None)),
("1.6", "노면정리", "㎡", None),
]
#: 구조물 집계표 열 id 머리 → 공종(그 밖 = 2.2 배수공) · 커플링 = 자재라 뺌
STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3"}
SKIP_COLUMNS = ("pipe:cp|",)
def _same(row: dict[str, Any], want: tuple[str, str, str | None]) -> bool:
group, kind, spec = want
same = row.get("group") == group and (row.get("kind") or "") == kind
return same and (spec is None or row.get("spec") == spec)
def earth_items(earthwork: dict[str, Any] | None) -> list[dict[str, Any]]:
rows = (earthwork or {}).get("summary") or []
out = []
for n, (section, name, unit, want) in enumerate(EARTH, 1):
source = next((r for r in rows if want and _same(r, want)), None)
out.append(
{
"key": f"earth:{n}",
"section": section,
"name": name,
"spec": (source or {}).get("spec") or "",
"unit": unit,
"qty": source.get("value") if source else None,
"note": "B08 토공집계표" if source else "",
"ref": None,
"why": WHY_READY if section == "1.1" else WHY_EARTH,
}
)
return out
def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]:
columns = (summary or {}).get("columns") or []
total = (summary or {}).get("total") or {}
out = []
for column in columns:
cid = str(column.get("id") or "")
if cid.startswith(SKIP_COLUMNS):
continue
section = next((s for head, s in STRUCTURE_SECTION.items() if cid.startswith(head)), "2.2")
name, spec, unit = _structure_row(column)
combos = [c for c in column.get("combos") or [] if c]
out.append(
{
"key": f"st:{cid}",
"section": section,
"name": name,
"spec": spec,
"unit": unit,
"qty": total.get(cid),
"note": " · ".join(combos),
"ref": {"tab": "dg_list", "key": cid},
# 사본 주인마다 값(B08 145-3 덧칸) — 대가 블록마다 줄을 나눌 때 씀
"owners": dict(column.get("owners") or {}),
"why": WHY_STRUCTURE,
}
)
return out
def haul_items(
materials: dict[str, Any] | None,
refs: dict[str, Any] | None = None,
routes: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
"""운반공 줄 — B08 운반 묶음(251-1 · 장비 둘 + 총괄자재 묶음 · 옛 키 haul:1~6 그대로) +
거리이정표 행(253-1 · `B08_Quantity_Haul.haul_rows` · 행 하나 = 줄 하나 · 둘째 행부터 수량 빈칸) ·
이름 = 거리이정표 항목명 그대로(261-4 · 「운반」 안 붙임 · 비면 거래처 · 그것도 없으면 묶음 이름 · 「운반」) ·
연결 = 거리이정표 단가연결(251-5 · `refs{줄 키: ref}` · 설계내역서 연결 덮개가 이김) ·
`dist{L1, L2}` = 그 행 거리(253-3 「운반 단가」 모달 · 없으면 None)."""
rows = routes or []
by_key = {r["key"]: r for r in rows}
extra: dict[str, list[dict[str, Any]]] = {} # 자동 상자 둘째 행부터 — 묶음 줄 바로 뒤
for r in rows:
if r.get("haul") and r["key"] != bill_key(str(r["haul"])):
extra.setdefault(str(r["haul"]), []).append(r)
def line(key: str, name: str, unit: str, qty: Any, note: str) -> dict[str, Any]:
ref = (refs or {}).get(key)
route = by_key.get(key) or {}
return {
"key": key,
"section": "3",
"name": name,
"spec": "",
"unit": unit,
"qty": qty,
"note": note,
"ref": ref,
"dist": {"L1": route["L1"], "L2": route["L2"]} if route else None,
"why": WHY_LOGIC if (ref or {}).get("tab") == "logic" else WHY_HAUL,
}
out = []
for g in haul_groups(materials):
key = bill_key(g["key"])
name = (by_key.get(key) or {}).get("name") or g["name"]
out.append(
line(key, name, g["unit"], g["qty"], "B08 총괄자재" if g["qty"] is not None else "")
)
for r in extra.get(g["key"], []):
out.append(line(r["key"], r.get("name") or g["name"], "", None, "B08 거리이정표"))
for r in rows:
if r.get("user"):
out.append(line(r["key"], r.get("name") or "운반", "", None, "B08 거리이정표"))
return out
def bill_items(b08: dict[str, Any]) -> dict[str, Any]:
return {
"items": [
*earth_items(b08.get("earthwork")),
*structure_items(b08.get("summary")),
*haul_items(b08.get("materials"), b08.get("haul_refs"), b08.get("haul_routes")),
]
}
def hopyo_prices(project_root: Any) -> dict[str, dict[str, float]]:
"""호표 단가 `{호표 자리 키: {labor, material, expense}}` —
145-1 · 2(산근 · 대가)가 들어오면 채움 · 그전엔 빈 묶음(구조물 단가 = 빈칸 「연결 없음」)."""
return {}