- 서버 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>
284 lines
14 KiB
Python
284 lines
14 KiB
Python
# 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
|