"""702-2 · 702-4 B09 일위대가 고친 칸 덮개 — 사용자 값 `daega_cover` {블록 cover: {줄 cid: {qty · labor · material · expense}}}. - 덮개 열쇠 = 블록 `cover` · 줄 `cid`(`B09_Estimation_Labor_Source` · 내용 해시 밖) - 셈 = `B09_Estimation_Calc_Labor` 파이썬 · TS 짝(거울) — 고친 칸만 셈 값 대신 · `base` = 품셈 값 - 노임 새 판(바탕 단가 바뀜) · 사본 다시 만듦([재적용]) 뒤에도 덮개 칸 그대로 · 안 고친 칸만 새 값 - 짝 잃은 덮개 = `daega_orphans`(지우지 않음) · 저장 다듬기 = `B09_Estimation_Store.clean_cover` 견본 = `test_145_2_b09_labor_calc` 엑셀 블록 · 견본 사본. """ import copy import importlib.util import json import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B09_Estimation import B09_Estimation_Store as store # noqa: E402 from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor, daega_price # noqa: E402 from B09_Estimation.B09_Estimation_Labor_Source import cover_count, daega_inputs # noqa: E402 from M02_MasterTemplete import M02_Item_Snapshot as snapshots # 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) def _covered_excel(): """엑셀 블록 + cover · cid(사본 길이 붙이는 꼴).""" daega = copy.deepcopy(b145.EXCEL) for b in daega["blocks"]: b["cover"] = f"T:{b['id']}" for n, ln in enumerate(b["lines"]): ln["cid"] = f"{ln['kind']}:{ln.get('key') or ln['name']}#{n}" return daega #: DG9 찰붙임 석공 품 0.1 → 0.12 · DG2 % 2 → 3 · DG14 산근 SG10 노무 단가 손 · 짝 잃은 둘 COVER = { "T:DG9": {"labor:L00005#0": {"qty": 0.12}}, "T:DG2": {"pct:공구손료#3": {"qty": 3}}, "T:DG14": {"sangeun:SG10#2": {"labor": 20000}}, "T:없는블록": {"labor:L00017#0": {"qty": 1}}, "T:DG1": {"labor:없는줄#9": {"qty": 1}}, } def _line(out, bid, n): return next(b for b in out["daega_blocks"] if b["id"] == bid)["lines"][n] def test_고친_칸만_셈값_대신(): daega = _covered_excel() plain = compute_labor(daega, b145.SHEETS, b145.SANGEUN) out = compute_labor(daega, b145.SHEETS, b145.SANGEUN, COVER) stone = _line(out, "DG9", 0) assert stone["qty"] == 0.12 and stone["covered"] == ["qty"] assert stone["base"] == {"qty": 0.1, "labor": 258935, "material": 0, "expense": 0} assert stone["amounts"]["labor"] == 31072.2 # ROUNDDOWN(258,935 × 0.12, 1) assert "covered" not in _line(out, "DG9", 1) # 안 고친 줄 = 덧칸 없음 # 부른 블록이 바뀌면 부르는 블록 금액도(DG14 ← DG9) · 내역 단가도 nine = next(b for b in out["daega"] if b["key"] == "DG9") assert nine["labor"] == 42556 + 31072 - 25893 # 석공 줄만 바뀜(ROUNDDOWN 0) assert daega_price(out, "pipe@100.0")["sum"] != daega_price(plain, "pipe@100.0")["sum"] pct = _line(out, "DG2", 4) # % 줄 = 끝에 셈(차례 4) assert pct["qty"] == 3 and pct["base"] == {"qty": 2} and pct["covered"] == ["qty"] sg = _line(out, "DG14", 2) assert sg["prices"]["labor"] == 20000 and sg["base"]["labor"] == 18084 assert sg["covered"] == ["labor"] and sg["prices"]["material"] == 2498 assert out["daega_orphans"] == [ {"cover": "T:없는블록", "cid": "labor:L00017#0"}, {"cover": "T:DG1", "cid": "labor:없는줄#9"}, ] assert plain["daega_orphans"] == [] def test_덮개가_빈칸을_채움(): daega = _covered_excel() sheets = {**b145.SHEETS, "labor": b145.SHEETS["labor"][:2]} # 석공 노임 없음 bare = compute_labor(daega, sheets, b145.SANGEUN) assert _line(bare, "DG9", 0)["amount"] is None out = compute_labor(daega, sheets, b145.SANGEUN, {"T:DG9": {"labor:L00005#0": {"labor": 1e5}}}) assert _line(out, "DG9", 0)["amount"] == 10000.0 assert not [g for g in out["gaps"] if "찰붙임" in g["name"]] @pytest.mark.parametrize("case", ["cover", "none"]) def test_거울_파이썬_TS(case): daega = _covered_excel() cover = COVER if case == "cover" else {} py = json.loads(json.dumps(compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover))) ts = b145._ts(daega, b145.SHEETS, b145.SANGEUN, cover) assert b145._same(py, ts["out"]) assert b145._same(daega_price(py, "pipe@100.0"), ts["price"]) def test_노임_새_판_덮개_칸_그대로(): """702-4 — 바탕 단가가 새 판으로 바뀌어도 고친 칸 = 그대로 · 안 고친 칸만 새 값.""" daega = _covered_excel() cover = {"T:DG9": {"labor:L00005#0": {"qty": 0.12}, "labor:L00017#1": {"labor": 170000}}} old = compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover) new_sheets = { **b145.SHEETS, "labor": [{**r, "price": round(r["price"] * 1.05)} for r in b145.SHEETS["labor"]], } new = compute_labor(daega, new_sheets, b145.SANGEUN, cover) a, b = _line(old, "DG9", 0), _line(new, "DG9", 0) assert a["qty"] == b["qty"] == 0.12 # 고친 품 그대로 assert b["prices"]["labor"] == round(258935 * 1.05) != a["prices"]["labor"] # 안 고친 단가 = 새 assert b["base"]["labor"] == b["prices"]["labor"] # 품셈 값 곁 = 새 판 값 c, d = _line(old, "DG9", 1), _line(new, "DG9", 1) assert c["prices"]["labor"] == d["prices"]["labor"] == 170000 # 고친 단가 그대로 assert d["qty"] == 0.03 and d["base"]["labor"] == round(165545 * 1.05) assert new["daega_orphans"] == [] # ── 사본 → 덮개 열쇠 ─────────────────────────────────────────────────────── @pytest.fixture() def project(tmp_path) -> Path: root = b145._project( tmp_path / "proj", [(100, "SS000901"), (200, "SS000901"), (400, "SS000902")] ) b145._snap(root, "pipe@100.0", "SS000901", b145.LINES, b145._values(b145.STONE)) b145._snap(root, "pipe@200.0", "SS000901", b145.LINES, b145._values(b145.STONE)) promoted = [ {"로직": "GF000219", "승격": True, "수량": {"손": 1}}, {"로직": "GF000119", "승격": True, "수량": {"손": 1}}, {"참조": {"층": "system", "주인": None, "키": "SC-T"}, "수량": {"손": 4}}, ] b145._snap(root, "pipe@400.0", "SS000902", promoted, b145._values(b145.STONE), b145.SC) return root def test_사본_덮개_열쇠(project): got = daega_inputs(project) blocks = {b["id"]: b for b in got["blocks"]} one = blocks[got["owners"]["pipe@100.0"]] assert one is blocks[got["owners"]["pipe@200.0"]] # 같은 꼴 = 한 호표 · 처음 본 사본 열쇠 assert one["cover"] == f"S:{snapshots.folder_name('pipe@100.0')}" assert [ln["cid"] for ln in one["lines"]] == [ "sangeun:SG:GF000219#0", "sangeun:SG:GF000119#0", "sangeun:SG:GF000143#0", ] four = blocks[got["owners"]["pipe@400.0"]] assert [ln["cid"] for ln in four["lines"]] == [ "daega:GF000219#0", "daega:GF000119#0", "daega:참조|system||SC-T#0", ] stone = blocks[four["lines"][0]["key"]] assert stone["cover"].startswith("P:") and [ln["cid"] for ln in stone["lines"]] == [ "labor:LB000033#0", "labor:LB000002#0", ] assert blocks[four["lines"][2]["key"]]["cover"] == "C:참조|system||SC-T" def test_재적용_뒤_덮개_남음(project): """702-4 — 사본을 다시 만들어(재적용 꼴 · 수량 · 줄 차례 바뀜 · 줄 빠짐) 블록 id 가 바뀌어도 cover · cid 가 같아 고친 칸이 맞는 줄에 붙음 · 사라진 줄 덮개 = 짝 잃음(남김).""" before = daega_inputs(project) owner = before["owners"]["pipe@100.0"] first = next(b for b in before["blocks"] if b["id"] == owner) cover = { first["cover"]: { "sangeun:SG:GF000119#0": {"qty": 0.7}, "sangeun:SG:GF000143#0": {"labor": 1000}, } } folder = snapshots.structures_dir(project) / snapshots.folder_name("pipe@100.0") doc = json.loads((folder / "일위대가.json").read_text(encoding="utf-8")) lines = doc["줄"]["담은로직"] lines[0]["수량"] = {"손": 5} # 설계 바뀜(수량) doc["줄"]["담은로직"] = [lines[1], lines[0]] # 차례 바뀜 · GF000143 빠짐 (folder / "일위대가.json").write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8") after = daega_inputs(project) block = next(b for b in after["blocks"] if b["id"] == after["owners"]["pipe@100.0"]) assert block["id"] != owner and block["cover"] == first["cover"] cids = [ln["cid"] for ln in block["lines"]] assert cids == ["sangeun:SG:GF000119#0", "sangeun:SG:GF000219#0"] sheets = {"labor": [], "material": [], "machine": [], "gaps": []} out = compute_labor(after, sheets, {}, cover) got = next(b for b in out["daega_blocks"] if b["id"] == block["id"]) assert got["lines"][0]["qty"] == 0.7 and got["lines"][0]["covered"] == ["qty"] # 맞는 줄 assert got["lines"][1]["qty"] == 5 and "covered" not in got["lines"][1] assert out["daega_orphans"] == [{"cover": first["cover"], "cid": "sangeun:SG:GF000143#0"}] def test_저장_다듬기(): raw = { "daega_cover": { "S:a": {"labor:L1#0": {"qty": 0.5, "labor": "x", "memo": 1}, "b#0": {"qty": None}}, "S:빈": {}, "S:c": "글", "P:d": {"x#0": {"expense": float("inf"), "material": 3}}, } } assert store.clean_user(raw)["daega_cover"] == { "S:a": {"labor:L1#0": {"qty": 0.5}}, "P:d": {"x#0": {"material": 3.0}}, } assert "daega_cover" not in store.clean_user({"daega_cover": {"S:a": {}}}) def test_저장_뒤_서버_값(tmp_path): """[저장](write_user → read_user) 뒤 서버 셈 = 화면 짝 셈(덮개 입력값만 받고 다시 셈).""" stored = store.write_user(tmp_path, {"daega_cover": COVER}) assert store.read_user(tmp_path)["daega_cover"] == stored["daega_cover"] == COVER daega = _covered_excel() server = compute_labor(daega, b145.SHEETS, b145.SANGEUN, stored["daega_cover"]) screen = b145._ts(daega, b145.SHEETS, b145.SANGEUN, COVER)["out"] assert b145._same(json.loads(json.dumps(server)), screen) def test_B08_카드_고친_칸_수(project): """702-4 B08 구조물도 카드 — 그 사본 블록 + 부른 블록의 덮개 수(카드 금액 = 사본 셈).""" got = daega_inputs(project) four = next(b for b in got["blocks"] if b["id"] == got["owners"]["pipe@400.0"]) stone = next(b for b in got["blocks"] if b["id"] == four["lines"][0]["key"]) store.write_user( project, { "daega_cover": { stone["cover"]: {"labor:LB000033#0": {"qty": 0.3}}, "S:없음": {"x#0": {"qty": 1}}, } }, ) assert cover_count(project, snapshots.folder_name("pipe@400.0")) == 1 # 부른 품셈 블록 assert cover_count(project, snapshots.folder_name("pipe@100.0")) == 0