- 서버 snapshot_units: 칸마다 그 칸을 낸 조합의 연결된 사본 셈(자재 MT 코드 · 로직 재료 이름|단위) × 칸 값 ÷ 열 계 - 짝 계산(py · ts): 자재 열 = 사본 자재 + 관경 · 2공구 고정 열은 손 값 있을 때만 · 손 값 이김 · 관매설 → 파형강관 1 그대로 - 콘크리트소요 · 총괄 끌어옴 = 이름 · 규격 맞춤 · 안 맞는 사본 자재는 알림 「맞춤 없음」 - 구조물 집계표 결과에 칸 출처(sources) · 채집석 공제는 그대로 남김 - 시험: test_143_4 새로(서버 셈 · 로직 재료 · 거울) · test_121 기대 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
118 lines
5.8 KiB
Python
118 lines
5.8 KiB
Python
"""143-4 사본 일위대가 자재 → B08 자재수량집계표 단위수량 재연결.
|
||
|
||
- 서버 `snapshot_units` — 칸(측점 · 시설)마다 그 칸을 낸 조합의 연결된 사본 셈(자재 · 로직 재료) ×
|
||
칸 값 ÷ 열 계 · 사본 없거나 줄 수량 빈칸이면 빈칸(안 담음)
|
||
- 짝 계산 `compute` — 열 = 사본 자재 + 관경 · 2공구 고정 열은 사용자 값 있을 때만 · 손 값이 이김 ·
|
||
관매설 → 파형강관 1 그대로 · 콘크리트소요 · 총괄 끌어옴 = 이름 · 규격 맞춤 · 안 맞으면 `unmatched`
|
||
- 거울 = TS 짝(`B08_Quantity_Material_Calc.ts`)이 같은 답
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from B08_Quantity import B08_Quantity_Router_Material as router
|
||
from B08_Quantity.B08_Quantity_Material_Calc import clean_overrides, compute
|
||
from M02_MasterTemplete import M02_Item_Copy as copies
|
||
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
||
from resources.tester.test_121_b08_material_sheets import _run_ts
|
||
from resources.tester.test_item_copy_complete import ( # noqa: F401 — 고정물
|
||
FOLDER,
|
||
PIPE,
|
||
_pipe,
|
||
_save,
|
||
master_copy,
|
||
world,
|
||
)
|
||
|
||
COMBO = "system||SS000011"
|
||
LEN = "pipe:len|1000"
|
||
|
||
SUMMARY = {
|
||
"columns": [
|
||
{"id": LEN, "group": ["관 공", "Φ1000m/m", "관매설"], "label": "m"},
|
||
{"id": "pipe:revet|돌쌓기(찰)|2.0", "group": ["흙막이", "찰쌓기", "H=2.0"], "label": "m"},
|
||
{"id": "pipe:chute|메붙임", "group": ["돌붙임", "메붙임"], "label": "㎡"},
|
||
],
|
||
"total": {LEN: 20, "pipe:revet|돌쌓기(찰)|2.0": 10, "pipe:chute|메붙임": 5},
|
||
"snapshot": {
|
||
"materials": [
|
||
{"key": "MT000001", "name": "레미콘", "spec": "25-18-80", "unit": "㎥"},
|
||
{"key": "고임돌|㎥", "name": "고임돌", "spec": "채집", "unit": "㎥"},
|
||
{"key": "야면석|㎥", "name": "야면석", "spec": "", "unit": "㎥"},
|
||
],
|
||
"units": {
|
||
"pipe:revet|돌쌓기(찰)|2.0": {"MT000001": 0.31, "고임돌|㎥": 0.2, "야면석|㎥": 1.5},
|
||
"pipe:chute|메붙임": {"고임돌|㎥": 0.12},
|
||
},
|
||
},
|
||
}
|
||
|
||
|
||
def test_사본_자재가_열과_단위수량_기본():
|
||
over = clean_overrides({"units": {"pipe:chute|메붙임": {"고임돌|㎥": 0.5, "sod": 1}}})
|
||
sheets = compute(SUMMARY, over)
|
||
keys = [m["key"] for m in sheets["quantity"]["materials"]]
|
||
assert keys == ["pipe_1000", "sod", "MT000001", "고임돌|㎥", "야면석|㎥"] # 고정 열 = 손 값만
|
||
rows = {row["id"]: row for row in sheets["quantity"]["rows"]}
|
||
revet = rows["pipe:revet|돌쌓기(찰)|2.0"]
|
||
assert revet["units"]["MT000001"] == 0.31 and revet["amounts"]["MT000001"] == 3.1
|
||
assert revet["units"]["pipe_1000"] is None # 사본에 없는 자재 = 빈칸
|
||
assert rows["pipe:chute|메붙임"]["units"]["고임돌|㎥"] == 0.5 # 손 값이 이김
|
||
assert rows[LEN]["units"]["pipe_1000"] == 1 # 관매설 → 파형강관 1 그대로
|
||
# 끌어옴 — 레미콘 25-18-80 · 고임돌(채집) = 이름 · 규격 맞춤 · 야면석은 맞춤 없음
|
||
assert sheets["quantity"]["unmatched"] == ["야면석|㎥"]
|
||
total = {row["key"]: row for row in sheets["material"]["rows"]}
|
||
assert total["remicon_25_18_80"]["qty"] == 3.1
|
||
assert total["pipe_1000"]["qty"] == 20
|
||
concrete = {row["key"]: row for row in sheets["concrete"]["rows"]}
|
||
assert concrete["c40"]["qty"] is None # 콘크리트(기계) 사본 없음 = 빈칸
|
||
|
||
|
||
def test_거울_TS와_파이썬이_같다():
|
||
over = clean_overrides({"units": {"pipe:chute|메붙임": {"고임돌|㎥": 0.5, "sod": 1}}})
|
||
assert _run_ts(SUMMARY, over) == compute(SUMMARY, over)
|
||
bare = {k: v for k, v in SUMMARY.items() if k != "snapshot"}
|
||
assert _run_ts(bare, clean_overrides({})) == compute(bare, clean_overrides({}))
|
||
|
||
|
||
def test_서버_사본_셈_자재_x_칸값_나누기_열계(world): # noqa: F811
|
||
"""일위대가 자재(MT 코드 · 손 수량 2) = 단위수량 2 · 사본 없는 시설 칸은 안 셈."""
|
||
root = world["root"]
|
||
_save(root, [_pipe(), {**_pipe(None), "chainage_m": 80.0}])
|
||
folder = root / "templates/structures" / FOLDER
|
||
labor = json.loads((folder / "일위대가.json").read_text("utf-8"))
|
||
labor["줄"] = {
|
||
"종류": "일위대가",
|
||
"열": "x",
|
||
"단위": "m",
|
||
"담은로직": [{"로직": "GF000176"}], # 줄 수량 빈칸 — 로직 재료 안 셈
|
||
"자재": [{"코드": "MT003810", "수량": {"손": 2}}],
|
||
}
|
||
(folder / "일위대가.json").write_text(json.dumps(labor, ensure_ascii=False), "utf-8")
|
||
summary = {
|
||
"linked": snap.linked_snapshots(root),
|
||
"sources": [[LEN, 12.0, FOLDER, [COMBO]], [LEN, 8.0, "pipe@80.0", [COMBO]]],
|
||
"total": {LEN: 20.0},
|
||
}
|
||
got = router.snapshot_units(root, summary)
|
||
assert got["units"] == {LEN: {"MT003810": 1.2}} # 12 × 2 ÷ 20 · 80 m 관은 사본 없음
|
||
assert [m["key"] for m in got["materials"]] == ["MT003810"]
|
||
assert got["materials"][0]["name"] != "MT003810" # 이름 = 사본 단가 묶음 품목 이름
|
||
|
||
|
||
def test_로직_재료는_재료_x_줄수량_나누기_배(monkeypatch):
|
||
fake = {
|
||
"줄": [
|
||
{"수량": 4, "배": 10, "재료": [{"이름": "돌", "단위": "㎥", "수량": 5}]},
|
||
{"수량": None, "배": 1, "재료": [{"이름": "돌", "단위": "㎥", "수량": 9}]}, # 빈칸
|
||
{"수량": 1, "배": 1, "재료": [{"이름": "파형강관", "단위": "m", "수량": 1}]}, # 관 몫
|
||
],
|
||
"자재": [],
|
||
}
|
||
monkeypatch.setattr(copies, "calc", lambda root, folder: fake)
|
||
monkeypatch.setattr(copies, "folder_of", lambda root, folder: copies.Path("없음"))
|
||
meta: dict = {}
|
||
assert router._folder_units("r", "f", meta) == {"돌|㎥": 2.0}
|
||
assert meta == {"돌|㎥": {"key": "돌|㎥", "name": "돌", "spec": "", "unit": "㎥"}}
|