Files
Aislo/resources/tester/test_143_4_b08_snapshot_materials.py
T
eomsangdonandClaude Opus 5.5 8d4f4e8718 feat(B08): 사본 일위대가 자재 → 자재수량집계표 단위수량 재연결(143-4)
- 서버 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
2026-10-09 15:25:05 +09:00

118 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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": "㎥"}}