Files
Aislo/resources/tester/test_793_2_san_emit.py
T
eomsangdonandClaude Opus 5.5 193c3992a9 feat(793-2a): 경쟁사 산근 식 → 엔진 로직 줄 꼴 내보내기 · 여덟 파일 엔진 금액 = 엑셀
- 새 M02_San_Emit.py — 793-1 줄 나무 → 로직 JSON(중간 · 호표 값묶음 · 덧줄 · 번지 · 단계 · 합밖 · 자리 · 끝전 · 조정 · 글줄 · 가져옴 검산 · 계약)
- 자원 이음 열쇠(품명 · 규격 · 단위 · 경쟁사 코드 · 1차 줄 판정 M01 키) · reprice 단가 전파
- STmate 누계 % 밑 = 그 줄 앞까지(뒤 호표 빼고 · 안 빠진 제외 단계 더함) · 율 범위 C = 앞 줄 부름 합
- 지킴 시험 test_793_2 — STmate 233 · EST 222 블록 엔진 셈 = 엑셀 = 참조 평가기 · 줄 4,218 · 단계 695 · 전파 견본 7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:37:26 +09:00

250 lines
11 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 · EST 222 블록을 로직 JSON 으로 내보내 `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_Est as est
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, "비목버림"),
"동부청영월": (69, "합버림_큰나머지"),
"울진대흥": (47, "비목버림"),
"영덕계류": (15, "비목버림"),
"울진소광": (40, "비목버림"),
}
EST = {"북부청임도": 94, "북부청사방": 67, "거창": 61}
# 엑셀 글 식이 보인 값과 안 맞는 줄(793-1 틀린 줄 2) — 보인 값으로 얼림 · 블록 금액은 같음
EST_FROZEN = {"북부청임도": {27: ["37"], 33: ["51"]}}
K = ("노무비", "재료비", "경비", "계")
LABOR = {"종류": "노임", "이름": "보통인부", "규격": "", "열쇠비목": "L"}
def engine(logic: dict) -> dict:
master = mf.Master({"로직.json": {"그룹": "로직", "줄": [logic]}})
return mcut.engine(master, logic["키"], mf.run(master, logic["키"], {}))
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}")
return book.load(path)
def _stmate(tag: str, blk: dict) -> tuple[list, dict]:
nodes = stm.parse_block(blk["몸"])
return nodes, em.stmate(blk, nodes, _load(tag)["단가"], STMATE[tag][1], "GX990001")
def _lines_equal(logic: dict, got: dict) -> list:
"""줄마다 엔진 비목 금액 = 엑셀 · 단계 = 계표시 줄."""
bad = []
for item, line in zip([*logic["호표"], *logic["덧줄"]], got["줄"], strict=True):
for k, want in item["검산"].items():
if abs(float(line["비목"].get(k) or 0) - want) >= 0.05:
bad.append((item["이름"], k, line["비목"].get(k), want))
for step in got.get("단계", []):
want = logic["가져옴"]["단계"][step["이름"]]["검산"]
if any(abs(float(step[k]) - want[k]) >= 0.15 for k in want):
bad.append((step["이름"], want))
return bad
@pytest.mark.parametrize("tag", list(STMATE))
def test_stmate_engine_equals_excel(tag: str) -> None:
data = _load(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, logic = _stmate(tag, blk)
got = engine(logic)
ref = ev.stmate_block(blk, nodes, data["단가"], STMATE[tag][1])["계"]
want = logic["가져옴"]["검산"]
same = money(got) == tuple(round(want[k]) for k in K)
same &= money(got) == (ref["L"], ref["M"], ref["S"], ref["T"])
bad = _lines_equal(logic, got) + ms.check(logic, logic["키"]) + logic["가져옴"]["표지"]
if deal := logic["가져옴"].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]))
lines += len(logic["호표"]) + len(logic["덧줄"])
steps += len(got.get("단계", []))
assert (n, wrong) == (STMATE[tag][0], [])
assert lines >= 1 and steps >= 1
@pytest.mark.parametrize("tag", list(EST))
def test_est_engine_equals_excel(tag: str) -> None:
data = _load(tag)
n, wrong = 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
n += 1
parsed = est.parse_block(blk["글"], data["단가"])
logic = em.est(blk, parsed, want, "GX990002")
got = engine(logic)
ref = ev.est_block(parsed, ev.designate(parsed, want))["금액"]
same = money(got) == (want["L"], want["M"], want["S"], want["T"])
same &= money(got) == (ref["L"], ref["M"], ref["S"], ref["T"])
frozen = [m.split(":")[0] for m in logic["가져옴"]["표지"]]
bad = _lines_equal(logic, got) + ms.check(logic, logic["키"])
if not same or bad or frozen != EST_FROZEN.get(tag, {}).get(blk["번"], []):
wrong.append((blk["번"], money(got), bad[:3], frozen))
assert (n, wrong) == (EST[tag], [])
@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)
_, logic = _stmate(tag, blk)
assert money(engine(logic))[:3] == before
assert em.reprice(logic, {"코드": labor}, {"L": 180000}) >= 1
assert money(engine(logic))[:3] == after
@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)
logic = em.est(blk, est.parse_block(blk["글"], data["단가"]), data["총괄"][no], "GX990002")
assert em.reprice(logic, LABOR, {"L": 180000.0}) >= 1
assert money(engine(logic))[:3] == after
def test_link_keys_m01() -> None:
"""자원 이음 열쇠 — 엑셀 품명 · 규격 · 단위 · 경쟁사 코드 · 1차 줄 판정 M01 키."""
tag = "봉화"
blk = next(b for b in _load(tag)["블록"] if b["코드"] == "D00102")
logic = em.stmate(
blk, stm.parse_block(blk["몸"]), _load(tag)["단가"], "비목버림", "GX990001", True
)
labor = next(x for x in logic["호표"] if x["이름"] == "보통인부")
assert labor["자원"] | {"M01": labor["자원"].get("M01")} == {
"꼴": "STmate",
"갈래": "인력",
"코드": "L00007",
"이름": "보통인부",
"규격": None,
"단위": "인",
"단가": {"L": 165545, "M": 0, "S": 0},
"M01": "LB000002",
}
assert labor["값묶음"] == {"노무비": 165545, "재료비": 0, "경비": 0}
# ── 엑셀 없이 도는 문법 견본(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}
logic = em.stmate(block, nodes, prices, "비목버림", "GX990003")
assert [x["이름"] for x in logic["중간"]] == ["q", "q_2"] # 다시 대입 = 새 이름
assert logic["중간"][0] == {
"이름": "q",
"식": "(2.5 / 3)",
"표시": 2,
"원문": {"행": 1, "글": text[1]},
}
assert ms.check(logic, "견본") == [] and logic["가져옴"]["표지"] == []
got = engine(logic)
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 %
def test_est_grammar_sample_engine() -> 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)
want = {"T": 15950.0, "L": 15950.0, "M": 0.0, "S": 0.0}
logic = em.est({"번": 1, "이름": "견본", "단위": "㎡"}, lines, want, "GX990004")
assert [x["식"] for x in logic["덧줄"]] == [
"((줄.'보통인부' * 3) / 100)",
"줄.'보통인부' + 공구손료",
"소계",
]
assert logic["덧줄"][2]["자리"] == {"방법": "버림", "자리": 0}
assert [x.get("합밖") for x in (*logic["호표"], *logic["덧줄"])] == [True, True, True, None]
assert money(engine(logic)) == (15950, 0, 0, 15950)
em.reprice(logic, LABOR, {"L": 180000.0})
assert money(engine(logic)) == (16686, 0, 0, 16686) # 793-1 run_est 전파와 같음