- M01 단가산출 기초 · M02 단가산출 상세 보기 = 수식(줄 쌓기 · 기본) · 단가산출 상세 둘 - 줄 쌓기에 M02 사본 [단가 갱신] · [재생성](미리 셈 → 알림 → 저장 · 고침 있으면 막음) · 판 바뀜(409) 문서 그대로면 새 판으로 다시 저장 옮김 - 옛 수식 문서 화면 걷음(formula_doc_screen · defs · edit · lines · math · math_model · rows · sub · formula_doc.css) · 이미 안 부르던 796-2 수식 나무 화면(LogicLab_Equation · Defs · Var · css · M01_Logic_Equation_Model · sheet_formula_cell · ui_template_equation_defs · edit) 걷음 — 18 파일 4,860 줄 - formula_doc_render = 줄 쌓기가 쓰는 reach 만 · 옵션 가로 CSS 는 line_stack.css 로 - 남김 = 계산기 common_util_formula_doc* · 서버 길 · 옵션 · 사본 표지 · 합계 상자 · KaTeX · 시험 페이지 tools/line-stack-test - 시험: test_796_2 지움(걷은 화면 원본 검사뿐) · 799_1 · 800_2 걷은 화면 줄만 뺌 · 799_2 · 802_4b2 · 804_2 보기 · 화면 파일 대상 바꿈 · test_804_6 새로(걷은 파일 · 부르는 곳 없음 · 첫 탭 · 사본 할일) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
10 KiB
Python
228 lines
10 KiB
Python
# 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 = "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_꺼진줄_전수_1518():
|
||
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) >= 1518
|
||
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)
|