Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1

This commit is contained in:
2026-11-05 17:44:11 +09:00
2 changed files with 216 additions and 167 deletions
+109 -70
View File
@@ -27,21 +27,32 @@ 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, "비목버림"),
STMATE = { # 파일: (블록, 금액 줄, 계표시 줄, 끝전)
"봉화": (62, 349, 184, "비목버림"),
"동부청영월": (69, 411, 230, "합버림_큰나머지"),
"울진대흥": (47, 261, 128, "비목버림"),
"영덕계류": (15, 86, 41, "비목버림"),
"울진소광": (40, 208, 112, "비목버림"),
}
EST = {"북부청임도": 94, "북부청사방": 67, "거창": 61}
EST = {"북부청임도": (94, 1252), "북부청사방": (67, 849), "거창": (61, 802)} # (블록, 돈 · 값 줄)
# 엑셀 글 식이 보인 값과 안 맞는 줄(793-1 틀린 줄 2) — 보인 값으로 얼림 · 블록 금액은 같음
EST_FROZEN = {"북부청임도": {27: ["37"], 33: ["51"]}}
EST_FROZEN = {"북부청임도": {27: 1, 33: 1}}
K = ("노무비", "재료비", "경비", "계")
LABOR = {"종류": "노임", "이름": "보통인부", "규격": "", "열쇠비목": "L"}
def engine(logic: dict) -> dict:
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["키"], {}))
@@ -60,69 +71,79 @@ def _load(tag: str) -> dict:
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")
return nodes, em.stmate(blk, nodes, _load(tag)["단가"], STMATE[tag][3])
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
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, logic = _stmate(tag, blk)
got = engine(logic)
ref = ev.stmate_block(blk, nodes, data["단가"], STMATE[tag][1])["계"]
want = logic["가져옴"]["검산"]
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 = _lines_equal(logic, got) + ms.check(logic, logic["키"]) + logic["가져옴"]["표지"]
if deal := logic["가져옴"].get("계약"): # B09 몫 — 엔진 계 × 낙찰률 원 버림 = 엑셀 머리
bad, a, b = _lines_equal(out, got)
bad += ms.check(out["로직"], blk["코드"]) + 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]))
lines += len(logic["호표"]) + len(logic["덧줄"])
steps += len(got.get("단계", []))
assert (n, wrong) == (STMATE[tag][0], [])
assert lines >= 1 and steps >= 1
assert (n, wrong) == (blocks, [])
assert (lines, steps) == (n_lines, n_steps)
@pytest.mark.parametrize("tag", list(EST))
def test_est_engine_equals_excel(tag: str) -> None:
data = _load(tag)
n, wrong = 0, []
n, wrong, lines = 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
n += 1
parsed = est.parse_block(blk["글"], data["단가"])
logic = em.est(blk, parsed, want, "GX990002")
got = engine(logic)
out = em.est(blk, parsed, want)
got = engine(out)
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], [])
bad, a, _ = _lines_equal(out, got)
bad += ms.check(out["로직"], str(blk["번"]))
lines += a
frozen = len(out["원본"]["표지"])
if not same or bad or frozen != EST_FROZEN.get(tag, {}).get(blk["번"], 0):
wrong.append((blk["번"], money(got), bad[:3], out["원본"]["표지"]))
assert (n, wrong, lines) == (EST[tag][0], [], EST[tag][1])
@pytest.mark.parametrize(
@@ -135,10 +156,10 @@ def test_est_engine_equals_excel(tag: str) -> None:
)
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
_, out = _stmate(tag, blk)
assert money(engine(out))[:3] == before
assert em.reprice(out["로직"], {"코드": labor}, {"L": 180000}) >= 1
assert money(engine(out))[:3] == after
@pytest.mark.parametrize(
@@ -153,20 +174,29 @@ def test_stmate_price_propagates(tag, code, labor, before, after) -> None:
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
out = em.est(blk, est.parse_block(blk["글"], data["단가"]), data["총괄"][no])
assert em.reprice(out["로직"], LABOR, {"L": 180000.0}) >= 1
assert money(engine(out))[:3] == after
def test_link_keys_m01() -> None:
"""자원 이음 열쇠 — 엑셀 품명 · 규격 · 단위 · 경쟁사 코드 · 1차 줄 판정 M01 키."""
"""호표 줄 = 793-2 꼴(종류 · 단가 · 이음 = 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")} == {
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",
@@ -174,12 +204,21 @@ def test_link_keys_m01() -> None:
"규격": None,
"단위": "인",
"단가": {"L": 165545, "M": 0, "S": 0},
"M01": "LB000002",
"비목": "L",
"옮김": None,
}
assert labor["값묶음"] == {"노무비": 165545, "재료비": 0, "경비": 0}
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` 그대로) ──
# ── 엑셀 없이 도는 문법 견본(793-1 `test_stmate_grammar_sample` + 다시 대입 · 뒤 줄) ──
def test_stmate_grammar_sample_engine() -> None:
prices = {
"M00081": {"T": 11468, "L": 0, "M": 11468, "S": 0, "meta": {}, "이름": "합판(내수)"},
@@ -202,18 +241,16 @@ def test_stmate_grammar_sample_engine() -> None:
]
rows = [{"행": i, "G": g} for i, g in enumerate(text)]
nodes = stm.parse_block(rows)
block = {"코드": "D00001", "이름": "견본", "머리": {}, "꼬리": [], "몸": 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)
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"])
@@ -236,14 +273,16 @@ def test_est_grammar_sample_engine() -> None:
]
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")
out = em.est({"번": 1, "이름": "견본", "단위": "㎡"}, lines, want)
logic = out["로직"]
assert [x["식"] for x in logic["덧줄"]] == [
"((줄.'보통인부' * 3) / 100)",
"줄.'보통인부' + 공구손료",
"소계",
]
assert logic["호표"][0]["종류"] == "인력" and logic["호표"][0]["단가"] == 172068
assert logic["덧줄"][2]["자리"] == {"방법": "버림", "자리": 0}
assert [x.get("합밖") for x in (*logic["호표"], *logic["덧줄"])] == [True, True, True, None]
assert money(engine(logic)) == (15950, 0, 0, 15950)
assert money(engine(out)) == (15950, 0, 0, 15950)
em.reprice(logic, LABOR, {"L": 180000.0})
assert money(engine(logic)) == (16686, 0, 0, 16686) # 793-1 run_est 전파와 같음
assert money(engine(out)) == (16686, 0, 0, 16686) # 793-1 run_est 전파와 같음