Merge remote-tracking branch 'origin/dev' into sub_laptop_3
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""790-2 단가산출(SK) 숫자 넓힘 — 상수 · 표값 덮개 · 글 줄 · 까닭 · M01 원래로 · 짝잃음(요구서_652).
|
||||
|
||||
- 금액 = 서버 셈 = _분석_650-H2 4 장 견본(지붕 감률 17 → 20 = 61,088 · 공구손료 1 → 2 % = 51,802 ·
|
||||
인력운반 120 → 100 = 38,696 · 표값 운반량 30 = 35,764) · 옵션 바꿔도 상수 덮개 식 따라감
|
||||
- 식 속 숫자 차례 = 엔진 파서(찾기 머리 · 키 · EQ: · 따옴표 글 안 셈)
|
||||
- 옛 SK(새 칸 없음) = 꼴 · 금액 그대로 · 저장 = M01판 · 셈 답 `원래` · `시트`
|
||||
- [M01 원래로] · 더한줄 열쇠 옮김 · 덮개 갈래 = 파이썬 `M02_Sangeun_Const` ↔ TS `Const_Model` 거울
|
||||
- 펼침 = 상수 덮개 SK 안 펼침(참조 그대로 · 값 그대로) · [초기화] = 새 덮개도 걷음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
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 resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
FORM = "GC000211" # 합판거푸집
|
||||
CARRY = "GC001140" # 인력운반(지게)
|
||||
ROOF = "지붕 슬래브(경사 20˚ 미만)"
|
||||
TOOL = "pct:공구손료 및 경장비#0"
|
||||
F211 = {
|
||||
"자재지역": "포항",
|
||||
"유형": "보통",
|
||||
"사용횟수": "6회",
|
||||
"높이감률": 0,
|
||||
"지붕슬래브": "아님",
|
||||
"폼타이": "아님",
|
||||
"폼타이규격": "12.7㎜",
|
||||
"측압": "3t/㎡",
|
||||
"구멍땜": 0,
|
||||
}
|
||||
|
||||
|
||||
def _whole():
|
||||
return m01.loaded()[1]
|
||||
|
||||
|
||||
def _carry():
|
||||
return {
|
||||
**json.loads(json.dumps(_whole().logic(CARRY)["시험입력"], default=str)),
|
||||
"재료종별": "암석",
|
||||
"재료형상": "화강암",
|
||||
"운반회수": 0,
|
||||
}
|
||||
|
||||
|
||||
def _doc(code, given, **more):
|
||||
return {
|
||||
"키": "SK-000001",
|
||||
"이름": "견본",
|
||||
"단위": "식",
|
||||
"바탕": {"로직": code},
|
||||
"입력값": given,
|
||||
**more,
|
||||
}
|
||||
|
||||
|
||||
def _sum(doc):
|
||||
whole = _whole()
|
||||
return float(calc.run(sd.clean(doc, None, whole), whole)["기준"]["계"])
|
||||
|
||||
|
||||
# ── 식 속 자리 ────────────────────────────────────────────────────────
|
||||
def test_scan_엔진_차례():
|
||||
t = "찾기(QC000431, 유형=유형).시공량 * (1 - (높이감률 + 만약(지붕슬래브 == '아님', 0, 17)) / 100)"
|
||||
got = const.scan(t)
|
||||
assert [t[a:b] for a, b in got["num"]] == ["1", "0", "17", "100"]
|
||||
assert [t[a:b] for a, b in got["find"]] == ["찾기(QC000431, 유형=유형).시공량"]
|
||||
tool = "(형틀목공 * LB000007 + 보통인부 * LB000002) * 1 / 100"
|
||||
assert [tool[a:b] for a, b in const.scan(tool)["num"]] == ["1", "100"]
|
||||
assert const.scan("EQ:0201-0012.잡재료율 * 2 + '3t/㎡'")["num"] == [(20, 21)]
|
||||
assert const.scan("((((") == {"num": [], "find": []} # 못 읽는 식 = 빈
|
||||
assert const.swap(t, {"2": 20}, {"0": -45}).startswith("(-45) * (1 - ")
|
||||
assert ", 0, 20)) / 100)" in const.swap(t, {"2": 20}, {})
|
||||
|
||||
|
||||
# ── 금액 = 견본(서버 셈) ──────────────────────────────────────────────
|
||||
def test_상수_표값_덮개_견본_금액():
|
||||
roof = {**F211, "지붕슬래브": ROOF}
|
||||
assert _sum(_doc(FORM, F211)) == 51419
|
||||
assert _sum(_doc(FORM, roof)) == 59341
|
||||
assert _sum(_doc(FORM, roof, 상수덮개={"mid:시공량": {"2": 20}})) == 61088
|
||||
assert _sum(_doc(FORM, F211, 상수덮개={TOOL: {"0": 2}})) == 51802
|
||||
assert _sum(_doc(CARRY, _carry())) == 42917
|
||||
assert _sum(_doc(CARRY, _carry(), 상수덮개={"mid:일운반회수": {"1": 100}})) == 38696
|
||||
assert _sum(_doc(CARRY, _carry(), 표값덮개={"mid:운반량": {"0": 30}})) == 35764
|
||||
|
||||
|
||||
def test_옵션_바꿔도_상수_덮개_따라감():
|
||||
"""품 덮개(숫자로 굳힘)와 달리 식 구조 그대로 — 유형 복잡 = 손으로 식 고친 줄과 같은 값."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
whole = _whole()
|
||||
given = {**F211, "지붕슬래브": ROOF, "유형": "복잡"}
|
||||
got = _sum(_doc(FORM, given, 상수덮개={"mid:시공량": {"2": 20}}))
|
||||
row = json.loads(json.dumps(whole.logic(FORM), ensure_ascii=False, default=str))
|
||||
for s in row["중간"]:
|
||||
if s["이름"] == "시공량":
|
||||
s["식"] = s["식"].replace(", 0, 17))", ", 0, 20))")
|
||||
hand = labor.run_line(calc._Over(whole, FORM, row), FORM, given)["기준"]["계"]
|
||||
assert got == float(hand) and got not in (61088, _sum(_doc(FORM, given)))
|
||||
|
||||
|
||||
# ── 문서 꼴 ──────────────────────────────────────────────────────────
|
||||
def test_새_칸_꼴_검사():
|
||||
whole = _whole()
|
||||
doc = _doc(
|
||||
FORM,
|
||||
F211,
|
||||
상수덮개={"mid:시공량": {"2": "20"}, TOOL: {}},
|
||||
표값덮개={},
|
||||
글줄=[{"자리": "", "글": " 건설표준품셈 6-3-1 "}, {"자리": "mid:시공량", "글": ""}],
|
||||
까닭={"mid:시공량": "현장 경사 반영", "add:#0": " ", "add:#1/labor:LB000002#0": "x"},
|
||||
)
|
||||
got = sd.clean(doc, None, whole)
|
||||
assert got["상수덮개"] == {"mid:시공량": {"2": 20}} and "표값덮개" not in got
|
||||
assert got["글줄"] == [{"자리": "", "글": "건설표준품셈 6-3-1"}]
|
||||
assert got["까닭"] == {"mid:시공량": "현장 경사 반영", "add:#1/labor:LB000002#0": "x"}
|
||||
for bad in (
|
||||
{"상수덮개": {"엉뚱": {"0": 1}}},
|
||||
{"상수덮개": {"mid:시공량": {"a": 1}}},
|
||||
{"표값덮개": {"mid:운반량": {"0": "글"}}},
|
||||
{"까닭": {"x": "y"}},
|
||||
{"글줄": [{"자리": "", "글": "가" * 501}]},
|
||||
):
|
||||
with pytest.raises(sd.DocError):
|
||||
sd.clean(_doc(FORM, F211, **bad), None, whole)
|
||||
|
||||
|
||||
def test_옛_SK_꼴_금액_그대로():
|
||||
whole = _whole()
|
||||
old = _doc(FORM, F211, 덮개={"labor:LB000007#0": {"qty": 0.2}}, 뺀줄=["labor:LB000002#0"])
|
||||
got = sd.clean(old, None, whole)
|
||||
assert not set(got) & {*const.NEW, "M01판"}
|
||||
assert const.formulas(whole.logic(FORM), got) is whole.logic(
|
||||
FORM
|
||||
) # 새 덮개 없음 = 그 줄 그대로
|
||||
assert sd.clean({**old, "M01판": {"mid:시공량": "x"}}, None, whole) == got # M01판 = 서버 몫
|
||||
|
||||
|
||||
# ── 저장 · 셈 답(임시 storage) ─────────────────────────────────────────
|
||||
def _sk(folder, **doc):
|
||||
with store.at(folder):
|
||||
return refs.create("sangeun", {"이름": "내 단가산출", "단위": "식", "더한줄": [], **doc})[
|
||||
"이름"
|
||||
]
|
||||
|
||||
|
||||
def _read(folder, key):
|
||||
with store.at(folder):
|
||||
return store.read("sangeun", key)["문서"]
|
||||
|
||||
|
||||
def _calc(folder, doc, given=None):
|
||||
with store.at(folder):
|
||||
return calc.calc_doc(doc, given)
|
||||
|
||||
|
||||
def test_저장_M01판_셈답_원래_시트(world):
|
||||
mine = world["personal"]
|
||||
key = _sk(
|
||||
mine,
|
||||
바탕={"로직": FORM},
|
||||
입력값={**F211, "지붕슬래브": ROOF},
|
||||
상수덮개={"mid:시공량": {"2": 20}},
|
||||
덮개={"labor:LB000002#0": {"qty": 0.05}},
|
||||
글줄=[{"자리": "", "글": "※ 지붕 경사 반영"}],
|
||||
까닭={"mid:시공량": "감률 20 % 적용"},
|
||||
)
|
||||
doc = _read(mine, key)
|
||||
assert doc["상수덮개"] == {"mid:시공량": {"2": 20}} and len(doc["바탕판"]) == 16
|
||||
assert set(doc["M01판"]) >= {"mid:시공량", TOOL, "labor:LB000007#0"}
|
||||
got = _calc(mine, doc)
|
||||
assert got["원래"]["계"] == 59341 # 같은 옵션 · 덮개 없음
|
||||
assert got["기준"]["계"] != 59341
|
||||
no_qty = _calc(mine, {**doc, "덮개": {}})
|
||||
assert no_qty["기준"]["계"] == 61088 and no_qty["원래"]["계"] == 59341
|
||||
spot = got["시트"]["자리"]["mid:시공량"]
|
||||
assert spot["갈래"] == "중간" and spot["덮개"] == ["상수"] and spot["까닭"] == "감률 20 % 적용"
|
||||
assert spot["상수"][2] == {"n": 2, "값": 20, "원값": 17}
|
||||
assert spot["표값"][0]["글"].startswith("찾기(QC000431") and spot["표값"][0]["값"] == 45
|
||||
assert spot["원래"] > spot["지금"] and spot["M01바뀜"] is False
|
||||
line = got["시트"]["자리"]["labor:LB000002#0"]
|
||||
assert line["덮개"] == ["품"] and line["원래"] != line["지금"]
|
||||
assert got["시트"]["글줄"] == [{"자리": "", "글": "※ 지붕 경사 반영"}]
|
||||
assert got["시트"]["짝잃음"] == []
|
||||
# M01 바뀜 — 저장 지문과 지금 바탕이 다른 자리만 · 사라진 자리 = 짝잃음
|
||||
stale = {**doc, "M01판": {**doc["M01판"], "mid:시공량": "000000000000", "mid:없는줄": "1"}}
|
||||
got = _calc(mine, stale)
|
||||
flags = {s for s, x in got["시트"]["자리"].items() if x.get("M01바뀜")}
|
||||
assert flags == {"mid:시공량"}
|
||||
assert {"cid": "mid:없는줄", "까닭": "M01 바뀜 — 바탕에 없는 줄"} in got["시트"]["짝잃음"]
|
||||
|
||||
|
||||
def test_짝잃은_새_덮개_알림(world):
|
||||
doc = _doc(FORM, F211, 상수덮개={"mid:없음": {"0": 1}, "mid:시공량": {"9": 1}})
|
||||
got = _calc(world["personal"], doc)
|
||||
assert got["기준"]["계"] == 51419
|
||||
why = {(x["cid"], x["까닭"]) for x in got["시트"]["짝잃음"]}
|
||||
assert why == {
|
||||
("mid:없음", "상수덮개 — 바탕에 없는 줄"),
|
||||
("mid:시공량", "상수덮개 — 10 번째 없음"),
|
||||
}
|
||||
|
||||
|
||||
def test_옛_저장본_셈답(world):
|
||||
"""새 칸 없는 옛 저장본(M01판 없음) = 금액 그대로 · M01바뀜 없음."""
|
||||
got = _calc(world["personal"], {**_doc(FORM, F211), "바탕판": "0" * 16})
|
||||
assert got["기준"]["계"] == 51419 == got["원래"]["계"]
|
||||
assert not any(x.get("M01바뀜") for x in got["시트"]["자리"].values())
|
||||
|
||||
|
||||
def test_펼침_막음_초기화_걷음(world):
|
||||
mine = world["personal"]
|
||||
a = _sk(mine, 바탕={"로직": CARRY}, 입력값=_carry(), 상수덮개={"mid:일운반회수": {"1": 100}})
|
||||
b = _sk(mine, 바탕=None, 더한줄=[{"참조": {**MINE, "키": a}, "수량": 1}])
|
||||
assert _read(mine, b)["더한줄"][0]["참조"]["키"] == a # 안 펼침 = 참조 그대로
|
||||
assert _calc(mine, _read(mine, b))["기준"]["계"] == 38696
|
||||
with store.at(mine), pytest.raises(store.StoreError):
|
||||
flat.flat_lines("sangeun", {**MINE, "키": a})
|
||||
doc, n = flat.reset("sangeun", {**_read(mine, a), "까닭": {"mid:일운반회수": "가정"}})
|
||||
assert n == 1 and not set(doc) & {"상수덮개", "까닭"}
|
||||
|
||||
|
||||
# ── 파이썬 ↔ TS 거울 ─────────────────────────────────────────────────
|
||||
MIRROR_DOC = {
|
||||
"덮개": {"labor:LB000002#0": {"qty": 1}, "labor:LB000007#0": {"labor": 5}},
|
||||
"뺀줄": ["pct:소모자재#0"],
|
||||
"더한줄": [
|
||||
{
|
||||
"종류": "로직",
|
||||
"로직": "GC000001",
|
||||
"덮개": {"labor:X#0": {"qty": 1}},
|
||||
"뺀줄": ["material:Y#0"],
|
||||
},
|
||||
{"종류": "인력"},
|
||||
],
|
||||
"상수덮개": {"mid:시공량": {"2": 20}, "labor:LB000002#0": {"0": 3}},
|
||||
"표값덮개": {"mid:운반량": {"0": 30}},
|
||||
"글줄": [{"자리": "add:#1", "글": "a"}, {"자리": "add:#0", "글": "b"}],
|
||||
"까닭": {
|
||||
"mid:시공량": "c",
|
||||
"add:#0": "d",
|
||||
"add:#1": "e",
|
||||
"add:#0/labor:X#0": "f",
|
||||
"pct:소모자재#0": "g",
|
||||
},
|
||||
}
|
||||
KEYS = [
|
||||
None,
|
||||
"mid:시공량",
|
||||
"labor:LB000002#0",
|
||||
"pct:소모자재#0",
|
||||
"add:#0/labor:X#0",
|
||||
"add:#0/material:Y#0",
|
||||
"없음",
|
||||
]
|
||||
|
||||
|
||||
def _ts(body: str):
|
||||
src = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Const_Model.ts").read_text("utf-8")
|
||||
got = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src + body],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert got.returncode == 0, got.stderr
|
||||
return json.loads(got.stdout)
|
||||
|
||||
|
||||
def test_거울_M01원래로_열쇠옮김_갈래():
|
||||
d = json.dumps(MIRROR_DOC, ensure_ascii=False)
|
||||
keys = json.dumps(KEYS)
|
||||
spots = json.dumps(
|
||||
["labor:LB000002#0", "labor:LB000007#0", "pct:소모자재#0", "mid:운반량", "x"]
|
||||
)
|
||||
ts = _ts(
|
||||
f"\nconst d = {d};"
|
||||
f"\nconst out = {{r: {keys}.map((k) => restoreM01(d, k ?? undefined)),"
|
||||
" a: [0, 1, 5].map((k) => dropAddKeys(d, k)),"
|
||||
f" k: {spots}.map((s) => coverKinds(d, s))}};"
|
||||
"\nconsole.log(JSON.stringify(out));"
|
||||
)
|
||||
assert ts["r"] == [const.restore(MIRROR_DOC, k) for k in KEYS]
|
||||
assert ts["a"] == [const.drop_add_keys(MIRROR_DOC, k) for k in (0, 1, 5)]
|
||||
assert ts["k"] == [const.kinds(MIRROR_DOC, s) for s in json.loads(spots)]
|
||||
assert ts["k"][0] == ["품", "상수"] and ts["k"][2] == ["뺌"]
|
||||
|
||||
|
||||
def test_TS_고치기_꼴_서버가_받음():
|
||||
"""setConst · setTable · addText · editText · dropText · setReason 결과 = 서버 꼴 그대로 통과."""
|
||||
ts = _ts(
|
||||
"\nlet d = {키: 'SK-000001', 이름: '견본', 단위: '식', 바탕: {로직: 'GC000211'}};"
|
||||
"\nd = setConst(d, 'mid:시공량', 2, 20); d = setConst(d, 'mid:시공량', 0, 1);"
|
||||
"\nd = setConst(d, 'mid:시공량', 0, null); d = setTable(d, 'mid:시공량', 0, 45);"
|
||||
"\nd = setTable(d, 'mid:시공량', 0, null);"
|
||||
"\nd = addText(d, '', '근거'); d = addText(d, 'mid:시공량', '※ 조건', 0);"
|
||||
"\nd = editText(d, 1, '근거 고침'); d = dropText(d, 0);"
|
||||
"\nd = setReason(d, 'mid:시공량', ' 까닭 '); d = setReason(d, 'add:#0', '');"
|
||||
"\nconsole.log(JSON.stringify(d));"
|
||||
)
|
||||
assert ts["상수덮개"] == {"mid:시공량": {"2": 20}} and "표값덮개" not in ts
|
||||
assert ts["글줄"] == [{"자리": "", "글": "근거 고침"}] and ts["까닭"] == {"mid:시공량": "까닭"}
|
||||
got = sd.clean({**ts, "입력값": F211}, None, _whole())
|
||||
assert {k: got[k] for k in const.NEW if k in got} == {k: ts[k] for k in const.NEW if k in ts}
|
||||
@@ -0,0 +1,171 @@
|
||||
"""793-1 지킴 — 경쟁사 산근 글 식 파서 · 참조 평가기(`M02_San_*`) · 실무 여덟 파일 금액 그대로.
|
||||
|
||||
STmate = 숨은 G 열 원문 식 → 233/233 블록 · 금액 줄 1,315 · 계표시 줄 695 다 맞음.
|
||||
EST = 보이는 글 → 식 복원 → 222/222 블록 · 돈 · 값 줄 2,901/2,903 · 모호 단가 토큰 9.
|
||||
단가 바뀜 전파 견본 = `_분석_649-G2` 4 장(보통인부 → 180,000).
|
||||
엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만) · 없으면 skip(다른 PC).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
import helper_m02_hopyo_golden as golden
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
from M02_MasterTemplete import M02_San_Est as est
|
||||
from M02_MasterTemplete import M02_San_Eval as ev
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
|
||||
# 파일: (블록, 금액 줄, 계표시 줄, 끝전)
|
||||
STMATE = {
|
||||
"봉화": (62, 349, 184, "비목버림"),
|
||||
"동부청영월": (69, 411, 230, "합버림_큰나머지"),
|
||||
"울진대흥": (47, 261, 128, "비목버림"),
|
||||
"영덕계류": (15, 86, 41, "비목버림"),
|
||||
"울진소광": (40, 208, 112, "비목버림"),
|
||||
}
|
||||
# 파일: (블록, 돈 · 값 줄, 틀린 줄 상한, 모호 토큰)
|
||||
EST = {"북부청임도": (94, 1252, 2, 0), "북부청사방": (67, 849, 0, 7), "거창": (61, 802, 0, 2)}
|
||||
LABOR = ("노임", "보통인부", "", "L")
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _load(tag: str) -> dict:
|
||||
path = golden.FILES[tag]
|
||||
if not path.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {path.name}")
|
||||
return book.load(path)
|
||||
|
||||
|
||||
def _stmate_runs(tag: str) -> list[tuple]:
|
||||
data = _load(tag)
|
||||
out = []
|
||||
for blk in data["블록"]:
|
||||
if not any(ln["G"].strip() for ln in blk["몸"]):
|
||||
continue
|
||||
res = ev.stmate_block(blk, stm.parse_block(blk["몸"]), data["단가"], STMATE[tag][3])
|
||||
out.append((blk, res, ev.stmate_check(blk, res)))
|
||||
return out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(STMATE))
|
||||
def test_stmate_blocks_equal_excel(tag: str) -> None:
|
||||
blocks, amount_lines, total_lines, _ = STMATE[tag]
|
||||
runs = _stmate_runs(tag)
|
||||
assert len(runs) == blocks
|
||||
wrong = [b["코드"] for b, _, c in runs if not c["맞음"]]
|
||||
assert not wrong, wrong
|
||||
assert sum(c["금액줄"] for _, _, c in runs) == amount_lines
|
||||
assert [x for _, _, c in runs for x in c["틀린줄"]] == []
|
||||
assert sum(c["계줄"] for _, _, c in runs) == total_lines
|
||||
assert sum(c["계줄맞음"] for _, _, c in runs) == total_lines
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(EST))
|
||||
def test_est_blocks_equal_excel(tag: str) -> None:
|
||||
blocks, lines, bad_max, vague = EST[tag]
|
||||
data = _load(tag)
|
||||
n, wrong, got_lines, got_bad, got_vague = 0, [], 0, 0, 0
|
||||
for blk in data["블록"]:
|
||||
want = data["총괄"].get(blk["번"])
|
||||
if not want or not (want["T"] or any(want[c] for c in "LMS")):
|
||||
continue
|
||||
parsed = est.parse_block(blk["글"], data["단가"])
|
||||
chk = ev.est_check(parsed, want)
|
||||
n += 1
|
||||
if not chk["맞음"]:
|
||||
wrong.append(blk["번"])
|
||||
got_lines += chk["줄수"]
|
||||
got_bad += len(chk["틀린줄"])
|
||||
got_vague += sum(1 for L in parsed for r in (L.get("역할") or {}).values() if r.get("모호"))
|
||||
assert (n, wrong) == (blocks, [])
|
||||
assert got_lines == lines
|
||||
assert got_bad <= bad_max
|
||||
assert got_vague == vague
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tag, code, labor, before, after",
|
||||
[
|
||||
("봉화", "D00102", "L00007", (34119, 11192, 341), (34697, 11192, 346)),
|
||||
("동부청영월", "D00003", "L00002", (4764, 310, 397), (5097, 309, 397)),
|
||||
("울진소광", "D01489", "L00016", (182223, 230359, 8152), (185067, 230359, 8209)),
|
||||
],
|
||||
)
|
||||
def test_stmate_price_propagates(tag, code, labor, before, after) -> None:
|
||||
data = _load(tag)
|
||||
blk = next(b for b in data["블록"] if b["코드"] == code)
|
||||
nodes = stm.parse_block(blk["몸"])
|
||||
rule = STMATE[tag][3]
|
||||
prices = dict(data["단가"])
|
||||
prices[labor] = {**prices[labor], "T": 180000, "L": 180000}
|
||||
for p, want in ((data["단가"], before), (prices, after)):
|
||||
got = ev.stmate_block(blk, nodes, p, rule)["계"]
|
||||
assert (got["L"], got["M"], got["S"]) == want
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tag, no, after",
|
||||
[
|
||||
("북부청사방", 31, (14877, 5491, 0)),
|
||||
("북부청임도", 2, (22774, 3235, 3830)),
|
||||
("거창", 1, (3485, 832, 1049)),
|
||||
("북부청사방", 3, (20147, 391, 0)),
|
||||
],
|
||||
)
|
||||
def test_est_price_propagates(tag, no, after) -> None:
|
||||
data = _load(tag)
|
||||
blk = next(b for b in data["블록"] if b["번"] == no)
|
||||
parsed = est.parse_block(blk["글"], data["단가"])
|
||||
des = ev.designate(parsed, data["총괄"][no])
|
||||
got = ev.est_block(parsed, des, {LABOR: 180000.0})["금액"]
|
||||
assert (got["L"], got["M"], got["S"]) == after
|
||||
|
||||
|
||||
# ── 엑셀 없이 도는 문법 견본(`_분석_648-B` 3-1 봉화 D00102) ──
|
||||
def test_stmate_grammar_sample() -> None:
|
||||
prices = {
|
||||
"M00081": {"T": 11468, "L": 0, "M": 11468, "S": 0, "meta": {}},
|
||||
"L00007": {"T": 165545, "L": 165545, "M": 0, "S": 0, "meta": {}},
|
||||
"L00099": {"T": 2, "L": 2, "M": 0, "S": 0, "meta": {"UNT": "L%"}},
|
||||
}
|
||||
text = [
|
||||
"'합판(내수) :'&M00081& * 1.03 * 0.327 :=A01",
|
||||
";;Q = 2.5 / 3 =",
|
||||
"'보통인부' &L00007& * [Q] / 4 :=A02",
|
||||
"&PA01M:M& * 11'%' =",
|
||||
"&PA02L:S& * 1'%' = // 공구손료",
|
||||
"(=)",
|
||||
"&L00099& * 2 =",
|
||||
"(-=)",
|
||||
]
|
||||
nodes = stm.parse_block([{"행": i, "G": g} for i, g in enumerate(text)])
|
||||
assert [n["종류"] for n in nodes] == ["참조", "대입", "참조", "율", "율", "계", "참조", "계"]
|
||||
assert nodes[1]["숨김"] and nodes[4]["덧글"] == "공구손료"
|
||||
assert nodes[3]["율"] == {"곱": False, "범위": "A", "번지": ["A01"], "비목": "M", "옮김": "M"}
|
||||
lines, tot = ev.run_stmate(nodes, prices)
|
||||
assert lines[0]["값"]["M"] == 3862.5
|
||||
assert lines[2]["값"]["L"] == 34350.5 # 165,545 × 0.83(Q 반올림) / 4 → 0.1 버림
|
||||
assert lines[3]["값"]["M"] == 424.8 # A01 재료 3,862.5 × 11 %
|
||||
assert lines[4]["값"]["S"] == 343.5 # A02 노무 34,350.5 × 1 % → 경비
|
||||
assert lines[5]["값"] == pytest.approx({"L": 34350.5, "M": 4287.3, "S": 343.5})
|
||||
assert lines[6]["종류"] == "특수" and lines[6]["값"]["L"] == 687.0 # 노무 누계 × 2 %
|
||||
assert lines[7]["값"]["L"] == 687.0 # (-=) 찍되 합에서 뺌
|
||||
assert tot == pytest.approx({"L": 34350.5, "M": 4287.3, "S": 343.5})
|
||||
|
||||
|
||||
def test_est_grammar_sample() -> None:
|
||||
prices = [{"종류": "노임", "이름": "보통인부", "규격": "", "비목": "L", "값": 172068.0}]
|
||||
text = [
|
||||
"보통인부 : 172,068 × 0.09 인 = 15,486.1",
|
||||
"공구손료 : 15,486.1 × 3 % = 464.5",
|
||||
"소 계 : 15,950.6",
|
||||
"노무비 : 15,950.6 × 1 = 15,950",
|
||||
]
|
||||
lines = est.parse_block(text, prices)
|
||||
assert [L.get("종류") for L in lines] == ["단가줄", "참조줄", "값줄", "참조줄"]
|
||||
assert lines[3]["규칙"] == "버림0" and lines[2]["숨은"]["갈래"] == "합"
|
||||
val = ev.run_est(lines, {LABOR: 180000.0})
|
||||
assert val[0] == 16200.0 and val[1] == 486.0 and val[3] == 16686.0
|
||||
Reference in New Issue
Block a user