diff --git a/B08_Quantity/B08_Quantity_Router_Summary.py b/B08_Quantity/B08_Quantity_Router_Summary.py index 20b13dcf9..23afc7b27 100644 --- a/B08_Quantity/B08_Quantity_Router_Summary.py +++ b/B08_Quantity/B08_Quantity_Router_Summary.py @@ -17,6 +17,8 @@ 관 몫 = 관 공 › 관매설 · 커플링에 더함(B06 새 키 `ford_pipe_length_m` × 련 · 131) 물넘이포장 › 월류 폭 › H={월류 높이} m 노출형 횡단수로 · 개거 · 맹암거(B05 구조물) 개소 + 석축 · 옹벽(C군 벽 · 구조물 표 형상 wall) › H=높이 m 길이 = 시작~종료(옛 길이 칸) · + 빈 높이 = 「미입력」 열 · 값 빈칸(금액 안 듦 · 701-1) 흙막이 · 돌붙임 열은 관 · 박스 · 세월교 공용(실무 꼴) — 빈 높이: 관 = 관경 최소 · 박스 = 박스 높이 · 세월교 = 「미입력」(본체 끝 높이는 기하에만) 열 id 접두 `pipe:` = 이 집계표(횡단배수) 이름공간 · 같은 측점 박스 + 관은 종류로 갈라 셈. @@ -50,6 +52,7 @@ from B06_Section.B06_Section_Repository import ( ) from B08_Quantity.B08_Quantity_Router_Hydro import _project_root from B08_Quantity.B08_Quantity_Summary_Sheet import _linked, _Sheet, unlinked_facilities +from common_util.common_util_structure_catalog import type_ids_with from common_util.common_util_drainage_pipes import pipe_points_path_in, read_pipe_points_file from common_util.common_util_summary_item import GROUP_JOIN from config.config_db import get_db_pool @@ -79,6 +82,8 @@ SIDE_NAMES = {"inlet": "유입", "outlet": "유출"} SIMPLE = ("cross_drain_exposed", "open_ditch", "cross_underdrain") #: 이 집계표가 펼치는 종류 — 화면은 이 종류를 조합 열에서 뺌(응답 `types`) DRAIN_TYPES = ("pipe", "box_culvert", "ford_bridge", "ford_pavement", *SIMPLE) +#: 석축 · 옹벽 — 구조물 표 형상 wall(본보기 복사 종류 포함 · 701-1) +WALL_TYPES = tuple(sorted(type_ids_with(shape="wall"))) FORMS = ("돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자") SHAPES = ("I", "L", "U") BONDS = ("찰붙임", "메붙임") @@ -460,6 +465,26 @@ def _drain( _chutes(sheet, at, options, links, pipe_names) +def _wall(sheet: _Sheet, item: dict[str, Any], combo: str | None, kinds: dict[str, Any]) -> None: + """석축 · 옹벽 한 곳 — 높이마다 길이 m · 빈 높이 = 「미입력」 열 · 값 빈칸(701-1).""" + type_id = item["type_id"] + sheet.owner, sheet.kind = item["복사본"], type_id + title = next( + (t["name"] for t in kinds.get("types") or [] if t.get("type_id") == type_id), type_id + ) + height = _num(item["options"].get("height_m")) + text = fill._label("height_m", height) + sheet.add( + float(item["chainage_m"]), + f"wall|{type_id}|{text}", + (7, _order(WALL_TYPES, type_id), math.inf if height is None else height), + [title, fill.EMPTY_TEXT if height is None else f"H={text}"], + "m", + fill._length(item) if height is not None else None, + [combo], + ) + + def pipe_summary( project_root: str | Path, designs: list[dict[str, Any]], @@ -492,11 +517,14 @@ def pipe_summary( by_owner: dict[tuple[str, str], dict[str, Any]] = {} # 집계표 밖 구조물 — 사본 하나라도 있나(조합 열은 화면이 그림) others: dict[tuple[str, str], bool] = {} + walls: list[dict[str, Any]] = [] for item in items: owner, _, link = item["복사본"].partition(GROUP_JOIN) if item["type_id"] not in DRAIN_TYPES: key = (item["type_id"], owner) others[key] = others.get(key, False) or item["복사본"] in linked + if item["type_id"] in WALL_TYPES and not link: + walls.append(item) # 석축 · 옹벽 본체 = 길이 열(701-1) continue entry = by_owner.setdefault((item["type_id"], owner), {"links": {}}) # 거름 한 곳 — 사본 없는 잇기는 조합 없음으로(그 칸만 안 셈 · 143-1) @@ -596,6 +624,10 @@ def pipe_summary( _revets(sheet, at, wall_options, links, sources, wall, names, wall_in) _chutes(sheet, at, options, links, names) + for item in walls: + linked_combo = _combo(item.get("summary_item")) if item["복사본"] in linked else None + _wall(sheet, item, linked_combo, kinds) + out = sheet.result() notes = [] missing = sheet.unlinked | {key for key, found in others.items() if not found} @@ -618,7 +650,7 @@ def pipe_summary( **out, "notes": notes, "one_length_m": one_m, - "types": list(DRAIN_TYPES), + "types": [*DRAIN_TYPES, *WALL_TYPES], "pipes": pipes, "linked": linked, # 154-1 · 줄 누름 → 바로 연결(154-2) diff --git a/B09_Estimation/B09_Estimation_Bill_Items.py b/B09_Estimation/B09_Estimation_Bill_Items.py index 9282a4e92..f9b4fa725 100644 --- a/B09_Estimation/B09_Estimation_Bill_Items.py +++ b/B09_Estimation/B09_Estimation_Bill_Items.py @@ -4,7 +4,7 @@ 토공(1.1~1.6) = B08 토공집계표 줄 짝(`EARTH` — 엑셀 항목 이름 ↔ 토공집계표 구분 · 종류 · 규격) · 단가 = 조합 밖 토공 산근 · 준비공 대가 — 원천 없음 → 빈칸 「연결 없음」(지시) 구조물(2.1~2.3) = B08 구조물 집계표 열(사본 있을 때만 · 143) · - 구조물군 → 공종 짝(`STRUCTURE_SECTION`) + 구조물군 → 공종 짝(`STRUCTURE_SECTION` · 석축 · 옹벽 = 2.3 · 701-1) · 커플링 열은 자재(관급자재대)라 뺌 · 단가 = 대가 호표(145-2 가 `hopyo` 로 줌 · 그전엔 빈칸) 운반(3) = 엑셀 운반공 줄 · 수량 = B08 총괄자재 짝 · 거리 · 단가 = 운반 산근(원천 없음 → 빈칸) 자재대 · 관급자재대 = 화면 짝이 대비표 「별도」 · 「관급」 표시로 짬(사용자 조작 즉시) @@ -45,7 +45,7 @@ EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [ ] #: 구조물 집계표 열 id 머리 → 공종(그 밖 = 2.2 배수공) · 커플링 = 자재라 뺌 -STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3"} +STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3", "pipe:wall|": "2.3"} SKIP_COLUMNS = ("pipe:cp|",) diff --git a/resources/tester/test_120_b08_drain_summary.py b/resources/tester/test_120_b08_drain_summary.py index 870c4e11d..a3effb254 100644 --- a/resources/tester/test_120_b08_drain_summary.py +++ b/resources/tester/test_120_b08_drain_summary.py @@ -245,6 +245,7 @@ def test_차례_계_종류(out): "cross_drain_exposed", "open_ditch", "cross_underdrain", + *summary.WALL_TYPES, # 석축 · 옹벽 길이 열(701-1) } diff --git a/resources/tester/test_701_1_b08_wall_summary.py b/resources/tester/test_701_1_b08_wall_summary.py new file mode 100644 index 000000000..c8723be4b --- /dev/null +++ b/resources/tester/test_701_1_b08_wall_summary.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""701-1 — 석축 · 옹벽 길이 → B08 구조물 집계표 열 · B09 설계내역서 구조물 줄(owners 수량). + +견본: 옹벽 H=2.0 구간 100~112(12 m) · 돌쌓기(찰) H=2.0 구간 196~204(옛 길이 칸 99 안 씀) · +돌쌓기(찰) 높이 빈칸(null · 금액 안 듦 = 값 빈칸) · 큰돌쌓기 사본 없음(안 셈 · 미연결) +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from B08_Quantity import B08_Quantity_Router_Summary as summary +from B09_Estimation.B09_Estimation_Bill_Items import structure_items +from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition + + +def _item(key: str) -> dict: + return {"layer": "system", "owner": None, "key": key, "name": key} + + +STRUCTURES = [ + { + "structure_id": "s-wall", + "type_id": "retaining_wall", + "placement": "interval", + "chainage_m": 106, + "start_m": 100, + "end_m": 112, + "options": {"height_m": 2}, + "summary_item": _item("SS000201"), + }, + { + "structure_id": "s-wet", + "type_id": "masonry_wet", + "placement": "interval", + "chainage_m": 200, + "start_m": 196, + "end_m": 204, + "options": {"height_m": 2.0, "length_m": 99}, + "summary_item": _item("SS000202"), + }, + { + "structure_id": "s-wet-blank", + "type_id": "masonry_wet", + "placement": "interval", + "chainage_m": 300, + "start_m": 295, + "end_m": 305, + "options": {"height_m": None}, + "summary_item": _item("SS000202"), + }, + { + "structure_id": "s-boulder", + "type_id": "boulder_masonry", + "placement": "interval", + "chainage_m": 400, + "start_m": 395, + "end_m": 405, + "options": {"height_m": 3}, + "summary_item": _item("SS000203"), + }, +] + + +@pytest.fixture +def out(tmp_path: Path) -> dict: + root = tmp_path / "proj" + route = root / "B05_Profile/route" + route.mkdir(parents=True) + (route / "structures.json").write_text( + json.dumps({"revision": 1, "structures": STRUCTURES}, ensure_ascii=False), + encoding="utf-8", + ) + names = definition.read()["종류"]["pipe"]["이름"] + linked = {"s-wall": {}, "s-wet": {}, "s-wet-blank": {}} # 큰돌쌓기 = 사본 없음 + return summary.pipe_summary(root, [], names, 6.0, linked=linked) + + +def _col(out: dict, cid: str) -> dict: + return next(c for c in out["columns"] if c["id"] == cid) + + +def test_옹벽_석축_높이마다_길이_열(out): + wall = _col(out, "pipe:wall|retaining_wall|2.0") + assert wall["group"] == ["옹벽", "H=2.0"] and wall["label"] == "m" + assert wall["combos"] == ["system||SS000201"] + assert wall["owners"] == {"s-wall": 12.0} # 구간 시작~종료 + wet = _col(out, "pipe:wall|masonry_wet|2.0") + assert wet["owners"] == {"s-wet": 8.0} # 구간이 옛 길이 칸을 이김 + assert out["total"]["pipe:wall|retaining_wall|2.0"] == 12.0 + + +def test_빈_높이_미입력_열_값_빈칸(out): + blank = _col(out, "pipe:wall|masonry_wet|-") + assert blank["group"] == ["돌쌓기(찰)", "미입력"] + assert blank["owners"] == {} and "pipe:wall|masonry_wet|-" not in out["total"] + + +def test_사본_없는_벽_안_셈_미연결(out): + assert not any("boulder_masonry" in c["id"] for c in out["columns"]) + assert [f["id"] for f in out["unlinked"]] == ["s-boulder"] + assert {"retaining_wall", "masonry_wet", "boulder_masonry"} <= set(out["types"]) + + +def test_B09_구조물_줄_기타구조물_수량(out): + rows = {r["key"]: r for r in structure_items(out)} + wall = rows["st:pipe:wall|retaining_wall|2.0"] + assert (wall["section"], wall["name"], wall["spec"], wall["unit"]) == ( + "2.3", + "옹벽", + "H=2.0", + "m", + ) + assert wall["qty"] == 12.0 and wall["owners"] == {"s-wall": 12.0} + assert rows["st:pipe:wall|masonry_wet|-"]["qty"] is None + + +def test_B09_금액_길이_곱하기_대가(out): + from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation + + price = {"no": "대가1", "key": "blk:w", "labor": 96868, "material": 11309, "expense": 16417} + inputs = {"bill": {"items": structure_items(out)}} + sheets = compute_estimation(inputs, {}, {"s-wall": price}.get) + row = next(r for r in sheets["bill"]["rows"] if r["key"].endswith("#blk:w")) + assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · system||SS000201")