"""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"]] # 고정 열 = 손 값만 · 콘크리트소요 원천 둘은 늘(160-4) assert keys == [ "concrete_machine", "pipe_1000", "mortar_1_3", "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"] == ["야면석|㎥"] last = sheets["material"]["rows"][-1] # 맞춤 없음 = 총괄 끝 줄 그대로(160-3) assert (last["key"], last["group"], last["unit"], last["qty"]) == ( "야면석|㎥", "야면석", "㎥", 15, ) 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": "㎥"}}