# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 """802-4b-1 엑셀 불러오기 → 수식 문서(요구 664 · 665 · 요구서_664 0 장) — 임시 storage · M01 그대로. ① 가져온 식 SK = 수식 문서(손 값 = 숫자 · 이음 = 연결 + 엑셀 값 기억 · 중간 = 식 줄 · 얼린 변수 = 숫자 · 금액 줄 = 노무비 · 재료비 · 경비 · 단계 · 끝전 · 머리 `가져온` 표지) ② [엑셀 단가 그대로] = 숫자 값 셈 = 엑셀 · [우리 단가로] = 연결 값 셈(기계 = 시간당 그 비목) ③ 산근 속 산근 = 형제 SK 단가산출 연결(안쪽이 바뀌면 따라감 · 부르는 쪽 단가기준) ④ 소요량 · 계수 고른 칸 = 그 수 자리에 표 연결 기호(금액 그대로) ⑤ SK 금액 = 문서 셈 = 옛 엔진 셈 · 수식 길 읽기 · 고침(따로) · 옛 칸 고침 409 · 옮기기 대조 실무 엑셀 없으면 그 시험만 skip. """ from __future__ import annotations import copy import json import subprocess from decimal import Decimal from pathlib import Path import helper_m02_hopyo_golden as golden import pytest from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Doc_Import as dim from M02_MasterTemplete import M02_Doc_Migrate as mig from M02_MasterTemplete import M02_Doc_Sk as dsk from M02_MasterTemplete import M02_Doc_Sk_Path as path from M02_MasterTemplete import M02_Hopyo_Store as hs from M02_MasterTemplete import M02_Machine_Hourly as machine from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh from M02_MasterTemplete import M02_Sangeun_Imported as imp from M02_MasterTemplete import M02_Sangeun_QtyLinks as qty from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 KEY = "HP-000001" def _doc(items: list[dict], extra: dict | None = None, src: dict | None = None) -> dict: row = {"이름": "견본 sub2임시", "결과단위": "원/식", "호표": items, **(extra or {})} meta = {"로직": row, "단가기준": "엑셀", "원본": {"엑셀금액": {}, **(src or {})}} return {"키": "SK-000001", "이름": "견본", "단위": "식", "바탕": None, imp.KEY: meta} def _item(kind: str, name: str, price: float, part: str, link: str | None = None, qty_="1"): out = {"종류": kind, "이름": name, "단위": "식", "수량": qty_, "비목": part, "단가": price} return out | ({"이음": link} if link else {}) def _ours(doc: dict) -> dict: return {**doc, imp.KEY: {**doc[imp.KEY], "단가기준": "우리"}} def _var(doc: dict, part: str) -> dict: """금액 줄 `이름` 의 단가 기호 변수.""" line = next(x for x in doc["금액"] if x["이름"] == part) sym = str(line["단가"]).removesuffix(".계") return next((v for v in doc["변수"] if dim.dc.md.var_id(v) == sym), {}) def _eq(good: bool) -> str: for row in m01.loaded()[1].numbers["EQ"].values(): if bool(machine.hourly(row["키"]).get("ok")) is good: return row["키"] pytest.skip("EQ 견본 없음") # ── ① ② 꼴 · 두 단가기준 ────────────────────────────────────────────────── def test_doc_form_numbers_links_and_both_bases() -> None: eq = _eq(True) items = [ _item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"), _item("인력", "작업반장", 150000, "노무비", None, "0.1"), _item("기계", "굴착기", 30000, "경비", eq, "2"), ] mids = {"중간": [{"이름": "Qd", "식": "600"}, {"이름": "Q", "식": "Qd / 8"}]} marks = ["참고 · 금액 줄 못 읽음 · 보인 값 얼림", "추정 · 단가 후보 여럿"] doc = _doc(items, mids, {"표지": marks, "갈래": "EST", "파일": "견본.xls"}) root = dim.derive(doc) assert root["꼴"] == "수식문서" and root["키"] == "SK-000001" assert root["가져온"]["표지"] == marks and root["가져온"]["갈래"] == "EST" # 까닭 그대로 labor = _var(root, "보통인부")["연결"] assert labor == {"갈래": "요소", "키": "LB000002", "엑셀": Decimal(100000)} assert _var(root, "작업반장")["연결"] == {"갈래": "숫자", "값": Decimal(150000)} # 이음 없음 mach = _var(root, "굴착기")["연결"] assert mach["갈래"] == "단가산출" and mach["칸"] == "경비" and mach["엑셀"] == Decimal(30000) qd = next(v for v in root["변수"] if v["열쇠"] == "Qd") # 얼린 변수 = 숫자 · 식 = 식 줄 assert qd["연결"] == {"갈래": "숫자", "값": Decimal(600)} assert [x["식"] for x in root["식"]] == [f"{dim.dc.md.var_id(qd)} ÷ 8"] excel = dim.run(root, doc) assert excel["계"] == Decimal(50000 + 15000 + 60000) # 엑셀 단가 그대로 ours = dim.run(root, _ours(doc)) lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"])) hour = machine.hourly(eq)["경비"] assert ours["노무비"] == int(lb * Decimal("0.5") * 10) / Decimal(10) + 15000 assert float(ours["경비"]) == pytest.approx(hour * 2, abs=1) # 기계 = 시간당 그 비목 def test_machine_without_hourly_keeps_number_with_note() -> None: eq = _eq(False) doc = _doc([_item("기계", "리퍼", 5000, "경비", eq)]) var = _var(dim.derive(doc), "리퍼") assert var["연결"] == {"갈래": "숫자", "값": Decimal(5000)} and eq in var["설명"] assert dim.run(dim.derive(doc), _ours(doc))["계"] == 5000 def test_sk_money_is_doc_calc_and_equals_old_engine(world) -> None: """⑤ SK 금액 = 문서 셈(수식셈 표지) = 옛 엔진 셈 — 엑셀 · 우리 둘 다 · 덧줄 율 · 합밖.""" items = [ _item("인력", "보통인부", 100000, "노무비", "LB000002", "0.33"), _item("재료", "경유", 1700, "재료비", "MO000002", "3") | {"합밖": True}, ] extra = {"덧줄": [{"이름": "공구손료", "식": "노무비 * 0.03", "비목": "경비"}]} doc = _doc(items, extra) for basis in ("엑셀", "우리"): mine = imp._with(doc, 단가기준=basis) new = imp._money(imp._run(mine, imp._whole())) token = dsk.OLD.set(True) try: old = imp._money(imp._run(mine, imp._whole())) finally: dsk.OLD.reset(token) assert new == old, basis got = imp.to_price(doc, "우리") assert got["전"]["계"] == 33000 + 990 and got["후"]["계"] != got["전"]["계"] # ── ③ 산근 속 산근 · 실무 STmate · EST ────────────────────────────────────── def _hp(world, tag: str): path_ = golden.FILES.get(tag) if path_ is None or not path_.exists(): pytest.skip(f"실무 엑셀 없음 {tag}") folder = world["personal"] hs._dir(folder).mkdir(parents=True, exist_ok=True) (hs._dir(folder) / f"{KEY}.json").write_text(f'{{"키": "{KEY}", "판": {hs.BOOK}}}', "utf-8") fh.keep(folder, KEY, path_.name, path_.read_bytes()) return folder def _read(key: str) -> dict: return store.read("sangeun", key)["문서"] def test_stmate_nested_is_sibling_link(world) -> None: folder = _hp(world, "울진대흥") with store.at(folder): made = fh.make(folder, KEY, 2200) # D02200 안에 D00075 콘크리트믹서사용 doc = _read(made["이름"]) root = dim.derive(doc) assert root["가져온"]["갈래"] == "STmate" and root["가져온"]["코드"] == "D02200" inner = _var(root, "콘크리트믹서사용")["연결"] assert inner["갈래"] == "단가산출" and inner["키"].startswith("SK-") assert inner["엑셀"]["노무비"] == 6216 # 엑셀 얼린 값 기억 assert dim.run(root, doc)["계"] == 67383 == made["검산"]["엑셀"]["계"] assert dim.run(root, _ours(doc))["셈됨"] is True sub = _read(inner["키"]) # 안쪽 단가 줄 하나 두 배 → 바깥이 따라감(엑셀 기준도) one = next(x for x in sub[imp.KEY]["로직"]["호표"] if "단가" in x) one["단가"] = one["단가"] * 2 store.write("sangeun", inner["키"], store.read("sangeun", inner["키"])["판"], sub) now = dim.run(dim.derive(_read(inner["키"])), _read(inner["키"]))["계"] assert now != 7528 assert dim.run(root, doc)["계"] == pytest.approx(67383 - 7528 + now, abs=1) def test_est_by_part_and_frozen_vars(world) -> None: folder = _hp(world, "북부청임도") with store.at(folder): made = fh.make( folder, KEY, 67 ) # 단산68 세 줄(노 · 재 · 경) = 묶음 한 줄(812-1 · 원파일 길) doc = _read(made["이름"]) root = dim.derive(doc) parts = [ (x["비목"], _var(root, x["이름"])["연결"].get("칸")) for x in root["금액"] if (_var(root, x["이름"]).get("연결") or {}).get("키", "").startswith("SK-") ] assert parts == [("묶음", None)] assert not any(x["식"].strip().replace(".", "").isdigit() for x in root["식"]) excel = dim.run(root, doc) assert float(excel["계"]) == pytest.approx(float(made["검산"]["엑셀"]["계"]), abs=1) assert dim.run(root, _ours(doc))["셈됨"] is True # ── ④ 소요량 · 계수 고른 칸 ───────────────────────────────────────────────── def test_qty_pick_becomes_table_link(world) -> None: folder = _hp(world, "북부청임도") found = None for no in range(1, 200): try: out = fh.emit(folder, KEY, no) except store.StoreError: continue if "야면석채집및운반(계곡내)" in out["로직"]["이름"]: found = out break if found is None: pytest.skip("야면석채집및운반(계곡내) 블록 없음") row = found["로직"] doc = _doc(row["호표"], {k: row[k] for k in ("중간", "덧줄", "단계", "끝전") if row.get(k)}) doc[imp.KEY]["로직"] |= {k: row[k] for k in ("이름", "결과단위")} at = next(i for i, x in enumerate(row["호표"]) if x["이름"] == "보통인부") cell = next(c for c in qty.view(doc)["줄"] if c["자리"] == f"호표:{at}:0")["후보"][0] kept = qty.pick(doc, f"호표:{at}:0", {k: cell[k] for k in ("표", "줄", "칸")})["문서"] root = dim.derive(kept) pick = next(v for v in root["변수"] if v["열쇠"].startswith(dim.PICK)) assert pick["연결"]["갈래"] == "표" and pick["연결"]["표"] == cell["표"] assert pick["연결"]["칸"] == cell["칸"] and pick["연결"][dim.XL] == Decimal(str(cell["값"])) line = next(x for x in root["금액"] if x["이름"] == "보통인부") assert dim.dc.md.var_id(pick) in line["수량"] before = dim.run(dim.derive(doc), doc)["계"] assert dim.run(root, kept)["계"] == before # 엑셀 그대로 금액 같음 assert dim.run(root, _ours(kept))["셈됨"] is True # 표 칸 = 같은 값 # ── ⑤ 수식 길 · 옮기기 ─────────────────────────────────────────────────── def test_formula_route_edit_then_old_edit_409_and_regen(world) -> None: folder = world["personal"] doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")]) with store.at(folder): key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 견본")["이름"] read = path.read(key) assert read["셈"]["계"] == 50000 and read["사본"]["단가기준"] == "엑셀" edit = copy.deepcopy(read["문서"]) edit["금액"][0]["수량"] = "1" pre = path.preview(key, {"문서": edit}) assert pre["전"]["계"] == 50000 and pre["후"]["계"] == 100000 path.save(key, {"문서": edit, "판": read["판"]}) again = path.read(key) assert again["사본"]["따로"] is True and again["셈"]["계"] == 100000 sk = _read(key) moved = imp.link(sk, sd_id(sk), None)["문서"] # 이음 풂 = 옛 칸 고침 → 409 with pytest.raises(store.StoreError) as why: store.write("sangeun", key, store.read("sangeun", key)["판"], moved) assert why.value.status == 409 ours = imp.to_price(sk, "우리")["문서"] # 단가기준만 바꿈 = 됨(문서 그대로 · 연결 값) store.write("sangeun", key, store.read("sangeun", key)["판"], ours) lb = Decimal(str(m01.loaded()[1].get("LB000002")["값"])) assert path.read(key)["셈"]["노무비"] == int(lb * 10) / Decimal(10) def sd_id(sk: dict) -> str: return imp.sd.line_ids(imp.row_of(sk))[0] def test_migrate_imported_same(world) -> None: folder = world["personal"] doc = _doc([_item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5")]) with store.at(folder): key = imp.create(doc[imp.KEY]["로직"], {"엑셀금액": {"계": 50000}}, "sub2임시 옮김")["이름"] file = folder / "sangeun" / f"{key}.json" got = mig.check("sangeun", file) assert got["상태"] in ("같음", "있음") if got["상태"] == "같음": assert got["수식"]["문서"]["키"] == key and got["전"] == got["후"] # ── TS 짝(importView) 거울 ───────────────────────────────────────────── _NODE = """ const fs = require("fs"), path = require("path"); const ts = require(path.join(process.cwd(), "config", "node_modules", "typescript")); const at = path.join(process.cwd(), "common_util", "common_util_formula_doc_import.ts"); const js = ts.transpileModule(fs.readFileSync(at, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, () => ({})); const got = JSON.parse(fs.readFileSync(0, "utf8")); process.stdout.write(JSON.stringify(got.map(([d, b]) => box.exports.importView(d, b)))); """ def test_ts_view_mirror() -> None: eq = _eq(True) items = [ _item("인력", "보통인부", 100000, "노무비", "LB000002", "0.5"), _item("인력", "작업반장", 150000, "노무비", None, "0.1"), _item("기계", "굴착기", 30000, "경비", eq, "2"), ] root = dim.derive(_doc(items)) root["변수"].append( {"열쇠": "S", "기호": "S", "연결": {"갈래": "단가산출", "키": "SK-000009", "엑셀": 5}} ) plain = json.loads(json.dumps(root, ensure_ascii=False, default=float)) cases = [[plain, b] for b in ("엑셀", "우리")] out = subprocess.run( ["node", "-e", _NODE], input=json.dumps(cases, ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", cwd=Path(__file__).resolve().parents[2], check=True, ) ts_ = json.loads(out.stdout) py = [json.loads(json.dumps(dim.view(plain, b), ensure_ascii=False)) for b in ("엑셀", "우리")] assert ts_ == py assert _var(ts_[0], "보통인부")["연결"] == {"갈래": "숫자", "값": 100000.0} assert ts_[0]["변수"][-1]["연결"]["갈래"] == "단가산출" # 형제 SK = 늘 연결 def test_empty_block_is_excel_zero_not_dash() -> None: """줄 없는 엑셀 산근 = 0 원(옛 엔진 · 엑셀과 같음) · 표지 · 우리 단가로도 0.""" doc = _doc([]) root = dim.derive(doc) assert dim.EMPTY in root["가져온"]["표지"] assert dim.run(root, doc)["계"] == 0 == dim.run(root, _ours(doc))["계"] assert imp.check(doc)["셈"]["계"] == 0