Files
Aislo/resources/tester/test_792_1_sangeun_formula.py
T
eomsangdonandClaude Sonnet 5.5 137cfb701d feat(796-2c): M02 수식 탭이 SK 셈 답 전체를 그림 — 더한 기초 · 뺀줄 · 덮개 · 율 줄 · 합밖 소계표 · 합계 = SK 금액 · 고친 칸 표지
- 서버 M02_Sangeun_Equation_Full(새 파일) — 합계 = SK 셈 답 기준 · 소계 = 셈 답 줄마다 한 행(바탕 · 덮음 · 더함 · 뺌 · 합밖) + 끝수 · 더한 기초 수식 나무 · 줄 표지(식 · 상수 · 표값 · 품 · 단가 · 변수줄 · 뺌 + 원래 → 지금)
- M02_Sangeun_Equation — read · preview 가 얹음(전 = 고침 없는 SK 금액) · 수식 입력 = 자동값(SK 셈과 같은 입력)
- 부품 createEqSum(tree, open) 상태 알약 · 원래 · 접힘 기초 수식 · 줄 표지 · 흐림 CSS
- 시험 test_796_2c(견본 넷) · 낡은 시험 정리 — test_792_1 TS 거울 시험(799-2 에 걷은 모델 파일 읽음) 지움 · test_799_1 호출 글 갱신
- SK 금액 전후 같음 · M01 수식 탭 그대로

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 13:24:35 +09:00

284 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""792-1 단가산출(SK) 사용자 식 칸 — 변수 줄 · 식 덮개 · 더한줄 수량식 · 이름 꼴 ↔ 엔진 식 · 검사.
- 금액 = 서버 셈 = _분석_650-H2 4 장 견본(인력운반 「할증율 20」 변수 줄 + 보통인부 품 식
× (1 + 할증율 / 100) = 51,501) · 합판거푸집 줄 식 고침 · 공구손료 식 2 % = 51,802(790-2 같은 값)
- 이름 꼴(`×` · `[노임 형틀목공]` · `[별칭]`) ↔ 엔진 식 왕복 = M01 로직 견본 식 전부 같음
- 검사 까닭 — 없는 이름 · 괄호 · 순환 · 0 나눔 · 길이 · 괄호 깊이 · 변수 줄 수 · 자리 겹침
- 옛 SK(식 칸 없음) = 꼴 · 금액 그대로 · 저장 = 엔진 식 · 틀린 식 저장 400 · 셈 답 덧칸
- B09 사본 굳힘(`GX9#####`) 줄 = 식 칸 그대로 · 다시 셈 같은 금액
- 799-2 — TS 거울(`Formula_Model.ts` · `Const_Model.ts`)은 시트 탭과 함께 걷음 · 서버만 지킴
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Item_Sangeun as sk
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Flat as flat
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Const as const
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Sangeun_Formula as fx
from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층
ROOT = Path(__file__).resolve().parents[2]
FORM, CARRY = "GC000211", "GC001140" # 합판거푸집 · 인력운반(지게)
MAN = "labor:LB000002#0" # 인력운반 보통인부
TOOL = "pct:공구손료 및 경장비#0"
F211 = {"자재지역": "포항", "유형": "보통", "사용횟수": "6회", "높이감률": 0, "지붕슬래브": "아님"}
F211 |= {"폼타이": "아님", "폼타이규격": "12.7㎜", "측압": "3t/㎡", "구멍땜": 0}
PLUS = "무게 / 운반량 / 일운반회수 × (1 + 할증율 / 100)"
def _whole():
return m01.loaded()[1]
def _carry():
base = json.loads(json.dumps(_whole().logic(CARRY)["시험입력"], default=str))
return {**base, "재료종별": "암석", "재료형상": "화강암", "운반회수": 0}
def _doc(code, given, **more):
return {
"키": "SK-000001",
"이름": "견본",
"단위": "식",
"바탕": {"로직": code},
"입력값": given,
}
def _sk_doc(code, given, **more):
return {**_doc(code, given), **more}
def _run(doc):
whole = _whole()
return calc.run(sd.clean(doc, None, whole), whole)
def _sum(doc):
return float(_run(doc)["기준"]["계"])
def _plus(**more):
var = [{"이름": "할증율", "식": "20", "자리": ""}]
return _sk_doc(CARRY, _carry(), **({"변수줄": var, "식덮개": {MAN: PLUS}} | more))
# ── 견본 금액 ─────────────────────────────────────────────────────────
def test_변수_줄_할증율_견본():
assert _sum(_doc(CARRY, _carry())) == 42917
assert _sum(_plus()) == 51501 # _분석_650-H2 4 장
var = [{"이름": "할증율", "식": "만약(운반거리 > 5, 20, 0)", "표시": 0, "자리": "mid:무게"}]
assert _sum(_sk_doc(CARRY, _carry(), 변수줄=var, 식덮개={MAN: PLUS})) == 51501
eng = PLUS.replace("×", "*")
assert _sum(_sk_doc(CARRY, _carry(), 변수줄=var, 식덮개={MAN: eng})) == 51501 # 엔진 꼴도
def test_합판거푸집_줄_식_고침():
base = _sum(_doc(FORM, F211))
assert base == 51419
tool = "(형틀목공 × [노임 형틀목공] + 보통인부 × [노임 보통인부]) × 2 / 100"
assert _sum(_sk_doc(FORM, F211, 식덮개={TOOL: tool})) == 51802 # = 790-2 공구손료 2 %
more = "[합판거푸집설치및해체일당].형틀목공 × 1.1 / 시공량"
got = _run(_sk_doc(FORM, F211, 식덮개={"mid:형틀목공": more}))
line = next(x for x in got["줄"] if x["cid"] == "labor:LB000007#0")
assert float(line["수량"]) == pytest.approx(5 * 1.1 / 45)
assert got["기준"]["계"] > base
def test_더한줄_수량식():
add = {
"종류": "인력",
"요소": "LB000002",
"수량": 1,
"수량식": "무게 / 운반량 / 일운반회수 / 2",
}
got = _run(_sk_doc(CARRY, _carry(), 더한줄=[add]))
one, mine = (next(x for x in got["줄"] if x["cid"] == c) for c in (MAN, "add:#0"))
assert float(mine["수량"]) == pytest.approx(float(one["수량"]) / 2)
for bad in ({"수량식": "1", "종류": "로직", "로직": FORM, "수량": 1},):
with pytest.raises(sd.DocError, match="수만"):
sd.clean(_sk_doc(FORM, F211, 더한줄=[bad]), None, _whole())
# ── 이름 꼴 ↔ 엔진 식 ────────────────────────────────────────────────
def test_이름_꼴_양방향():
whole = _whole()
row = whole.logic(FORM)
tool = row["덧줄"][0]["식"]
named = fx.to_name(tool, row)
assert named == "(형틀목공 × [노임 형틀목공] + 보통인부 × [노임 보통인부]) × 1 / 100"
mid = fx.to_name(row["중간"][2]["식"], row)
assert mid.startswith("[합판거푸집설치및해체일당].시공량 × (1 - ")
assert fx.to_engine("[LB000007] ÷ 2", row) == ("LB000007 / 2", [])
assert fx.to_engine("[노임 형틀목공 LB000007] × 2", row)[0] == "LB000007 * 2"
assert fx.to_engine("합판거푸집구멍땜.시멘트", row)[0] == "찾기(QC000430).시멘트" # 맨 별칭
assert fx.to_engine("'[가]' × 1", row)[0] == "'[가]' * 1" # 따옴표 안 그대로
assert fx.to_engine("[없는것] × 1", row)[1] == ["없는 이름 「[없는것]」"]
assert fx.to_engine("[노임 × 1", row)[1] == ["대괄호 짝 안 맞음"]
for code in (FORM, CARRY, "GC000374", "GC000360"): # 띄어 쓴 자재 이름 · 긴 덧줄
one = whole.logic(code)
for part, f in (("중간", "식"), ("호표", "수량"), ("덧줄", "식")):
for x in one.get(part) or []:
assert fx.to_engine(fx.to_name(str(x[f]), one), one) == (str(x[f]), [])
# ── 검사 ─────────────────────────────────────────────────────────────
def _check(spot, text, doc=None):
return fx.check_doc(doc or _plus(), spot, text)
def test_검사_까닭():
ok = _check(MAN, PLUS)
assert ok["되나"] and ok["식"] == PLUS.replace("×", "*") and ok["글"] == PLUS
assert _check(MAN, "무게 / 없는것")["까닭"] == ["모르는 이름 「없는것」"]
assert "「)」 없음" in _check(MAN, "(무게 / 2")["까닭"][0]
assert _check("mid:할증율", "할증율 + 1")["까닭"] == ["순환 — 「할증율」 이 자기 자신을 씀"]
two = _plus(
변수줄=[{"이름": "가", "식": "1", "자리": ""}, {"이름": "나", "식": "가", "자리": ""}]
)
assert _check("mid:가", "나 + 1", two)["까닭"] == ["순환 — 가 → 나 → 가"]
assert _check("mid:할증율", "운반무게 / 0")["까닭"] == ["0 으로 나눔"]
assert _check(MAN, "무게 / 운반회수")["알림"] == ["「운반회수」 가 0 이면 0 나눔"]
assert _check(MAN, "1" + " + 1" * 80)["까닭"] == [f"식이 {fx.LONG} 자를 넘음"]
assert _check(MAN, "(" * 13 + "1" + ")" * 13)["까닭"] == [f"괄호가 {fx.DEEP} 겹을 넘음"]
assert _check("add:#3", "1")["까닭"] == ["없는 자리 「add:#3」"]
assert _check(MAN, " ")["까닭"] == ["식이 빔"]
def test_꼴_상한_겹침():
whole = _whole()
many = [{"이름": f"v{i}", "식": "1", "자리": ""} for i in range(fx.MANY + 1)]
for bad, why in (
({"변수줄": many}, "까지 목록"),
({"변수줄": [{"이름": "1가", "식": "1"}]}, "이름 꼴"),
({"변수줄": [{"이름": "가", "식": "1"}, {"이름": "가", "식": "2"}]}, "같은 이름"),
({"변수줄": [{"이름": "가", "식": "1", "표시": 7}]}, "0~6"),
({"식덮개": {"mid:시공량": "1"}, "상수덮개": {"mid:시공량": {"0": 1}}}, "같이 못 둠"),
({"식덮개": {"labor:LB000007#0": "1"}, "덮개": {"labor:LB000007#0": {"qty": 1}}}, "qty"),
({"식덮개": {"엉뚱": "1"}}, "자리 꼴"),
({"식덮개": {"mid:시공량": "(" * 13}}, "괄호"),
):
with pytest.raises(sd.DocError, match=why):
sd.clean(_sk_doc(FORM, F211, **bad), None, whole)
def test_틀린_식은_그_자리만_M01_식():
"""셈 길 = 틀린 자리만 M01 식 + 짝잃음 · 이름 겹친 변수 = 뺌."""
doc = _sk_doc(
CARRY,
_carry(),
변수줄=[{"이름": "무게", "식": "1", "자리": ""}],
식덮개={MAN: "무게 / 없는것", "mid:없는줄": "1"},
)
got = _run(doc)
assert got["기준"]["계"] == 42917
why = {(x["cid"], x["까닭"]) for x in got["짝잃음"]}
assert (MAN, "식 — 모르는 이름 「없는것」") in why
assert ("mid:없는줄", "식 — 바탕에 없는 줄") in why
assert any(c == "mid:무게" and "이미 있음" in w for c, w in why)
# ── 옛 SK · 저장 · 셈 답 · 펼침 · 초기화 ─────────────────────────────────
def test_옛_SK_그대로():
whole = _whole()
old = _sk_doc(
FORM, F211, 덮개={"labor:LB000007#0": {"qty": 0.2}}, 상수덮개={"mid:시공량": {"2": 20}}
)
got = sd.clean(old, None, whole)
assert not set(got) & set(fx.FIELDS) and not fx.has(got)
row = whole.logic(FORM)
assert fx.settle(row, got) == (row, [], got)
assert _sum(_doc(FORM, F211)) == 51419 and _sum(old) == _sum({**old, "식덮개": {}})
def _sk(folder, **doc):
with store.at(folder):
made = refs.create("sangeun", {"이름": "내 단가산출", "단위": "식", "더한줄": [], **doc})
return made["이름"]
def _read(folder, key):
with store.at(folder):
return store.read("sangeun", key)["문서"]
def _calc(folder, doc):
with store.at(folder):
return calc.calc_doc(doc)
def test_저장_엔진_식_셈_답(world):
mine = world["personal"]
add = {"종류": "인력", "요소": "LB000002", "수량": 1, "수량식": "1 ÷ 일운반회수"}
var = [{"이름": "할증율", "식": "20", "자리": ""}]
key = _sk(mine, 바탕={"로직": CARRY}, 입력값=_carry(), 변수줄=var, 식덮개={MAN: PLUS})
doc = _read(mine, key)
assert doc["식덮개"] == {MAN: PLUS.replace("×", "*")} and doc["변수줄"] == var
got = _calc(mine, doc)
assert got["기준"]["계"] == 51501 and got["원래"]["계"] == 42917
spot = got["시트"]["자리"][MAN]
assert spot["식덮개"] is True and spot["덮개"] == ["식"] and spot["식글"] == PLUS
assert spot["원식글"] == "무게 / 운반량 / 일운반회수" and spot["식"] == spot["원식글"]
v = got["시트"]["자리"]["mid:할증율"]
assert v["갈래"] == "변수" and v["식글"] == "20" and float(v["지금"]) == 20
names = {(x["글"], x["갈래"]) for x in got["시트"]["이름표"]}
assert {("할증율", "변수"), ("[인력운반기본공식]", "별칭"), ("노무비", "비목")} <= names
assert ("[노임 보통인부]", "자원") in names # 호표 요소
key2 = _sk(mine, 바탕={"로직": CARRY}, 입력값=_carry(), 더한줄=[add])
doc2 = _read(mine, key2)
assert doc2["더한줄"][0]["수량식"] == "1 / 일운반회수"
assert _calc(mine, doc2)["시트"]["자리"]["add:#0"]["식글"] == "1 / 일운반회수"
with store.at(mine), pytest.raises(store.StoreError) as e: # 틀린 식 저장 = 400
refs.create("sangeun", {**_plus(식덮개={MAN: "무게 / 없는것"}), "더한줄": []})
assert e.value.status == 400 and "모르는 이름" in str(e.value.detail)
def test_펼침_막음_초기화_걷음(world):
mine = world["personal"]
a = _sk(mine, 바탕={"로직": CARRY}, 입력값=_carry(), 변수줄=[{"이름": "할증율", "식": "20"}])
with store.at(mine), pytest.raises(store.StoreError):
flat.flat_lines("sangeun", {**MINE, "키": a})
doc, n = flat.reset("sangeun", {**_plus(), "까닭": {MAN: "할증"}})
assert n == 1 and "식덮개" not in doc and "까닭" not in doc and doc["변수줄"]
assert const.restore(_plus(), MAN).get("식덮개") is None
assert const.kinds(_plus(), MAN) == ["식"] and const.covers_n(_plus()) == 1
# ── B09 사본 굳힘 ─────────────────────────────────────────────────────
def test_B09_굳힘_식_칸_그대로():
whole = _whole()
add = {"종류": "인력", "요소": "LB000002", "수량": 1, "수량식": "1 / 일운반회수"}
doc = sd.clean({**_plus(), "더한줄": [add]}, None, whole)
live = calc.run(doc, whole)["기준"]["계"]
rid = "personal|7/42|SK-000001"
frozen = sk.freeze({rid: doc})[rid]
row, key = frozen[sk.ROW], frozen["바탕"]["로직"]
assert key.startswith("GX9") and frozen["원본"]["식덮개"] == doc["식덮개"]
assert [x["이름"] for x in row["중간"]][0] == "할증율"
assert row["호표"][0]["수량"] == PLUS.replace("×", "*")
assert row["호표"][1]["수량"] == "1 / 일운반회수"
over = calc._Over(whole, key, json.loads(json.dumps(row)))
again = calc.run(sd.clean(frozen, None, over), over)["기준"]["계"]
assert again == live and float(live) > 51501
def test_같은_내용_지문_식_칸_가림():
"""식 칸만 다른 SK = 다른 내용(409 같은 것 물음 안 함) · 옛 SK 지문 그대로."""
from M02_MasterTemplete import M02_Library_Same as same
plain = _doc(CARRY, _carry())
one, two = same.finger("sangeun", plain), same.finger("sangeun", _plus())
assert one and two and one != two
assert same.finger("sangeun", {**plain, "식덮개": {}, "변수줄": []}) == one