fix(B08·B09): 석축 · 옹벽 길이 → 구조물 집계표 열 · 설계내역서 곱하기(PLAN 701-1)
- B08 구조물 집계표 — 구조물 표 형상 wall(옹벽 · 돌쌓기 · 큰돌쌓기 · 흙막이) 높이마다 길이 m 열 · 빈 높이 = 「미입력」 열 값 빈칸 · 사본 없으면 안 셈(미연결) - 응답 types 에 벽 종류 — 화면 조합 열에서 뺌 - B09 설계내역서 — 벽 열 = 2.3 기타구조물 · owners 수량 × 대가 - test_120 종류 집합 낡은 값 갱신 · 시험 test_701_1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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|",)
|
||||
|
||||
|
||||
|
||||
@@ -245,6 +245,7 @@ def test_차례_계_종류(out):
|
||||
"cross_drain_exposed",
|
||||
"open_ditch",
|
||||
"cross_underdrain",
|
||||
*summary.WALL_TYPES, # 석축 · 옹벽 길이 열(701-1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user