"""일위대가 로직 자재(PLAN 103-1) — 담은 로직 맨 위 층 재료 줄 · 고름 · 합 이중 계산 없음. - 로직 줄 계산값 `재료` = 호표 재료 줄(차례 · 품목 · 단가 · 금액 · 걸림) · 재료 없는 로직은 빈 목록 - 고름 `{호표 차례: MT키}` → 그 줄 품목 · 단가 · 로직 재료비가 바뀜(엔진 `choosing`) · 고름 없으면 지금 값 - 합 · 알약에 로직 자재 금액을 다시 안 더함 · 저장 뒤 고름 남음 · 틀린 고름 400 - 사용자 자재와 같은 품목 = [저장] 400 · 입력에 걸린 고름은 입력이 바뀌면 풂(화면 짝 · Node) - 프로젝트 사본 씨앗 = 문서 고름 """ import json import os import subprocess import pytest from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Item_Snapshot as snap from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store from resources.tester.test_m02_structure import write_sample_table HERE = os.path.dirname(os.path.abspath(__file__)) NET = "GC000337" # 낙석방지망 — 재료 줄 3 · 4 · 5(철망 MT006697 · 결속선 MT000194 · 에폭시) BORING = "GC000431" # 보링 천공 — 재료 줄 4~8 조건에 {규격} · 9 = MT000008(입력 안 걸림) BARE = "GC000994" # 재료 줄 없는 돈 로직 WIRE = "MT000194" NET_ITEM = "MT006697" def _whole(): return m01.loaded()[1] def _doc(**extra): return {"열": "rv", "담은로직": [], "자재": [], **extra} def test_로직_자재는_맨_위_층_재료_줄_재료_없는_로직은_빈_목록(): got = labor.run_line(_whole(), NET, {}) assert [(m["차례"], m["품목"]["키"]) for m in got["재료"]] == [ (3, NET_ITEM), (4, WIRE), (5, "MT007705"), ] assert all(m["고름"] is False and m["걸림"] == [] for m in got["재료"]) assert labor.run_line(_whole(), BARE, {})["재료"] == [] def test_조건에_든_입력은_걸림(): mats = {m["차례"]: m for m in labor.run_line(_whole(), BORING, {})["재료"]} assert all(mats[n]["걸림"] == ["규격"] for n in range(4, 9)) assert mats[9]["걸림"] == [] def test_고름이_단가와_로직_재료비를_바꿈_고름_없으면_지금_값(): plain = labor.run_line(_whole(), NET, {}) again = labor.run_line(_whole(), NET, {}, None, None) assert again["기준"] == plain["기준"] # 고름 없음 = 지금 길 그대로(회귀) picked = labor.run_line(_whole(), NET, {}, None, {"3": WIRE}) row = picked["재료"][0] assert (row["품목"]["키"], row["고름"]) == (WIRE, True) assert row["단가"] == picked["재료"][1]["단가"] != plain["재료"][0]["단가"] gap = (plain["재료"][0]["단가"] - row["단가"]) * row["수량"] assert plain["기준"]["재료비"] - picked["기준"]["재료비"] == pytest.approx(gap) assert picked["기준"]["노무비"] == plain["기준"]["노무비"] def test_합_알약에_로직_자재_금액_다시_안_더함(): doc = labor.clean(_doc(담은로직=[{"로직": NET, "수량": {"손": 2}}])) got = labor.calc(doc, {}) line = got["줄"][0] assert line["재료"] and got["자재"] == [] assert got["기준합"]["계"] == pytest.approx(line["기준"]["계"]) assert got["반영합"]["계"] == pytest.approx(line["기준"]["계"] * 2) assert not [p for p in labor.pills(doc) if p["키"].startswith(labor.MATERIAL_HEAD)] def test_고름_정리_틀리면_400(): got = labor.clean(_doc(담은로직=[{"로직": NET, "고름": {3: WIRE}}, {"로직": BARE, "고름": {}}])) assert got["담은로직"][0]["고름"] == {"3": WIRE} and "고름" not in got["담은로직"][1] for bad in ({"0": WIRE}, {"3": "MT999999"}, {"3": "LB000001"}, {"가": WIRE}, ["3"]): with pytest.raises(store.StoreError) as e: labor.clean(_doc(담은로직=[{"로직": NET, "고름": bad}])) assert e.value.status == 400 def test_사용자_자재가_로직_자재와_같으면_저장_400(): with pytest.raises(store.StoreError) as e: labor.server(_doc(담은로직=[{"로직": NET}], 자재=[{"코드": WIRE}])) assert e.value.status == 400 and WIRE in str(e.value.detail) # 고른 품목도 같음 — 결속선 줄을 다른 품목으로 바꾸면 사용자 자재로 담을 수 있음 with pytest.raises(store.StoreError): labor.server( _doc(담은로직=[{"로직": NET, "고름": {"3": "MT000008"}}], 자재=[{"코드": "MT000008"}]) ) ok = labor.server( _doc(담은로직=[{"로직": NET, "고름": {"4": NET_ITEM}}], 자재=[{"코드": WIRE}]) ) assert ok["자재"][0]["코드"] == WIRE @pytest.fixture() def system(tmp_path, monkeypatch): write_sample_table(tmp_path) monkeypatch.setattr(store, "FOLDER", tmp_path) store.create_structure("rv") return tmp_path def test_저장_뒤_고름_남고_계산값에_반영(system): have = store.read("labor", "rv") sent = {**have["문서"], "담은로직": [{"로직": NET, "고름": {"3": WIRE}, "수량": None}]} store.write("labor", "rv", have["판"], sent) doc = store.read("labor", "rv")["문서"] assert doc["담은로직"][0]["고름"] == {"3": WIRE} assert doc["계산값"]["줄"][0]["재료"][0]["품목"]["키"] == WIRE def test_단가승격도_고름을_따름_없으면_지금_값(): expand = refs.Expand(_whole()) plain = expand._promote({"담은로직": [{"로직": NET}], "배": 1}) picked = expand._promote({"담은로직": [{"로직": NET, "고름": {"3": WIRE}}], "배": 1}) assert plain["기준"] == labor.run_line(_whole(), NET, {})["기준"] assert picked["기준"]["재료비"] < plain["기준"]["재료비"] def test_사본_씨앗은_문서_고름(): labor_file = { "줄": { "담은로직": [ {"로직": NET, "고름": {"3": WIRE}}, {"로직": BARE}, {"참조": {"층": "system", "주인": None, "키": "SP-000001"}, "로직": ""}, ] } } assert snap.seed_picks(labor_file) == {NET: {"3": WIRE}} assert snap.seed_picks(None) == {} def _release(row: dict, before: dict, mats: list) -> dict: proc = subprocess.run( ["node", os.path.join(HERE, "helper_m02_labor_logic_mats.cjs")], input=json.dumps({"row": row, "before": before, "mats": mats}, ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(proc.stdout) def test_입력이_바뀌면_걸린_고름만_풂_화면_짝(): mats = labor._json(labor.run_line(_whole(), BORING, {})["재료"]) picks = {"4": "MT007895", "9": WIRE} row = {"로직": BORING, "입력값": {"규격": "NX"}, "고름": picks} assert _release(row, {"규격": "NX"}, mats)["고름"] == picks # 그대로 assert _release(row, {"규격": "BX"}, mats)["고름"] == {"9": WIRE} # 규격에 걸린 줄만 row = {**row, "고름": {"4": "MT007895"}} assert "고름" not in _release(row, {}, mats) # 다 풀리면 칸째 지움 def _src(name): with open(os.path.join(HERE, "..", "..", "M02_MasterTemplete", name), encoding="utf-8") as f: return f.read() def test_로직_자재_줄_누름은_그_로직_옵션_모달_사용자_줄은_그대로(): """148-1 — 고칠 때 · 읽기만 둘 다 `open: openRow` · 바꾸기 단추 칸은 swap 있을 때만 · 사용자 줄 = openMat.""" detail = _src("M02_MasterTemplete_Labor_Detail.ts") assert "open: openRow,\n swap: swapMat," in detail assert ": { open: openRow }," in detail assert "{ open: openMat, remove: removeMat, mark: markMat } : { open: openMat }" in detail mats = _src("M02_MasterTemplete_Labor_LogicMats.ts") assert "const tools = swap" in mats assert 'tr.addEventListener("click", () => on.open(r))' in mats assert '${on ? " is-open" : ""}' in mats