"""723-4 B09 공사 사본의 일위대가(SC) 안 줄 수량 = 프로젝트 값 연결. - 723-1 = M02 일위대가(SC) 안 줄 · 자재 수량 저장 안 함(null = 수량 없음 · 1 아님) - 구조물 조합에서 온 SC = 그 사본 면적 알약 값(「참조 id#차례」 · 「참조 id#자재|코드」)으로 굳힘 - 내역에서 바로 쓴 SC(서고 사본) = 빈 줄 `ask` → 이 공사에서 입력(702 덮개 qty) → 금액 - 옛 손(B09 굳힌 사본) = 그대로 · 덮개 열쇠 `C:참조` 그대로(기존 공사 금액 · 덮개 같음) · 알약 값 쓴 블록만 `C:참조|S:사본`(사본마다 값 다름 · 이 공사 입력 덮개와 안 섞임) - 셈 짝(`B09_Estimation_Calc_Labor` .py ↔ .ts) 거울 """ import importlib.util import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402 from B09_Estimation.B09_Estimation_Labor_Source import ASK, Builder # noqa: E402 from M02_MasterTemplete import M02_Labor_Ref as refs # noqa: E402 _spec = importlib.util.spec_from_file_location( "b145", Path(__file__).with_name("test_145_2_b09_labor_calc.py") ) b145 = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(b145) REF = {"층": "project", "주인": "t", "키": "SC-000001"} RID = refs.ref_id(REF) MAT = "MT00001" SHEETS = {"labor": [], "material": [{"key": MAT, "price": 500}], "machine": []} def _fix(name, labor): return {"고정": {"이름": name, "단위": "식", "단가": {"노무비": labor}}} def _doc(hand=None, mat=None): """SC 한 벌 — 고정 줄 둘(노무 1000 · 2000) + 자재 하나 · 손 = hand · mat(None = 수량 없음).""" q = (lambda v: {"수량": {"손": v}}) if hand is not None else (lambda v: {"수량": None}) return { "이름": "시험 SC", "단위": "㎡", "담은로직": [{**_fix("갑", 1000), **q(hand)}, {**_fix("을", 2000), **q(hand)}], "자재": [{"코드": MAT, "수량": {"손": mat} if mat is not None else None}], } def _build(doc, use=({}, "")): build = Builder() whole = {MAT: {"이름": "철선", "규격": "#8", "단위": "kg"}} # 사본 단가 대신(_material 만 읽음) bid = build.precombo(whole, REF, doc, {}, 1, use) block = build.blocks[bid] return {"blocks": [block], "owners": {"pipe@100.0": bid}}, block def test_수량_없는_SC_서고_사본_경고(): daega, block = _build(_doc(), ({}, "L:lib")) assert [ln["qty"] for ln in block["lines"]] == [None, None, None] assert all(ln["ask"] == ASK for ln in block["lines"]) assert block["cover"] == f"C:{RID}" # 공사 안 한 덮개(703-1 지난 공사 가져옴 꼴 그대로) out = compute_labor(daega, SHEETS) got = out["daega_blocks"][0] assert got["blank"] == 3 and got["sum"] == 0 assert [ln["why"] for ln in got["lines"]] == [ASK] * 3 assert sum(g["why"] == ASK for g in out["gaps"]) == 3 assert out["daega"][0]["blank"] == 3 # 내역 · 원가계산서 빈 줄(166-6 · 701-3)로 넘어감 def test_이_공사에서_입력_금액(): daega, block = _build(_doc(), ({}, "L:lib")) cids = [ln["cid"] for ln in block["lines"]] cover = {block["cover"]: {cids[0]: {"qty": 2}, cids[1]: {"qty": 0.5}, cids[2]: {"qty": 3}}} got = compute_labor(daega, SHEETS, None, cover)["daega_blocks"][0] assert [ln["amount"] for ln in got["lines"]] == [2000.0, 1000.0, 1500.0] assert got["blank"] == 0 and got["sum"] == 4500.0 assert not any("why" in ln for ln in got["lines"]) # 입력 뒤 경고 글 없음 def test_구조물_조합_알약_값_굳힘(): given = {f"{RID}#0": 1.5, f"{RID}#1": 2, f"{RID}#자재|{MAT}": 4} daega, block = _build(_doc(), (given, "S:pipe@100.0")) assert [ln["qty"] for ln in block["lines"]] == [1.5, 2.0, 4.0] assert not any("ask" in ln for ln in block["lines"]) assert block["cover"] == f"C:{RID}|S:pipe@100.0" got = compute_labor(daega, SHEETS)["daega_blocks"][0] assert got["sum"] == 1500.0 + 4000.0 + 2000.0 and got["blank"] == 0 def test_옛_손_그대로(): daega, block = _build(_doc(hand=3, mat=2), ({}, "S:pipe@100.0")) assert [ln["qty"] for ln in block["lines"]] == [3.0, 3.0, 2.0] assert block["cover"] == f"C:{RID}" # 옛 덮개 열쇠 그대로 got = compute_labor(daega, SHEETS)["daega_blocks"][0] assert got["sum"] == 3000.0 + 6000.0 + 1000.0 def test_거울_파이썬_TS(): daega, block = _build(_doc(), ({}, "L:lib")) cid = block["lines"][0]["cid"] for cover in ({}, {block["cover"]: {cid: {"qty": 2}}}): py = json.loads(json.dumps(compute_labor(daega, SHEETS, {}, cover), ensure_ascii=False)) ts = b145._ts(daega, SHEETS, {}, cover) assert b145._same(py, ts["out"]) def _structure(tmp_path, area): """구조물 사본(참조 SC 한 줄 · 굳힌 SC = 수량 없음) + 면적.json(SC 안 줄 알약 연결 · 723-1).""" from M02_MasterTemplete import M02_Item_Snapshot as snapshots root = b145._project(tmp_path / "proj", [(100, "SS000901")]) sc = {**_doc(), "자재": []} b145._snap(root, "pipe@100.0", "SS000901", [{"참조": REF, "수량": {"손": 1}}], {}, {RID: sc}) folder = snapshots.structures_dir(root) / snapshots.folder_name("pipe@100.0") if area is not None: (folder / "면적.json").write_text(json.dumps(area, ensure_ascii=False), encoding="utf-8") return root, folder def _sc_block(root): from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs got = daega_inputs(root) combo = next(b for b in got["blocks"] if b["id"] == got["owners"]["pipe@100.0"]) return got, next(b for b in got["blocks"] if b["id"] == combo["lines"][0]["key"]) def test_구조물_사본_면적_알약_값_굳힘_설계_연동(tmp_path): links = [{"칸": "A1", "키": f"{RID}#0"}, {"칸": "A2", "키": f"{RID}#1"}] area = {"시트": [{"id": "s", "칸": {"A1": {"값": 1.5}, "A2": {"값": 2}}, "연결": links}]} root, folder = _structure(tmp_path, area) got, block = _sc_block(root) assert [ln["qty"] for ln in block["lines"]] == [1.5, 2.0] assert block["cover"] == f"C:{RID}|S:{folder.name}" and not any( "ask" in ln for ln in block["lines"] ) out = compute_labor(got, SHEETS) by_id = {b["id"]: b for b in out["daega_blocks"]} assert by_id[block["id"]]["sum"] == 1500.0 + 4000.0 assert by_id[got["owners"]["pipe@100.0"]]["sum"] == 5500.0 # 조합 = SC 1 단위 × 1 # 설계 연동 — 면적 값이 바뀌면 다시 셈(사본 면적.json) area["시트"][0]["칸"]["A1"]["값"] = 3 (folder / "면적.json").write_text(json.dumps(area, ensure_ascii=False), encoding="utf-8") assert [ln["qty"] for ln in _sc_block(root)[1]["lines"]] == [3.0, 2.0] def test_구조물_사본_알약_안_이으면_빈_줄(tmp_path): root, folder = _structure(tmp_path, None) got, block = _sc_block(root) assert [ln["qty"] for ln in block["lines"]] == [None, None] assert block["cover"] == f"C:{RID}" # 알약 안 씀 = 서고 SC 와 같은 「이 공사 입력」 덮개 out = compute_labor(got, SHEETS) owner = next(r for r in out["daega"] if r["key"] == got["owners"]["pipe@100.0"]) assert owner["blank"] >= 1 # 부른 SC 빈 줄 = 조합도 빈 줄(166-6)