From 5770cb4ce7ec228bfeb6d25178eb039246b82f00 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 11 Oct 2026 01:09:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EC=9A=B4=EB=B0=98=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=ED=95=A8=EC=88=98=20=E2=80=94=20=EC=B4=9D=EA=B4=84?= =?UTF-8?q?=EC=9E=90=EC=9E=AC=20=E2=86=92=20=EC=9A=B4=EB=B0=98=20=ED=96=89?= =?UTF-8?q?=20=C2=B7=20B09=20=EC=9A=B4=EB=B0=98=EA=B3=B5=20=EC=A4=84=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=9B=90=EC=B2=9C=20(251-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY --- B08_Quantity/B08_Quantity_Haul.py | 103 ++++++++++++++++++++ B09_Estimation/B09_Estimation_Bill_Items.py | 27 ++--- resources/tester/test_251_1_haul_groups.py | 50 ++++++++++ 3 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 B08_Quantity/B08_Quantity_Haul.py create mode 100644 resources/tester/test_251_1_haul_groups.py diff --git a/B08_Quantity/B08_Quantity_Haul.py b/B08_Quantity/B08_Quantity_Haul.py new file mode 100644 index 000000000..b8ccc551f --- /dev/null +++ b/B08_Quantity/B08_Quantity_Haul.py @@ -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()) diff --git a/B09_Estimation/B09_Estimation_Bill_Items.py b/B09_Estimation/B09_Estimation_Bill_Items.py index 29891ac95..a3a3ac17d 100644 --- a/B09_Estimation/B09_Estimation_Bill_Items.py +++ b/B09_Estimation/B09_Estimation_Bill_Items.py @@ -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, diff --git a/resources/tester/test_251_1_haul_groups.py b/resources/tester/test_251_1_haul_groups.py new file mode 100644 index 000000000..127f09d49 --- /dev/null +++ b/resources/tester/test_251_1_haul_groups.py @@ -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)