# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 """802-4b-2 M02 단가산출(SK) 수식 탭 = 공용 수식 문서 화면 — 임시 storage · M01 은 메모리 덮개. ① 길 앞머리 — M02 수식 탭 = 공용 화면(`createDocScreen` · `/m02/layers/{층}/sangeun/{키}/doc`) · 옛 M02 수식 나무 화면 걷음 · 연결 후보 · 표 길 등록 ② 읽기 연결 기록만으로 같은 셈(화면이 네트워크 없이 다시 셈) — 끊은 단가산출 = 끊음 문서 먼저 ③ 권한 — 회사 층 일반 계정 = 읽기 · 후보 · 표만(미리 셈 · 저장 403) · 화면 = 읽기만(옵션 잠금) ④ 꺼진 줄 — 만약 0 갈래가 계산 변수를 거쳐 와도 꺼짐(폼타이 「아님」 = 세 줄) · 입력 0 안 숨김 · 1,518 문서 기본 옵션 — 옛 판정 ⊆ 새 판정 · 꺼진 줄 금액 모두 0 """ from __future__ import annotations import copy import json import os import subprocess from decimal import Decimal from pathlib import Path import pytest from fastapi import HTTPException from M02_MasterTemplete import M02_Doc_Copy as dc from M02_MasterTemplete import M02_Doc_Router as router from M02_MasterTemplete import M02_Doc_Screen as screen from M02_MasterTemplete import M02_Doc_Sk_Path as path from M02_MasterTemplete import M02_MasterTemplete_Store as store from resources.tester.test_795_1_sk_copy import _sk from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 from resources.tester.test_m02_structure import write_sample_table ROOT = Path(__file__).resolve().parents[2] HERE = os.path.dirname(os.path.abspath(__file__)) FORM = "GC000211" # 합판거푸집 — 폼타이 · 측압 · 규격 옵션 BASIC = "GF000089" # 토사깍기(기계) — 굴착기 시간당 × 1 / 작업량(813-2 GC001148 지움 뒤) ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub3임시 굴착기"} def _at(folder, fn, *args): with store.at(folder): return fn(*args) def _src(rel: str) -> str: return (ROOT / rel).read_text(encoding="utf-8") def test_길_앞머리_M02_공용화면(): tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts") assert "createStackScreen({" in tabs and "copy: { route: (k, p) => skDocPath(" in tabs assert "/m02/layers/${scope.layer}/sangeun/${encodeURIComponent(key)}/doc${tail}" in tabs assert "admin: opt.edit" in tabs # 층 쓰기 권한 = 고침 scr = _src("A00_Common/line_stack_screen.ts") # 804-6 수식 탭 = 줄 쌓기 assert "cp ? cp.route(key, path) : opt.base + path" in scr assert "{ 문서: doc, 끊음: cut, 판: version }" in scr # 저장 몸 detail = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts") assert 'tabs.draw(/^SK-\\d{6}$/.test(doc.키 ?? "")' in detail # 저장한 SK 만 · 줄 모달 = 글 assert not (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Equation.ts").exists() main = _src("main.py") assert '__import__("M02_MasterTemplete.M02_Doc_Screen", fromlist=["router"]).router' in main for name in ("line_stack_screen", "formula_doc_copy"): assert len(_src(f"A00_Common/{name}.ts").splitlines()) <= 700, name class _Replay: """화면 캐시 흉내 — 서버 연결 기록만으로 셈(`common_util_formula_doc_cache.ts` 짝).""" def __init__(self, rec: dict): self.rec = rec self.els = {self._k(e["p"]): e for e in rec.get("요소") or []} @staticmethod def _k(p) -> str: return json.dumps(p, ensure_ascii=False, sort_keys=True, default=str) def element(self, p): got = self.els[self._k(dc.ml._norm(p))] if "빈" in got: raise dc.mc.Gap(got["빈"]) if "끊김" in got: raise dc.mc.Broken(got["끊김"]) return Decimal(str(got["값"])) if isinstance(got["값"], (int, float)) else got["값"] def table(self, p): return dc.ml.Links(_Tables(self.rec["표"]), {}).table(p) def doc(self, key): d = (self.rec.get("문서") or {}).get(key) return json.loads(json.dumps(d), parse_float=Decimal, parse_int=Decimal) if d else d class _Tables: """기록 표만 든 색인(`Links.table` 이 `master.get` 으로 찾음).""" def __init__(self, tables: dict): self.tables = tables def get(self, ref): return json.loads(json.dumps(self.tables[ref]), parse_float=Decimal, parse_int=Decimal) def _replay(read: dict) -> dict: doc = json.loads(json.dumps(read["문서"]), parse_float=Decimal, parse_int=Decimal) return dc.mc.calc(doc, _Replay(read["연결"])) def test_연결기록만으로_같은셈_끊음먼저(world): me = world["personal"] key = _sk( me, 바탕={"로직": FORM}, 입력값={"폼타이": "사용", "측압": "4t/㎡"}, 더한줄=[copy.deepcopy(ADD)], ) read = _at(me, path.read, key) rec = read["연결"] assert {"요소", "표", "문서", "이름"} <= set(rec) and BASIC in rec["문서"] assert _replay(read)["계"] == read["셈"]["계"] cut = copy.deepcopy(read["부름"][BASIC]) line = cut["금액"][0] line["수량"] = f"({line.get('수량') or 1}) × 2" # 끊은 문서 안 줄 고침 saved = _at(me, path.save, key, {"끊음": {BASIC: cut}, "판": read["판"]}) assert saved["연결"]["문서"][BASIC]["금액"][0]["수량"] == line["수량"] # 끊음 먼저 assert saved["셈"]["계"] > read["셈"]["계"] assert _replay(saved)["계"] == saved["셈"]["계"] again = _at(me, path.read, key) assert again["끊음"][BASIC]["금액"][0]["수량"] == line["수량"] and again["판"] == saved["판"] got = _at(me, path.save, key, {"끊음": {}, "판": again["판"]}) # 다시 잇기 assert got["끊음"] == {} and got["셈"]["계"] == read["셈"]["계"] def _session(role="USER"): return {"role": role, "company_id": 7, "user_id": 42} def test_권한_회사층_일반계정_읽기만(world): company = world["company"] company.mkdir(parents=True, exist_ok=True) write_sample_table(company) with store.at(company): store.create_structure("rv") key = _sk(company, 바탕={"로직": FORM}) read = router.read_doc("company", key, None, _session()) assert read["셈"]["셈됨"] and read["연결"]["표"] body = router.DocBody(문서=read["문서"]) for fn in (router.preview_doc, router.save_doc): with pytest.raises(HTTPException) as e: fn("company", key, body, None, _session()) assert e.value.status_code == 403 found = screen.doc_links( "company", key, screen.LinksBody(갈래="단가산출", 검색="굴착기 시간당"), None, _session() ) assert any(c["키"] == "GC000267" for c in found["후보"]) assert "줄" in screen.doc_table("company", key, "QC000431", None, _session()) scr = _src("A00_Common/line_stack_screen.ts") assert "buildOptions(doc, given, !!cp && !opt.admin," in scr # 사본 읽기만 = 옵션 잠금 copy_src = _src("A00_Common/formula_doc_copy.ts") assert "if (!edit) return [];" in copy_src and 'createBadge(ft("Read_Only")' in copy_src # ── ④ 꺼진 줄(TS 판정 그대로 · Node) ───────────────────────────────── def _off(cases: list[dict]) -> list[dict]: proc = subprocess.run( ["node", os.path.join(HERE, "helper_802_4b2_off.cjs")], input=json.dumps(dc.ml._norm(cases), ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(proc.stdout) def _vals(doc: dict, given: dict | None = None) -> dict: links = dc.ml.Links(dc.skc.whole_(), dc.folder()) return dc.mc.calc(doc, links, given).get("값") or {} def test_꺼진줄_계산변수_거침(): doc = dc.folder()[FORM] names = [x["이름"] for x in doc["금액"]] tie = [i for i, n in enumerate(names) if n.startswith("폼타이")] hole = [i for i, n in enumerate(names) if "구멍땜" in n] assert len(tie) == 3 and hole off, on, on2 = _off( [ {"문서": doc, "값": _vals(doc)}, # 폼타이 아님 · 규격 12.7 {"문서": doc, "값": _vals(doc, {"폼타이": "사용"})}, {"문서": doc, "값": _vals(doc, {"폼타이": "사용", "폼타이규격": "9.5㎜"})}, ] ) assert set(tie) <= set(off["새"]) and tie[0] not in off["옛"] # Q_조 = 만약 0 을 거침 assert set(on["새"]) == set(tie) - {tie[0]} and set(on2["새"]) == set(tie) - {tie[1]} assert not set(hole) & set(off["새"]) # 구멍땜 0 개소 = 입력 0 · 안 숨김 plain = { "식": [{"기호": "Z", "식": "0"}, {"기호": "Y", "식": "만약(S == 'a', 0, 2)"}], "금액": [ {"이름": "글 0", "비목": "노무비", "수량": "Z × 3", "단가": "w"}, {"이름": "입력 0", "비목": "노무비", "수량": "X × 3", "단가": "w"}, {"이름": "거침", "비목": "노무비", "수량": "Y × 3 ÷ 10", "단가": "w"}, {"이름": "합", "비목": "노무비", "수량": "Y + 1", "단가": "w"}, {"이름": "순환", "비목": "노무비", "수량": "V", "단가": "w"}, ], } plain["식"].append({"기호": "V", "식": "V × 2"}) got = _off([{"문서": plain, "값": {"S": "a", "Z": 0, "X": 0, "Y": 0, "w": 5}}])[0] assert got["새"] == [2] def test_꺼진줄_전수_1513(): docs = dc.folder() links = dc.ml.Links(dc.skc.whole_(), docs) keys, cases, rows = [], [], [] for key, doc in docs.items(): ans = dc.mc.calc(doc, links) keys.append(key) cases.append({"문서": doc, "값": ans.get("값") or {}}) rows.append(ans.get("줄") or []) got = _off(cases) assert len(keys) >= 1513 # 813-2 지움 6 new = sum(len(g["새"]) for g in got) assert all(set(g["옛"]) <= set(g["새"]) for g in got) and new > sum(len(g["옛"]) for g in got) for key, g, r in zip(keys, got, rows): for i in g["새"]: amt = r[i].get("금액") if i < len(r) else None assert amt is None or float(amt) == 0, (key, i, amt)