feat(b08): 운반 묶음 함수 — 총괄자재 → 운반 행 · B09 운반공 줄 같은 원천 (251-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
This commit is contained in:
2026-10-11 01:09:11 +09:00
co-authored by Claude Opus 5.5
parent 6fa684e0fe
commit 5770cb4ce7
3 changed files with 160 additions and 20 deletions
+103
View File
@@ -0,0 +1,103 @@
"""B08 운반 행(PLAN 251-1 · 사용자 159번) — 총괄자재표 → 운반 묶음.
거리이정표 행 · B09 운반공 줄의 한 원천.
묶음 = 총괄 줄 중 수량 있는 것 · 같은 묶음(이형철근 · 레미콘 · 깬돌 · 파형강관 ·
커플링밴드)은 한 행 · 골재(모래 · 자갈 · 막자갈 · 혼합석) · 그 밖 줄은 따로 ·
채집석 공제 뺌 · 장비 두 행(중기 · 기계) 늘.
옛 B09 운반 6줄 키(`haul:1~6` · 중기 · 기계 · 모래 · 자갈 · 시멘트 · 철근)는 그 묶음에 그대로 —
사용자 설계내역서 덮개가 고아가 되지 않게 · 그 여섯은 수량이 없어도 늘 있음.
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Material_Calc import DEDUCT_KEY
#: 장비 — (묶음 키, 이름, 단위, 옛 번호)
EQUIP: list[tuple[str, str, str, int]] = [
("equip_heavy", "중기", "대", 1),
("equip_machine", "기계", "대", 2),
]
#: 옛 B09 운반 줄 번호 — 묶음 키 → n(`haul:n`)
LEGACY = {
"equip_heavy": 1,
"equip_machine": 2,
"sand": 3,
"gravel": 4,
"cement": 5,
"rebar": 6,
}
#: 총괄 줄 키 머리 → (묶음 키, 이름) — 여러 규격 = 한 행
PREFIX = [
("rebar_", "rebar", "철근"),
("remicon_", "remicon", "레미콘"),
("stone_buy_", "stone", "깬돌"),
("pipe_", "pipe", "파형강관"),
("coupling_", "coupling", "커플링밴드"),
]
#: 총괄 고정 줄 이름 · 단위(줄에 이름이 없어도 같은 이름 · 옛 여섯 늘 행)
NAMES = {
"sand": ("모래", "㎥"),
"gravel": ("자갈", "㎥"),
"rubble": ("막자갈", "㎥"),
"mixed_aggregate": ("혼합석", "㎥"),
"cement": ("시멘트", "대"),
"rebar": ("철근", "ton"),
"wire_mesh": ("와이어메쉬", "㎡"),
"sod": ("떼", "㎡"),
"revet_block": ("호안블럭", "㎡"),
"flume_400c": ("수로관", "본"),
"steel_grating": ("스틸그레이팅", "EA"),
}
def _group(row: dict[str, Any]) -> tuple[str, str]:
key = str(row.get("key") or "")
for head, gkey, name in PREFIX:
if key.startswith(head):
return gkey, name
if key in NAMES:
return key, NAMES[key][0]
return key, str(row.get("group") or row.get("spec") or key) # 맞춤 없는 사본 자재 = 자재 이름
def bill_key(gkey: str) -> str:
"""B09 운반 줄 키 — 옛 여섯은 `haul:n` 그대로 · 새 묶음 = `haul:<묶음 키>`."""
return f"haul:{LEGACY[gkey]}" if gkey in LEGACY else f"haul:{gkey}"
def haul_groups(materials: dict[str, Any] | None) -> list[dict[str, Any]]:
"""총괄자재표(`material.rows`) → 운반 행 [{key, name, unit, qty}].
장비 둘 + 묶음(차례 = 총괄 줄 차례)."""
rows = ((materials or {}).get("material") or {}).get("rows") or []
out: dict[str, dict[str, Any]] = {
g: {"key": g, "name": n, "unit": u, "qty": None} for g, n, u, _ in EQUIP
}
for row in rows:
if row.get("key") == DEDUCT_KEY: # 채집석 공제(뺄 수량)
continue
gkey, name = _group(row)
amount = row.get("amount")
has = isinstance(amount, (int, float)) and amount > 0
slot = out.get(gkey)
if slot is None:
if not has and gkey not in LEGACY:
continue # 수량 없는 새 묶음 = 행 없음
slot = out[gkey] = {
"key": gkey,
"name": name,
"unit": row.get("unit") or "",
"qty": None,
}
if has and (slot["unit"] in ("", row.get("unit"))):
slot["unit"] = row.get("unit") or slot["unit"]
slot["qty"] = round((slot["qty"] or 0) + float(amount), 6)
for gkey in LEGACY: # 옛 여섯 = 늘
if gkey not in out:
name, unit = NAMES[gkey]
out[gkey] = {"key": gkey, "name": name, "unit": unit, "qty": None}
return list(out.values())
+7 -20
View File
@@ -14,6 +14,7 @@ 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 = "조합 밖 토공 산근 — 연결 없음"
@@ -46,16 +47,6 @@ EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [
STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3"}
SKIP_COLUMNS = ("pipe:cp|",)
#: 운반공 — (명칭, 단위, B08 총괄자재 줄 키(합) | None)
HAUL: list[tuple[str, str, tuple[str, ...] | None]] = [
("중기운반", "대", None),
("기계운반", "대", None),
("모래운반", "㎥", ("sand",)),
("자갈운반", "㎥", ("gravel",)),
("시멘트운반", "대", ("cement",)),
("철근운반", "ton", ("rebar_d13", "rebar_d16", "rebar_d19", "rebar_d22", "rebar_d25")),
]
def _same(row: dict[str, Any], want: tuple[str, str, str | None]) -> bool:
group, kind, spec = want
@@ -114,21 +105,17 @@ def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]:
def haul_items(materials: dict[str, Any] | None) -> list[dict[str, Any]]:
rows = ((materials or {}).get("material") or {}).get("rows") or []
amount = {row.get("key"): row.get("amount") for row in rows}
"""운반공 줄 — B08 운반 묶음(251-1 · 장비 둘 + 총괄자재 묶음 · 옛 키 haul:1~6 그대로)."""
out = []
for n, (name, unit, keys) in enumerate(HAUL, 1):
qty = None
if keys:
found = [amount[k] for k in keys if amount.get(k) is not None]
qty = round(sum(found), 6) if found else None
for g in haul_groups(materials):
qty = g["qty"]
out.append(
{
"key": f"haul:{n}",
"key": bill_key(g["key"]),
"section": "3",
"name": name,
"name": f"{g['name']}운반",
"spec": "",
"unit": unit,
"unit": g["unit"],
"qty": qty,
"note": "B08 총괄자재" if qty is not None else "",
"ref": None,
@@ -0,0 +1,50 @@
"""PLAN 251-1 — 총괄자재표 → 운반 묶음(거리이정표 행 · B09 운반공 줄 한 원천)."""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Haul import bill_key, haul_groups # noqa: E402
from B09_Estimation.B09_Estimation_Bill_Items import haul_items # noqa: E402
def _mats(rows):
return {"material": {"rows": rows}}
ROWS = [
{"key": "sand", "group": "골재", "spec": "모래", "unit": "㎥", "amount": 16},
{"key": "gravel", "group": "골재", "spec": "자갈", "unit": "㎥", "amount": None},
{"key": "mixed_aggregate", "group": "골재", "spec": "혼합석", "unit": "㎥", "amount": 3.5},
{"key": "remicon_25_24_15", "group": "레미콘", "spec": "25-24-15", "unit": "㎥", "amount": 2},
{"key": "remicon_25_21_80", "group": "레미콘", "spec": "25-21-80", "unit": "㎥", "amount": 1},
{"key": "rebar_d13", "group": "이형철근", "spec": "D13mm", "unit": "ton", "amount": 0.5},
{"key": "rebar_d16", "group": "이형철근", "spec": "D16mm", "unit": "ton", "amount": 0.25},
{"key": "cement", "group": "시멘트", "spec": "40kg/대", "unit": "대", "amount": None},
{"key": "pipe_600", "group": "파형강관", "spec": "Φ600mm", "unit": "m", "amount": 12},
{"key": "picked_deduct", "group": "채집석 공제", "unit": "㎥", "amount": 4},
{"key": "MT000777", "group": "앵커볼트", "spec": "M16", "unit": "EA", "amount": 8},
{"key": "sod", "group": "떼", "unit": "㎡", "amount": 0},
]
def test_묶음_장비_옛_여섯_늘_수량_없는_새_묶음_없음():
got = {g["key"]: (g["name"], g["unit"], g["qty"]) for g in haul_groups(_mats(ROWS))}
assert list(got)[:2] == ["equip_heavy", "equip_machine"] # 장비 둘 맨 앞
assert got["sand"] == ("모래", "㎥", 16) and got["gravel"] == ("자갈", "㎥", None)
assert got["mixed_aggregate"] == ("혼합석", "㎥", 3.5)
assert got["remicon"] == ("레미콘", "㎥", 3) and got["rebar"] == ("철근", "ton", 0.75)
assert got["cement"] == ("시멘트", "대", None) # 옛 여섯 = 수량 없어도 늘
assert got["pipe"] == ("파형강관", "m", 12) and got["MT000777"] == ("앵커볼트", "EA", 8)
assert "picked_deduct" not in got and "sod" not in got # 공제 · 수량 0 새 묶음 = 행 없음
def test_B09_운반_줄_옛_키_그대로_새_묶음_키():
items = {i["key"]: i for i in haul_items(_mats(ROWS))}
assert [bill_key(k) for k in ("equip_heavy", "sand", "rebar")] == ["haul:1", "haul:3", "haul:6"]
assert items["haul:3"]["name"] == "모래운반" and items["haul:3"]["qty"] == 16
assert items["haul:remicon"]["name"] == "레미콘운반" and items["haul:remicon"]["unit"] == "㎥"
assert all(i["section"] == "3" for i in items.values())
assert {"haul:1", "haul:2", "haul:3", "haul:4", "haul:5", "haul:6"} <= set(items)