"""프로젝트 복사본 · 일위대가 문서(PLAN 39-1c · 임시 저장소 · 임시 마스터 사본). - 층 `labor/<열>.json` 이 있으면 스냅샷 `일위대가.json` = {줄: 문서, 로직, 빠진로직, 알약} · 단가 묶음에 자재 MT - `입력값.json` 첫 값 = 문서 입력값(확인 거짓) · 재적용은 프로젝트에서 넣은 값이 이김 - 계산 = 수량(손 · 면적 알약 칸) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 기준 단가 × 수량 · 반영합 = 손셈 · 기준 · 반영이 원본 층 문서 계산(`M02_Labor_Doc.calc`)과 같음 - 일위대가 쓰기는 400(읽기만) """ from __future__ import annotations import json import shutil from decimal import Decimal from pathlib import Path from typing import Any import pytest from config import config_system from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Item_Copy as copies from M02_MasterTemplete import M02_Item_Snapshot as snap from M02_MasterTemplete import M02_Labor_Doc as labor_doc from M02_MasterTemplete import M02_Template_Layers as layers from resources.tester.helper_m02_master_sample import SAMPLE MASTER = config_system.PROJECT_ROOT / "resources/master_data" PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"} COLUMN = "pp_len" FOLDER = "pipe@40.0" LOGICS = ("GF000176", "GC000088", "GF000113") MATERIAL = "MT000008" HAND = 3 # GC000088 손 수량 AREA_QTY = 7.5 # 자재 · GF000113 은 면적 알약 칸 값 MAT_QTY = 4 @pytest.fixture(scope="module") def master_copy(tmp_path_factory) -> Path: target = tmp_path_factory.mktemp("master_data") for path in MASTER.glob("*.json"): shutil.copy2(path, target / path.name) return target def _read(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) def _write(path: Path, content: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(content, ensure_ascii=False), encoding="utf-8") def _first_input(code: str) -> tuple[str, Any]: whole = m01.loaded()[1] name, value = next(iter(m01.mau.values(whole, whole.logic(code)).items())) return name, int(value) if isinstance(value, Decimal) and value % 1 == 0 else value @pytest.fixture def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]: system = tmp_path / "master" system.mkdir() for name in (layers.COMBO_FILE, "자재조합.json"): shutil.copy2(SAMPLE / name, system / name) for kind in ("table", "structure", "area", "material"): shutil.copytree(SAMPLE / kind, system / kind) monkeypatch.setattr(layers, "SYSTEM_ROOT", system) monkeypatch.setattr(m01, "FOLDER", master_copy) storage = tmp_path / "storage" monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) root = storage / "3" / "12" / "proj" (root / "B05_Profile/route").mkdir(parents=True) (root / "B04_PreProcess/drainage/edits").mkdir(parents=True) name, value = _first_input("GC000088") doc = labor_doc.clean( { "열": COLUMN, "단위": "m", "담은로직": [ {"로직": LOGICS[0]}, {"로직": LOGICS[1], "입력값": {name: value}, "수량": {"손": HAND}}, {"로직": LOGICS[2]}, ], "자재": [{"코드": MATERIAL}], } ) _write(system / "labor" / f"{COLUMN}.json", doc) pills = {p["키"]: p for p in labor_doc.pills(doc)} area_pill = next(k for k, p in pills.items() if LOGICS[2] in p.get("로직", [])) area = _read(system / "area" / f"{COLUMN}.json") sheet = area["시트"][0] sheet["칸"] = {"A1": {"값": AREA_QTY}, "A2": {"값": MAT_QTY}} sheet["연결"] = [ {"키": area_pill, "칸": "A1"}, {"키": labor_doc.material_key(MATERIAL), "칸": "A2"}, ] _write(system / "area" / f"{COLUMN}.json", area) return { "root": root, "doc": doc, "input": (name, value), "pills": pills, "area_pill": area_pill, } def _save(root: Path) -> dict[str, list[str]]: _write(root / "B05_Profile/route/structures.json", {"revision": 1, "structures": []}) pipe = {"chainage_m": 40.0, "source": "user", "summary_item": PIPE} _write(root / "B04_PreProcess/drainage/edits/pipe_points.json", {"points": [pipe]}) return snap.sync_snapshots(root) def _fill_auto(root: Path) -> None: """빠진 입력을 자동값으로(확인 참) — 문서 입력값은 그대로 둠.""" current = copies.read_file(root, FOLDER, "입력값") values = current["내용"] whole = m01.loaded()[1] for code in LOGICS: for key, auto in m01.mau.values(whole, whole.logic(code)).items(): mine = values.setdefault(code, {}) if key not in mine and auto is not None: mine[key] = {"값": labor_doc._json(auto), "확인": True} copies.write_file(root, FOLDER, "입력값", current["판"], values) def test_스냅샷_일위대가는_문서_알약_자재_단가(world): root = world["root"] assert _save(root)["만듦"] == [FOLDER] folder = root / "templates/structures" / FOLDER labor = _read(folder / "일위대가.json") assert labor["줄"] == world["doc"] assert [row["키"] for row in labor["로직"]] == list(LOGICS) and labor["빠진로직"] == [] assert {p["키"] for p in labor["알약"]} == set(world["pills"]) keys = { row["키"] for data in _read(folder / "단가.json").values() for row in data.get("줄", []) } assert MATERIAL in keys and set(LOGICS) <= keys def test_입력값_첫_값은_문서_입력값(world): root = world["root"] _save(root) name, value = world["input"] values = _read(root / "templates/structures" / FOLDER / "입력값.json") assert values == {LOGICS[1]: {name: {"값": value, "확인": False}}} def test_재적용은_프로젝트_값이_이김(world): root = world["root"] _save(root) name, _ = world["input"] current = copies.read_file(root, FOLDER, "입력값") mine = {LOGICS[1]: {name: {"값": 99, "확인": True}}} copies.write_file(root, FOLDER, "입력값", current["판"], mine) got = copies.reapply(root, FOLDER) assert got["값있음"] is True assert _read(root / "templates/structures" / FOLDER / "입력값.json") == mine def test_계산_반영_합은_손셈_원본과_같음(world): root = world["root"] _save(root) _fill_auto(root) got = copies.calc(root, FOLDER) assert got["멈춤"] == [], got["줄"] lines = {line["로직"]: line for line in got["줄"]} hand, area = lines[LOGICS[1]], lines[LOGICS[2]] assert (hand["수량"], hand["수량출처"]) == (HAND, "손") assert (area["수량"], area["수량출처"]) == (AREA_QTY, "칸") assert lines[LOGICS[0]]["수량출처"] == "없음" and lines[LOGICS[0]]["반영"] is None for line in (hand, area): expect = Decimal(str(line["계"])) * Decimal(str(line["수량"])) / Decimal(str(line["배"])) assert Decimal(str(line["반영"]["계"])) == pytest.approx(expect) (mat,) = got["자재"] assert mat["코드"] == MATERIAL and mat["수량"] == MAT_QTY and mat["기준단가"] assert mat["반영"] == pytest.approx(mat["기준단가"] * MAT_QTY) total = hand["반영"]["계"] + area["반영"]["계"] + mat["반영"] assert got["반영합"]["계"] == pytest.approx(total) # 원본 층 문서 계산(마스터 같은 사본)과 같음 — 입력값은 프로젝트에 채운 것 values = _read(root / "templates/structures" / FOLDER / "입력값.json") doc = { **world["doc"], "담은로직": [ {**one, "입력값": {k: v["값"] for k, v in values.get(one["로직"], {}).items()}} for one in world["doc"]["담은로직"] ], } given = {world["area_pill"]: AREA_QTY, labor_doc.material_key(MATERIAL): MAT_QTY} origin = labor_doc.calc(doc, given) assert got["반영합"] == pytest.approx(origin["반영합"]) assert got["기준합"] == pytest.approx(origin["기준합"]) def test_일위대가_쓰기는_막힘(world): root = world["root"] _save(root) current = copies.read_file(root, FOLDER, "일위대가") assert current["내용"]["줄"]["열"] == COLUMN with pytest.raises(ValueError, match="원본"): copies.write_file(root, FOLDER, "일위대가", current["판"], current["내용"])