"""M02 일위대가 사전조합 자재목록(PLAN 175) — 구조물 일위대가와 같은 자재목록. 175-1 화면 · 저장 — 서고 모드 자재목록 · 로직 자재 세 칸 막힘 풂 · 저장 `_combo_lines` 가 고름 · 자재표시 남김 · 문서 `자재`(일위대가 자재 검사 · 로직 자재와 같은 품목 400). 175-2 셈 — 담은 자재(손 · 빈칸 = 안 셈)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄 재료(`_ref_mats`) · B09 겹 대가 자재 줄 · 프로젝트 사본 단가 묶음(`frozen_materials`)에 들어감. """ import json import os from decimal import Decimal import pytest from M01_MasterData import M01_MasterData_Store as m01 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_labor_ref import world # noqa: F401 — 임시 층 fixture HERE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete") NET = "GC000337" # 낙석방지망 — 재료 줄 3 · 4 · 5(철망 · 결속선 · 에폭시) NET_ITEM = "MT006697" WIRE = "MT000194" MAT = "MT000008" # 자재품목 · 물가정보 83000 STONE = "GF000219" # 원/㎡ SYS = {"층": "system", "주인": None} def _save(folder, doc): with store.at(folder): key = refs.create("precombo", {"이름": "자재 조합", **doc})["이름"] return key, json.loads((folder / "precombo" / f"{key}.json").read_bytes()) def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # noqa: F811 detail = open(os.path.join(SRC, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read() assert 'section(tc("Labor_Mats"), opt.card ? null : addMat, matHost),' in detail assert 'lib ? [] : [section(tc("Labor_Mats")' not in detail assert "mark: markLogicMat," in detail and "lib ? undefined" not in detail assert "delete out.자재" not in detail and "delete out.열" in detail def test_저장_왕복_고름_자재표시_자재_남음(world): # noqa: F811 line = {"로직": NET, "고름": {"4": NET_ITEM}, "자재표시": {"3": {"자재구분": "관급"}}} mats = [ {"코드": MAT, "수량": {"손": 2}, "운반비": "미포함", "이름": "틀린 이름"}, {"코드": WIRE, "수량": None}, ] _, got = _save(world["system"], {"담은로직": [line], "자재": mats}) one = got["담은로직"][0] assert one["고름"] == {"4": NET_ITEM} and one["자재표시"] == {"3": {"자재구분": "관급"}} assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, {"손": 2}), (WIRE, None)] assert got["자재"][0]["운반비"] == "미포함" and got["자재"][0]["이름"] != "틀린 이름" # 자재 없음 = 빈 목록(옛 문서 꼴 그대로 읽힘) _, empty = _save(world["system"], {"담은로직": [{"로직": NET}]}) assert empty["자재"] == [] def test_로직_자재와_같은_품목_400_없는_자재_400(world): # noqa: F811 with pytest.raises(store.StoreError) as e: _save(world["system"], {"담은로직": [{"로직": NET}], "자재": [{"코드": WIRE}]}) assert e.value.status == 400 and WIRE in str(e.value.detail) with pytest.raises(store.StoreError) as e: _save(world["system"], {"담은로직": [], "자재": [{"코드": "MT999999"}]}) assert e.value.status == 400 # ── 175-2 셈 ────────────────────────────────────────────────────────── def _price(code): whole = m01.loaded()[1] return Decimal(str(labor.material_price(whole, code, None)["기준단가"])) def _world_combo(world): # noqa: F811 """STONE 1 + MAT 손 2 + WIRE 빈칸(안 셈) · NET 고름 4 = NET_ITEM — (키, 손셈 1 단위 값).""" lines = [ {"로직": STONE, "수량": {"손": 1}}, {"로직": NET, "수량": {"손": 0}, "고름": {"4": NET_ITEM}}, ] # MAT = 직접계산(222-1 · 기본 별도계상이면 셈에서 빠짐) mats = [{"코드": MAT, "수량": {"손": 2}, "별도계상": "직접계산"}, {"코드": WIRE, "수량": None}] with store.at(world["system"]): key = refs.create("precombo", {"이름": "자재 조합", "담은로직": lines, "자재": mats})[ "이름" ] stone = Decimal(str(labor.run_line(m01.loaded()[1], STONE, {})["기준"]["계"])) return key, stone + _price(MAT) * 2 def test_사전조합_값에_자재_금액_빈칸은_안_셈_안_줄에_자재(world): # noqa: F811 key, want = _world_combo(world) with store.at(world["system"]): doc = labor.clean( {"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]} ) one = labor.calc(doc, {})["줄"][0] assert one["기준"]["계"] == pytest.approx(float(want)) assert one["반영"]["계"] == pytest.approx(float(want * 3)) mats = [x for x in one["안"] if x.get("자재")] assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE 빈칸 = 안 셈 assert mats[0]["기준"]["재료비"] == pytest.approx(float(_price(MAT))) # 목록 기준금액(51-4)도 같은 펼침 rows = [{"종류": "precombo", "이름": key}] refs.summaries(world["system"], rows) assert rows[0]["기준금액"] == pytest.approx(float(want)) def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F811 from B08_Quantity.B08_Quantity_Router_Material import _ref_mats key, _ = _world_combo(world) with store.at(world["system"]): outer = refs.create( "precombo", {"이름": "겹", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]}, )["이름"] got = _ref_mats(m01.loaded()[1], refs.read_ref, {**SYS, "키": outer}) mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")] assert mats == [(MAT, pytest.approx(6.0))] # 2 × 겹 손 3 · WIRE 빈칸 없음 def test_B09_겹_대가에_자재_줄(world): # noqa: F811 from B09_Estimation.B09_Estimation_Labor_Source import Builder key, _ = _world_combo(world) ref = {**SYS, "키": key} build = Builder() bid = build.precombo(m01.loaded()[1], ref, refs.read_ref(ref), {}) mats = [x for x in build.blocks[bid]["lines"] if x["kind"] == "material"] assert [(x["key"], x["qty"]) for x in mats] == [(MAT, 2.0), (WIRE, None)] assert mats[0]["name"] and mats[0]["unit"] def test_사본_단가_묶음에_사전조합_자재_고름_사본_값_같음(world): # noqa: F811 from M02_MasterTemplete import M02_Item_Prices as prices from M02_MasterTemplete import M02_Item_Source as source key, want = _world_combo(world) doc = {"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 1}}], "자재": []} with store.at(world["system"]): rows = source.labor_doc_rows(labor.clean(doc)) mats = refs.frozen_materials(rows["참조사본"]) assert {MAT, WIRE, NET_ITEM} <= set(mats) assert "refs.frozen_materials(" in open(source.__file__, encoding="utf-8").read() bundle = prices.to_json(prices.collect(refs.frozen_logics(rows["참조사본"]), mats)) (world["system"] / "precombo" / f"{key}.json").unlink() # 원본이 사라져도 굳힌 사본 got = prices.calc(rows, bundle, {}, {}, None)["줄"][0] assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9)