Files
Aislo/resources/tester/test_793_2_san_emit.py
T
eomsangdonandClaude Opus 5.5 61bb15fbde refactor(812-1): 옛 엑셀 엔진 걷기 2 · 실무 11 금액 100 % · NativeEst 자리 dict 묶음 고침 · 옛 시험 정리
- M02_Doc_NativeEst.triple: 세 줄 수량 식이 같은지 = repr 로 맞댐(자리 규칙 dict 가 set 에 안 들어가 TypeError — 엑셀 줄은 보인 값이 반올림 자리를 타 원파일에 없던 꼴 · 812-3 끝나 브레인이 맡김)
- 엑셀 읽기기: 절 소계 덧셈이 뺀 오타 줄 = 비목 없음(블록 금액도 그래야 맞을 때만) · 세 줄 계 금액 칸 비목 하나 = 옮김 · 소계 61 = 금액 칸 비목 하나 · 「.1」 수 · 총괄표 당초 칸 표지 · STmate 엑셀 줄 금액 · 계 꼬리(검산 줄)
- 화면: 가져온 식 규격 칸 꼬리표 = 원파일 · 엑셀 갈림(line_stack_sourced)
- 시험: 793_1 지움(옛 EST · STmate 엑셀 글 추정 엔진 · 812_1 골든이 대신) · 793_2_san_emit EST 시험 지움(STmate 남김) · 797_1a 옛 표지 ① ② · 줄 단위 합격 · 표본 밖 수 · 버림1 감시 지움(옛 엔진) · 810_1 (c) 엑셀 엔진 대조 지움 · 793_2 실무 블록 · 793_3b 끝전 = 새 길 · 812_1 금액 엇갈림 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:38:12 +09:00

225 lines
9.7 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.
"""793-2a 지킴 — 경쟁사 산근 → 우리 엔진 로직 줄 꼴(`M02_San_Emit`) · 엔진 셈 = 엑셀.
STmate 233 블록을 로직 JSON 으로(EST 엑셀 = 812-1 엑셀 읽기기 → 원파일 길 · `test_812_1` 이 지킴) 내보내 `master_formula.run` →
`master_cut.engine` 으로 셈 — 노 · 재 · 경 · 계 = 엑셀(STmate 계 꼬리 · EST 총괄표) =
참조 평가기(`M02_San_Eval`) · 줄마다(STmate 금액 줄 · EST 돈 · 값 줄) · 계표시(단계) 같음.
단가 전파 견본 = `_분석_649-G2` 4 장(보통인부 → 180,000) · 계약단가(낙찰률)는 B09 몫 — 엔진 계 ×
낙찰률 원 버림 = 엑셀 머리만 확인. 엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만 · 없으면 skip).
"""
from __future__ import annotations
import importlib
import math
from functools import lru_cache
import helper_m02_hopyo_golden as golden
import pytest
from M01_MasterData import M01_MasterData_Store as m01 # noqa: F401 — scripts 길을 둠
from M02_MasterTemplete import M02_San_Book as book
from M02_MasterTemplete import M02_San_Emit as em
from M02_MasterTemplete import M02_San_Eval as ev
from M02_MasterTemplete import M02_San_Stmate as stm
mf = importlib.import_module("master_formula")
mcut = importlib.import_module("master_cut")
ms = importlib.import_module("master_formula_sum")
STMATE = { # 파일: (블록, 금액 줄, 계표시 줄, 끝전)
"봉화": (62, 349, 184, "비목버림"),
"동부청영월": (69, 411, 230, "합버림_큰나머지"),
"울진대흥": (47, 261, 128, "비목버림"),
"영덕계류": (15, 86, 41, "비목버림"),
"울진소광": (40, 208, 112, "비목버림"),
}
K = ("노무비", "재료비", "경비", "계")
LABOR = {"종류": "노임", "이름": "보통인부", "규격": "", "열쇠비목": "L"}
def engine(out: dict) -> dict:
"""내보낸 로직 → 엔진 셈 — 단가 줄은 SK 서버처럼 값을 얹음(요소 대신 값묶음 {비목: 단가})."""
logic = {**out["로직"], "키": "GX990001"}
items = []
for item in logic["호표"]:
one = {k: v for k, v in item.items() if k not in ("이음", "자원", "단가", "규격")}
if "단가" in item:
money = dict.fromkeys(K[:3], 0) | {item["비목"]: item["단가"]}
one |= {"종류": "로직", "값묶음": money}
del one["비목"]
items.append(one | {"요소": None})
logic["호표"] = items
master = mf.Master({"로직.json": {"그룹": "로직", "줄": [logic]}})
return mcut.engine(master, logic["키"], mf.run(master, logic["키"], {}))
def hard(out: dict) -> list[str]:
"""못 옮김 표지만(「참고 · …」 사람 확인 표지 · 797-1a 는 뺌 — test_797_1a 가 지킴)."""
return [m for m in out["원본"]["표지"] if em.NOTE not in m]
def money(got: dict) -> tuple:
return tuple(int(got[k]) for k in K)
@lru_cache(maxsize=8)
def _load(tag: str) -> dict:
path = golden.FILES[tag]
if not path.exists():
pytest.skip(f"실무 엑셀 없음 {path.name}")
sheets = book.sheets(path.name, path.read_bytes())
return {"블록": book.stmate_blocks(sheets), "단가": book.stmate_prices(sheets)}
def _stmate(tag: str, blk: dict) -> tuple[list, dict]:
nodes = stm.parse_block(blk["몸"])
return nodes, em.stmate(blk, nodes, _load(tag)["단가"], STMATE[tag][3])
def _lines_equal(out: dict, got: dict) -> tuple[list, int, int]:
"""원본 글줄마다 엔진 줄 금액(계) = 엑셀 · 단계 = 계표시 줄 → (틀린 것, 줄 수, 단계 수)."""
lines = {x["이름"]: x for x in got["줄"]}
steps = {x["이름"]: x for x in got.get("단계", [])}
bad, n_line, n_step = [], 0, 0
for g in out["원본"]["글줄"]:
if g.get("값") is None or not g.get("자리") or g["자리"].startswith("mid:"):
continue
if g["자리"].startswith("단계:"):
n_step += 1
have = float(steps[g["자리"][3:]]["계"])
tol = 0.15
else:
n_line += 1
have = sum(float(lines[n]["금액"] or 0) for n in g.get("자리들") or [g["자리"]])
tol = 0.05
if abs(have - g["값"]) >= tol:
bad.append((g["행"], g["글"].strip(), have, g["값"]))
return bad, n_line, n_step
@pytest.mark.parametrize("tag", list(STMATE))
def test_stmate_engine_equals_excel(tag: str) -> None:
data = _load(tag)
blocks, n_lines, n_steps, rule = STMATE[tag]
n, wrong, lines, steps = 0, [], 0, 0
for blk in data["블록"]:
if not any(ln["G"].strip() for ln in blk["몸"]):
continue
n += 1
nodes, out = _stmate(tag, blk)
got = engine(out)
ref = ev.stmate_block(blk, nodes, data["단가"], rule)["계"]
want = out["원본"]["엑셀금액"]
same = money(got) == tuple(round(want[k]) for k in K)
same &= money(got) == (ref["L"], ref["M"], ref["S"], ref["T"])
bad, a, b = _lines_equal(out, got)
bad += ms.check(out["로직"], blk["코드"]) + hard(out)
lines, steps = lines + a, steps + b
if deal := out["원본"].get("계약"): # B09 몫 — 엔진 계 × 낙찰률 원 버림 = 엑셀 머리
cut = [math.floor(round(int(got[k]) * deal["낙찰률"] / 100, 6)) for k in K[:3]]
same &= cut == [deal["금액"][k] for k in K[:3]]
if not same or bad:
wrong.append((blk["코드"], money(got), bad[:3]))
assert (n, wrong) == (blocks, [])
assert (lines, steps) == (n_lines, n_steps)
@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:
blk = next(b for b in _load(tag)["블록"] if b["코드"] == code)
_, out = _stmate(tag, blk)
assert money(engine(out))[:3] == before
assert em.reprice(out["로직"], {"코드": labor}, {"L": 180000}) >= 1
assert money(engine(out))[:3] == after
def test_link_keys_m01() -> None:
"""호표 줄 = 793-2 꼴(종류 · 단가 · 이음 = 1차 줄 판정 M01 키) · 덧칸 자원 · 원본."""
tag = "봉화"
blk = next(b for b in _load(tag)["블록"] if b["코드"] == "D00102")
out = em.stmate(blk, stm.parse_block(blk["몸"]), _load(tag)["단가"], "비목버림", True)
labor = next(x for x in out["로직"]["호표"] if x["이름"] == "보통인부")
assert {k: v for k, v in labor.items() if k != "자원"} == {
"이름": "보통인부",
"규격": "",
"단위": "인",
"수량": "(2 / 50)",
"종류": "인력",
"비목": "노무비",
"단가": 165545,
"이음": "LB000002",
"번지": "A02",
}
assert labor["자원"] == {
"꼴": "STmate",
"갈래": "인력",
"코드": "L00007",
"이름": "보통인부",
"규격": None,
"단위": "인",
"단가": {"L": 165545, "M": 0, "S": 0},
"비목": "L",
"옮김": None,
}
src = out["원본"]
assert {k: src[k] for k in ("시트", "갈래", "코드", "행")} == {
"시트": "단가산출근거",
"갈래": "STmate",
"코드": "D00102",
"행": [2912, 2937],
}
assert src["엑셀금액"] == {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652}
assert next(g for g in src["글줄"] if g.get("자리") == "보통인부")["값"] == 6621.8
# ── 엑셀 없이 도는 문법 견본(793-1 `test_stmate_grammar_sample` + 다시 대입 · 뒤 줄) ──
def test_stmate_grammar_sample_engine() -> 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 =",
"(-=)",
"'별도' &M00081& * 2 =",
"Q = 1 / 3 =",
"&L00099& * [Q] =",
"'뒤 인부' &L00007& * 0.01 =",
]
rows = [{"행": i, "G": g} for i, g in enumerate(text)]
nodes = stm.parse_block(rows)
block = {"코드": "D00001", "이름": "견본", "꼬리": [], "몸": rows}
block["머리"] = {"A": "", "B": "", "C": 0, "D": 0, "E": 0, "F": 0}
out = em.stmate(block, nodes, prices, "비목버림")
logic = out["로직"]
assert logic["중간"] == [
{"이름": "q", "식": "(2.5 / 3)", "표시": 2},
{"이름": "q_2", "식": "(1 / 3)", "표시": 2}, # 다시 대입 = 새 이름
]
assert ms.check(logic, "견본") == [] and out["원본"]["표지"] == []
got = engine(out)
lines, tot = ev.run_stmate(nodes, prices)
want = ev.block_total(tot, "비목버림")
assert money(got) == (want["L"], want["M"], want["S"], want["T"])
by = {x["이름"]: x for x in got["줄"]}
assert by["보통인부"]["비목"]["노무비"] == 34350.5 # 165,545 × 0.83 / 4
assert by["공구"]["비목"]["노무비"] == 687 # 노무 누계 × 2 % · (-=) 로 합에서 뺌
assert logic["덧줄"][-1]["식"] == "(노무비 - 줄.'뒤 인부') * ((q_2)) / 100" # 뒤 줄을 뺀 누계
assert (
float(by["공구_2"]["비목"]["노무비"]) == lines[10]["값"]["L"] == 113.3
) # 34,350.5 × 0.33 %