- 표지 = 수식 문서 머리 · 옛 로직 줄 칸 「부품」 — 기계경비식 9 · 작업량 틀 60(기종 = 기계표 상세구분 · 규격 열쇠) · 계수식 4 · 변환기가 옮김 · 틀 문서 9 장 - 지움 = 작업량당 기계경비 6(GC001145 · 1148 · 1151 · 1156 · 1159 · 1166 · 부르는 곳 0 · 요구 734-4 · 735-4) — 수식 문서 · 옛 로직 · 키대장 폐기 칸 · 빌더 · 변수표 - 이름 꼬리 4 = 원문 제목(지게운반 · 인력운반(기계설비) · 벽돌운반 · 수중펌프 설치 및 해체) · 키 폐기 0 - 서버 = `M01_MasterData_Parts` — M01 `/logics` · M02 `/logic-lib/logics` 목록에서 부품 뺌(1,440) · 키로 열기 · 부르기 그대로 · `/m01/parts`(기종 계산식 · 틀 · 계수식) · `/m02/logic-lib/machine-parts` · 연결 찾기 과녁에서 부품 뺌(1,444 → 1,435 · 판정 코드 그대로) - 금액 전후 = 문서 1,513 · 셈 9,626(기본 + 옵션 표본) 모두 같음 · 791_1 지문 남은 1,513 같음 → 지문 다시 뜸 · 802_2 변환 대조 1,513 · 목록 xlsx 다시 씀(요약 수) - 낡은 시험 = 1,519 · 1,518 기대 → 1,513 · GC001148 견본 → 같은 꼴 GF000089 토사깍기(기계)(블록 Q 39.293 · 합계 2,190) · 지운 6 을 셈하던 줄 걷음 · 목록 수 1,440 · 묶음 328 · 부르는 수 507 - 새 지킴 시험 test_813_2_parts 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 = "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)
|