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>
This commit is contained in:
@@ -64,7 +64,7 @@ export function buildSourced(
|
||||
if (guess) f.root.dataset.guess = mark;
|
||||
return guess
|
||||
? cell(f.root, lt("Guess"), "warning", lt("Guess_Hint"))
|
||||
: cell(f.root, lt("Src_Native"), "info", lt("Src_Hint"));
|
||||
: cell(f.root, lt(head.원파일 ? "Src_Native" : "Src_Excel"), "info", lt("Src_Hint")); // 812-1 엑셀 읽기기 = 「엑셀」
|
||||
};
|
||||
if (head.규격) out.push(ro(lt("Spec"), head.규격, "", false));
|
||||
for (const g of head.추정 ?? [])
|
||||
|
||||
@@ -52,7 +52,10 @@ def pack(got: dict, name: str) -> dict[str, Any]:
|
||||
out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []}
|
||||
out |= {"엑셀": True} if got.get("엑셀") else {} # 812-1 엑셀 읽기기(`M02_San_Xlsx`) — 같은 꼴
|
||||
head = ("코드", "번", "이름", "규격", "단위", "저장")
|
||||
if got.get("엑셀"): # 엑셀 읽기기 — 줄 행 = 시트 행(원문 카드 · 옛 엑셀 길과 같은 자리)
|
||||
if got.get(
|
||||
"엑셀"
|
||||
): # 엑셀 읽기기 — 줄 행 = 시트 행(원문 카드 · 옛 엑셀 길과 같은 자리) · 블록 표지
|
||||
head += ("표지", "몸", "꼬리")
|
||||
got = {**got, "산근": [_rows(b) for b in got["산근"]]}
|
||||
if got["꼴"] == "EST":
|
||||
out["산근"] = [
|
||||
@@ -106,27 +109,29 @@ def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]:
|
||||
names = {b["코드"]: n for n, b in numbers(saved).items()}
|
||||
out = ne.block(blk, int(no), keys, saved["파일"], names)
|
||||
out["원본"] |= {"원코드": blk["코드"]} | ({"규격": blk["규격"]} if blk.get("규격") else {})
|
||||
return _excel(saved, out)
|
||||
return _excel(saved, out, blk)
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
|
||||
nodes = [{"행": ln["행"], "글": ln["글"], **stm.parse_line(ln["글"])} for ln in blk["줄"]]
|
||||
saved_amt = blk.get("저장") or {}
|
||||
head = {"C": saved_amt.get("T"), "D": saved_amt.get("L"), "E": saved_amt.get("M")}
|
||||
head["F"] = saved_amt.get("S")
|
||||
block = {"코드": blk["코드"], "이름": blk.get("이름"), "몸": [], "꼬리": [], "머리": head}
|
||||
block = {"코드": blk["코드"], "이름": blk.get("이름"), "머리": head}
|
||||
block |= {"몸": blk.get("몸") or [], "꼬리": blk.get("꼬리") or []} # 엑셀 = 줄 금액 · 계 꼬리
|
||||
out = em.stmate(block, nodes, saved["단가"], saved["끝전"], keys, saved["파일"])
|
||||
marks = sum(1 for one in out["원본"]["글줄"] if one.get("표지"))
|
||||
sure = len(out["로직"]["호표"]) + len(out["로직"].get("덧줄") or []) + len(out["로직"]["중간"])
|
||||
out["원본"] |= {"원파일": True, "갈래": "STmate", "확정": {"확정": sure, "표지": marks}}
|
||||
return _excel(saved, out)
|
||||
return _excel(saved, out, blk)
|
||||
|
||||
|
||||
def _excel(saved: dict, out: dict) -> dict[str, Any]:
|
||||
def _excel(saved: dict, out: dict, blk: dict) -> dict[str, Any]:
|
||||
"""엑셀 읽기기 꼴(812-1) — 원본 `원파일` 거짓 · `엑셀` 참(화면 꼬리표 「엑셀」 · 원코드 = 단산 번)."""
|
||||
if saved.get("엑셀"):
|
||||
out["원본"] |= {"원파일": False, "엑셀": True}
|
||||
out["원본"] |= {"시트": "단가산출"} if saved["꼴"] == "EST" else {}
|
||||
out["원본"].pop("원코드", None)
|
||||
out["원본"]["표지"] += [m for m in blk.get("표지") or [] if m.startswith("참고 · ")]
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ class Block:
|
||||
"""기계 · 산근 · 견적 세 줄 → 로직 값묶음 한 줄(수량 식 · 자리 같을 때) · 아니면 줄마다."""
|
||||
got = [self.money(ln) for ln in rows]
|
||||
parts = [ln["비목"] for ln in rows]
|
||||
same = all(got) and len({g for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||||
same = all(got) and len({repr(g) for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||||
if not same:
|
||||
for ln, g in zip(rows, got):
|
||||
self.machine_line(ln, g)
|
||||
|
||||
@@ -141,8 +141,9 @@ def resources(sheets: dict[str, list]) -> dict[str, list[dict[str, Any]]]:
|
||||
got = []
|
||||
for one in _table(sheets.get(sheet) or [], ()):
|
||||
r = list(one["줄"]) + [None] * 30
|
||||
vals = {k: float(v or 0) for k, v in book.applied(r).items()}
|
||||
got.append({**_drop(one), "표": sheet, "값": vals})
|
||||
vals, note = book.applied_how(r)
|
||||
vals = {k: float(v or 0) for k, v in vals.items()}
|
||||
got.append({**_drop(one), "표": sheet, "값": vals} | ({"칸": note} if note else {}))
|
||||
out[kind] = got
|
||||
return out
|
||||
|
||||
|
||||
@@ -300,6 +300,45 @@ class Lines:
|
||||
self.plain(ln)
|
||||
return self.done(ln)
|
||||
|
||||
def sections(self, saved: dict) -> None:
|
||||
"""절 소계(가 · 나 … 머리 아래 합계 줄 「노무비 : a + b = X」)가 뺀 줄 하나 = 합에 안 든 줄(비목 없음) —
|
||||
절(앞 숫자 머리부터) 안 그 비목 줄 결과 합 − X 가 꼭 한 줄 값이고 나머지 줄 값은 모두 덧셈 글에 있을
|
||||
때만 · 블록 금액도 그 줄을 빼야 맞을 때만(엑셀 글 덧셈 · 원파일 SCheck 0)."""
|
||||
lines, start = self.lines, 0
|
||||
for i, ln in enumerate(lines):
|
||||
text = ln["보임"]
|
||||
if ln["단"] == 1 and re.match(r"^\s*\d", text):
|
||||
start = i
|
||||
if not (ln["합계"] and ln["비목"] in ("L", "M", "S") and ln["결과"] and "+" in text):
|
||||
continue
|
||||
head = next((k for k in range(i - 1, start - 1, -1) if lines[k]["단"] == 1), None)
|
||||
if head is None or not re.match(r"^\s*[가-하]", lines[head]["보임"]):
|
||||
continue
|
||||
part, want = ln["비목"], ln["결과"][-1]
|
||||
kids = [
|
||||
x for x in lines[start:head] if x["비목"] == part and x["결과"] and not x["합계"]
|
||||
]
|
||||
kids += [
|
||||
x for x in lines[start:head] if x["비목"] == part and x["줄참조"] and not x["결과"]
|
||||
]
|
||||
value = {id(x): (x["결과"] or x["줄참조"])[-1] for x in kids}
|
||||
extra = sum(value.values()) - want
|
||||
odd = [x for x in kids if abs(value[id(x)] - extra) < 0.5]
|
||||
colon = _COLON.search(text)
|
||||
adds = [v for _, _, v in _nums(text, colon.end() if colon else 0, text.rfind("="))]
|
||||
rest = [value[id(x)] for x in kids if odd and x is not odd[0]]
|
||||
listed = all(any(_close(v, a) for a in adds) for v in rest)
|
||||
if len(odd) != 1 or abs(extra) < 0.5 or not listed:
|
||||
continue
|
||||
every = sum(
|
||||
(x["결과"] or x["줄참조"])[-1]
|
||||
for x in lines
|
||||
if x["비목"] == part and not x["합계"] and (x["결과"] or x["줄참조"])
|
||||
)
|
||||
block = float(saved.get(part) or 0) # 블록 금액이 그 줄을 빼야 맞을 때만
|
||||
if abs(every - block) >= 1 and abs(every - extra - block) < 1:
|
||||
odd[0]["비목"] = None
|
||||
|
||||
def unused(self) -> None:
|
||||
"""머리 아닌 줄의 기계 이름 — 아래 세 줄이 안 쓰면 글 줄(설명 속 장비 이름)."""
|
||||
for h in self.heads:
|
||||
@@ -597,10 +636,13 @@ def read(sheets: dict[str, list]) -> dict[str, Any]:
|
||||
}
|
||||
if not one:
|
||||
tags.append(f"{sx.NOTE}총괄표에 단산{b['번']} 없음")
|
||||
elif one.get("칸"): # 총괄표 당초 / 적용 칸(797-1a)
|
||||
tags.append(f"참고 · {one['칸']}")
|
||||
work = Lines(look, tags)
|
||||
for src in b["줄"]:
|
||||
work.run(src)
|
||||
work.unused()
|
||||
work.sections(head["저장"] or {})
|
||||
lines = []
|
||||
for ln in work.lines:
|
||||
marks = ln.pop("_marks")
|
||||
|
||||
@@ -59,7 +59,14 @@ def read(sheets: dict[str, list]) -> dict[str, Any]:
|
||||
head = {"코드": blk["코드"], "이름": blk.get("이름") or me.get("이름")}
|
||||
head |= {"규격": me.get("규격"), "단위": me.get("단위"), "저장": _amt(blk["머리"])}
|
||||
_, tot = ev.run_stmate([ln["풀이"] for ln in lines], prices)
|
||||
out["산근"].append({**head, "줄": lines, "줄합": tot, "표지": tags, "엑셀행": blk["행"]})
|
||||
body = [{k: ln.get(k) for k in ("행", "B", "C", "D", "E", "F")} for ln in blk["몸"]]
|
||||
out["산근"].append(
|
||||
{**head, "줄": lines, "줄합": tot, "표지": tags, "엑셀행": blk["행"]}
|
||||
| {
|
||||
"몸": body,
|
||||
"꼬리": blk["꼬리"],
|
||||
} # 엑셀 줄 금액 · 계 꼬리(검산 · `M02_San_Emit.stmate`)
|
||||
)
|
||||
rule, got = vote(out["산근"], prices)
|
||||
out["옵션"] = {"SAN_TYP": RULE[rule]}
|
||||
out["내역"] = None
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
"""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
|
||||
@@ -1,6 +1,6 @@
|
||||
"""793-2a 지킴 — 경쟁사 산근 → 우리 엔진 로직 줄 꼴(`M02_San_Emit`) · 엔진 셈 = 엑셀.
|
||||
|
||||
여덟 파일 STmate 233 · EST 222 블록을 로직 JSON 으로 내보내 `master_formula.run` →
|
||||
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 몫 — 엔진 계 ×
|
||||
@@ -19,7 +19,6 @@ 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
|
||||
|
||||
@@ -34,9 +33,6 @@ STMATE = { # 파일: (블록, 금액 줄, 계표시 줄, 끝전)
|
||||
"영덕계류": (15, 86, 41, "비목버림"),
|
||||
"울진소광": (40, 208, 112, "비목버림"),
|
||||
}
|
||||
EST = {"북부청임도": (94, 1252), "북부청사방": (67, 849), "거창": (61, 802)} # (블록, 돈 · 값 줄)
|
||||
# 엑셀 글 식이 보인 값과 안 맞는 줄(793-1 틀린 줄 2) — 보인 값으로 얼림 · 블록 금액은 같음
|
||||
EST_FROZEN = {"북부청임도": {27: 1, 33: 1}}
|
||||
K = ("노무비", "재료비", "경비", "계")
|
||||
LABOR = {"종류": "노임", "이름": "보통인부", "규격": "", "열쇠비목": "L"}
|
||||
|
||||
@@ -71,7 +67,8 @@ def _load(tag: str) -> dict:
|
||||
path = golden.FILES[tag]
|
||||
if not path.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {path.name}")
|
||||
return book.load(path)
|
||||
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]:
|
||||
@@ -127,30 +124,6 @@ def test_stmate_engine_equals_excel(tag: str) -> None:
|
||||
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, 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["단가"])
|
||||
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"])
|
||||
bad, a, _ = _lines_equal(out, got)
|
||||
bad += ms.check(out["로직"], str(blk["번"]))
|
||||
lines += a
|
||||
frozen = len(hard(out))
|
||||
if not same or bad or frozen != EST_FROZEN.get(tag, {}).get(blk["번"], 0):
|
||||
wrong.append((blk["번"], money(got), bad[:3], hard(out)))
|
||||
assert (n, wrong, lines) == (EST[tag][0], [], EST[tag][1])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tag, code, labor, before, after",
|
||||
[
|
||||
@@ -167,23 +140,6 @@ def test_stmate_price_propagates(tag, code, labor, before, after) -> None:
|
||||
assert money(engine(out))[: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)
|
||||
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:
|
||||
"""호표 줄 = 793-2 꼴(종류 · 단가 · 이음 = 1차 줄 판정 M01 키) · 덧칸 자원 · 원본."""
|
||||
tag = "봉화"
|
||||
@@ -266,28 +222,3 @@ def test_stmate_grammar_sample_engine() -> None:
|
||||
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}
|
||||
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(out)) == (15950, 0, 0, 15950)
|
||||
em.reprice(logic, LABOR, {"L": 180000.0})
|
||||
assert money(engine(out)) == (16686, 0, 0, 16686) # 793-1 run_est 전파와 같음
|
||||
|
||||
@@ -337,26 +337,20 @@ def test_원본_보존_사본():
|
||||
|
||||
# ── 실무 블록(793-2a 내보내기 → 가져온 식 SK) ─────────────────────────────
|
||||
REAL = [("봉화", "D00102"), ("동부청영월", "D00003"), ("울진소광", "D01489"), ("북부청임도", 18)]
|
||||
RULE = {"봉화": "비목버림", "동부청영월": "합버림_큰나머지", "울진소광": "비목버림"}
|
||||
|
||||
|
||||
def _emitted(tag, code):
|
||||
"""실무 블록 → 812-1 엑셀 읽기기 → 원파일 길(`M02_Doc_FromNative.emit`)."""
|
||||
import helper_m02_hopyo_golden as golden
|
||||
|
||||
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_Stmate as stm
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||||
|
||||
if not golden.FILES[tag].exists():
|
||||
path = golden.FILES[tag]
|
||||
if not path.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
data = book.load(golden.FILES[tag])
|
||||
if tag in RULE:
|
||||
blk = next(b for b in data["블록"] if b["코드"] == code)
|
||||
return em.stmate(blk, stm.parse_block(blk["몸"]), data["단가"], RULE[tag], True, tag)
|
||||
blk = next(b for b in data["블록"] if b["번"] == code)
|
||||
want = data["총괄"][blk["번"]]
|
||||
return em.est(blk, est.parse_block(blk["글"], data["단가"]), want, True, tag)
|
||||
saved = fn.pack(sx.read(path.name, path.read_bytes()), path.name)
|
||||
return fn.emit(saved, int(str(code).lstrip("D")))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("tag", "code"), REAL)
|
||||
|
||||
@@ -87,14 +87,16 @@ RULES = [("봉화", "D00102", "비목버림"), ("동부청영월", "D00003", "
|
||||
|
||||
@pytest.mark.parametrize(("tag", "code", "rule"), RULES)
|
||||
def test_STmate_끝전_엑셀_계와_맞는_쪽(tag, code, rule):
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
"""끝전 = 파일 투표(812-1 `M02_San_Xlsx_Stmate.vote` · 블록 계가 맞는 수가 많은 쪽)."""
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||||
from M02_MasterTemplete import M02_San_Xlsx_Stmate as xs
|
||||
|
||||
if not golden.FILES[tag].exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
data = book.load(golden.FILES[tag])
|
||||
blk = next(b for b in data["블록"] if b["코드"] == code)
|
||||
assert fh._rule(blk, stm.parse_block(blk["몸"]), data["단가"]) == rule
|
||||
path = golden.FILES[tag]
|
||||
got = sx.read(path.name, path.read_bytes())
|
||||
assert got["옵션"]["SAN_TYP"] == xs.RULE[rule]
|
||||
assert any(b["코드"] == code for b in got["산근"])
|
||||
|
||||
|
||||
# ── 화면 소스 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
"""797-1a 지킴 — 엑셀 불러오기 빠진 정보 표지 다섯 · 줄 단위 합격 · 표본 밖 둘 고정(요구 658).
|
||||
"""797-1a 지킴 — 엑셀 불러오기 빠진 정보 표지(요구 658) · 812-1 뒤 남은 것.
|
||||
|
||||
표지 다섯(`M02_San_Emit` · `M02_San_Est` · `M02_San_Book` · `M02_Sangeun_FromHopyo`) —
|
||||
① 금액 줄 못 읽음(글끝 · 식못읽음 · 결과 뒤 꼬리 글은 떼고 읽음) ② 단가 후보 여럿 · 추정
|
||||
③ 당초 / 적용 칸 ④ 조정 비율 · 낙찰률 미지원 ⑤ 읽기 불가 사유 — 작은 견본(엑셀 없이).
|
||||
줄 단위 합격 = 블록 합만이 아니라 표지 없는 줄 중 틀린 줄 0(표지 달린 줄은 틀림에서 뺌) ·
|
||||
표본 밖(개발에 안 쓴 파일: 2026 수목진료 142 · 울진대흥 공통 46 · 2016 사방 374)을 합격 수로 고정 ·
|
||||
EST 단가줄 버림1 일치율 감시(낮아지면 실패). 실무문서 엑셀 없으면 skip(읽기만).
|
||||
③ 당초 / 적용 칸(`M02_San_Book.applied_how` → 엑셀 읽기기 블록 표지 → 원본 표지) ④ 조정 비율 · 낙찰률
|
||||
미지원(`M02_San_Emit.stmate`) ⑤ 읽기 불가 사유(`M02_Sangeun_FromHopyo.keep`) — 작은 견본(엑셀 없이).
|
||||
옛 EST 글 추정 표지(① 금액 줄 못 읽음 · ② 단가 후보 여럿) · 줄 단위 합격 · 표본 밖 수 · 버림1 감시는
|
||||
옛 엔진과 함께 걷음 — 812-1 `test_812_1_xlsx_golden`(실제 짝 · 합성 · 실무 11 금액)이 지킴.
|
||||
`FILES`(실무 11) = 812-1 시험이 씀.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import helper_m02_hopyo_golden as golden
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn
|
||||
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_Expr as ex
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
|
||||
|
||||
NOTE = em.NOTE
|
||||
@@ -37,63 +33,6 @@ def hard(out: dict) -> list[str]:
|
||||
return [m for m in out["원본"]["표지"] if NOTE not in m]
|
||||
|
||||
|
||||
# ── ① 금액 줄 못 읽음 ──────────────────────────────────────────────────
|
||||
TAIL = " 보통인부 : 94,338 ×0.0328인 = 3,094.2 : 채집"
|
||||
PRICE_L = {"종류": "노임", "이름": "보통인부", "규격": "", "비목": "L", "값": 94338.0}
|
||||
|
||||
|
||||
def _est_out(text: list[str], prices: list[dict], want: dict):
|
||||
rows = list(range(10, 10 + len(text)))
|
||||
blk = {"이름": "견본", "단위": "m", "번": 1, "글": text, "글행": rows}
|
||||
lines = est.parse_block(text, prices)
|
||||
return lines, em.est(blk, lines, want)
|
||||
|
||||
|
||||
def test_tail_text_is_stripped_and_read_as_money() -> None:
|
||||
one = est.parse_line(TAIL)
|
||||
assert (one["꼴"], one["값"], one["라벨"]) == ("돈", 3094.2, "보통인부")
|
||||
want = {"T": 3094, "L": 3094, "M": 0, "S": 0}
|
||||
lines, out = _est_out([TAIL, " 노무비 : 3,094"], [PRICE_L], want)
|
||||
assert [x["종류"] for x in lines] == ["단가줄", "값줄"]
|
||||
assert hard(out) == [] and notes(out) == []
|
||||
assert out["로직"]["호표"][0]["이름"] == "보통인부"
|
||||
|
||||
|
||||
def test_unreadable_money_line_is_flagged_with_shown_value() -> None:
|
||||
cut = " 절취량 : (0.5×0.6×1.0) / 2 = 0.15㎥/m"
|
||||
note = " 야산지 25%, 경사도 중(15~30˚)) 5% => 30% 할증"
|
||||
bad = " 산출 : 가나다 = 12.5"
|
||||
assert est.parse_line(cut) == {"꼴": "글끝", "라벨": "절취량", "보인": 0.15}
|
||||
assert est.parse_line(note) == {"꼴": "글끝"} # 라벨 없는 설명 글 — 표지 안 함
|
||||
assert est.parse_line(bad)["꼴"] == "식못읽음"
|
||||
zero = {"T": 0, "L": 0, "M": 0, "S": 0}
|
||||
_, out = _est_out([cut, note, bad], [], zero)
|
||||
got = {g["행"]: g for g in out["원본"]["글줄"]}
|
||||
flag = f"{NOTE}금액 줄 못 읽음 · 보인 값 얼림"
|
||||
assert (got[10]["표지"], got[10]["값"]) == (flag, 0.15)
|
||||
assert "표지" not in got[11] and "값" not in got[11]
|
||||
assert (got[12]["표지"], got[12]["값"]) == (flag, 12.5)
|
||||
assert len(notes(out)) == 2 and hard(out) == []
|
||||
|
||||
|
||||
# ── ② 단가 후보 여럿 · 추정 ────────────────────────────────────────────
|
||||
def test_ambiguous_price_is_flagged_not_silently_picked() -> None:
|
||||
prices = [
|
||||
{"종류": "자재", "이름": "철근", "규격": "", "비목": "M", "값": 500.0},
|
||||
{"종류": "자재", "이름": "시멘트", "규격": "", "비목": "M", "값": 500.0},
|
||||
]
|
||||
text = [" 철근 : 500 ×2개 = 1,000", " 자재비 : 500 ×3개 = 1,500"]
|
||||
want = {"T": 1500, "L": 0, "M": 1500, "S": 0}
|
||||
lines, out = _est_out(text, prices, want)
|
||||
assert [x["역할"][0]["갈림"] for x in lines] == ["문맥", "문맥"]
|
||||
assert [x["역할"][0]["모호"] for x in lines] == [False, True]
|
||||
got = {g["행"]: g["표지"] for g in out["원본"]["글줄"]}
|
||||
assert got[10] == f"{NOTE}추정 — 단가 정체 근거 문맥"
|
||||
assert got[11] == f"{NOTE}단가 후보 여럿(2) · 사람 확인"
|
||||
assert hard(out) == []
|
||||
assert [it["자원"].get("모호") for it in out["로직"]["호표"]] == [None, True]
|
||||
|
||||
|
||||
# ── ③ 당초 / 적용 칸 ───────────────────────────────────────────────────
|
||||
def _row(applied: tuple, first: tuple) -> list:
|
||||
r = [None] * 29
|
||||
@@ -126,13 +65,18 @@ def test_applied_or_first_column_is_flagged() -> None:
|
||||
|
||||
def test_first_column_note_reaches_block_flags() -> None:
|
||||
head = [[None] * 29 for _ in range(3)]
|
||||
sheet = [*head, [None, 1, "견본", ""] + [None] * 25]
|
||||
sheet[3][25:29] = [90, 40, 30, 20]
|
||||
prices, san = book.est_prices({"단가산출총괄표": sheet})
|
||||
assert san[1]["칸"] == "당초 칸 사용 — 적용 칸 비어 있음" and san[1]["T"] == 90
|
||||
assert {(p["종류"], p["비목"], p["값"]) for p in prices} >= {("산근", "T", 90.0)}
|
||||
_, out = _est_out([" 노무비 : 40"], [], {**san[1]})
|
||||
total = [*head, [None, "1", "견본", "", "m"] + [None] * 24]
|
||||
total[2][1:5] = ["No", "품명", "규격", "단위"]
|
||||
total[3][25:29] = [90, 40, 30, 20]
|
||||
sheet = [[None, "단가산출"], [], [None, "산출근거", "합계", "노무비", "재료비", "경비"]]
|
||||
sheet += [
|
||||
[None, "1.견본[m]", 90, "40", "30", "20"],
|
||||
[None, " 계 : 90", 90, "40", "30", "20"],
|
||||
]
|
||||
got = sx.read_sheets({"단가산출": sheet, "단가산출총괄표": total})
|
||||
out = fn.emit(fn.pack(got, "견본.xls"), 1)
|
||||
assert f"{NOTE}당초 칸 사용 — 적용 칸 비어 있음" in out["원본"]["표지"]
|
||||
assert out["원본"]["엑셀"] is True
|
||||
|
||||
|
||||
# ── ④ 조정 비율 · 낙찰률 미지원 ────────────────────────────────────────
|
||||
@@ -172,7 +116,7 @@ def test_unreadable_reason_is_returned(tmp_path: Path) -> None:
|
||||
assert not fh.has(tmp_path, "HP990001") # 못 읽은 파일은 안 둠
|
||||
|
||||
|
||||
# ── 줄 단위 합격 · 표본 밖 고정 · 버림1 감시 ───────────────────────────
|
||||
# ── 실무 엑셀 11(812-1 시험이 씀) ──────────────────────────────────────
|
||||
def _oos() -> dict[str, Path]:
|
||||
pack = DOCS / "2026_실무일위대가 모음"
|
||||
common = DOCS / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료"
|
||||
@@ -185,102 +129,3 @@ def _oos() -> dict[str, Path]:
|
||||
|
||||
|
||||
FILES = {**golden.FILES, **_oos()}
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _load(tag: str) -> dict:
|
||||
path = FILES[tag]
|
||||
if not path.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {path.name}")
|
||||
return book.load(path)
|
||||
|
||||
|
||||
def _stmate_tally(tag: str) -> dict:
|
||||
data = _load(tag)
|
||||
t = dict.fromkeys(("블록", "맞음", "금액줄", "표지없는틀린줄", "계줄", "계줄맞음"), 0)
|
||||
for blk in data["블록"]:
|
||||
if not any(ln["G"].strip() for ln in blk["몸"]):
|
||||
continue
|
||||
nodes = stm.parse_block(blk["몸"])
|
||||
rule = fh._rule(blk, nodes, data["단가"])
|
||||
chk = ev.stmate_check(blk, ev.stmate_block(blk, nodes, data["단가"], rule))
|
||||
out = em.stmate(blk, nodes, data["단가"], rule, False, tag)
|
||||
marked = {g["행"] for g in out["원본"]["글줄"] if g.get("표지")}
|
||||
t["블록"] += 1
|
||||
t["맞음"] += chk["맞음"]
|
||||
t["금액줄"] += chk["금액줄"]
|
||||
t["계줄"] += chk["계줄"]
|
||||
t["계줄맞음"] += chk["계줄맞음"]
|
||||
t["표지없는틀린줄"] += sum(1 for b in chk["틀린줄"] if b["행"] not in marked)
|
||||
return t
|
||||
|
||||
|
||||
def _est_tally(tag: str) -> dict:
|
||||
data = _load(tag)
|
||||
t = dict.fromkeys(("블록", "맞음", "줄수", "표지없는틀린줄", "표지줄", "단가줄", "버림1"), 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)
|
||||
out = em.est(blk, parsed, want, False, tag)
|
||||
rows = blk.get("글행") or []
|
||||
marked = {g["행"] for g in out["원본"]["글줄"] if g.get("표지")}
|
||||
t["블록"] += 1
|
||||
t["맞음"] += chk["맞음"]
|
||||
t["줄수"] += chk["줄수"]
|
||||
t["표지줄"] += len(marked)
|
||||
for b in chk["틀린줄"]:
|
||||
k = b["차례"]
|
||||
t["표지없는틀린줄"] += (rows[k] if k < len(rows) else k) not in marked
|
||||
for ln in parsed:
|
||||
if ln.get("종류") == "단가줄" and ln.get("셈") is not None:
|
||||
t["단가줄"] += 1
|
||||
t["버림1"] += abs(ex.RULE["버림1"](ln["셈"]) - ln["값"]) < ex.EPS
|
||||
return t
|
||||
|
||||
|
||||
# 표본 밖 = 개발에 안 쓴 파일(합격 수 고정) · 골든 = 개발에 쓴 파일
|
||||
OOS_FIXED = {
|
||||
"2026수목": {"블록": 142, "맞음": 142, "줄수": 1654},
|
||||
"대흥공통": {"블록": 46, "맞음": 46, "금액줄": 258, "계줄": 127, "계줄맞음": 127},
|
||||
"2016사방": {"블록": 374, "맞음": 374, "줄수": 1870},
|
||||
}
|
||||
# 단가줄 버림1 일치 수 / 단가줄 수(낮아지면 실패 — 다른 규칙으로 갈리는 줄이 늘었다는 뜻)
|
||||
FLOOR_1 = {
|
||||
"북부청임도": (539, 552),
|
||||
"북부청사방": (363, 364),
|
||||
"거창": (381, 382),
|
||||
"2016사방": (718, 718),
|
||||
"2026수목": (653, 653),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(FILES))
|
||||
def test_no_unmarked_wrong_line(tag: str) -> None:
|
||||
"""줄 단위 — 표지 없는 틀린 줄 0 · 블록 다시 셈 = 엑셀 전부."""
|
||||
est_form = _load(tag)["꼴"] == "EST"
|
||||
t = _est_tally(tag) if est_form else _stmate_tally(tag)
|
||||
assert t["블록"] > 0
|
||||
assert (t["표지없는틀린줄"], t["맞음"]) == (0, t["블록"]), t
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(OOS_FIXED))
|
||||
def test_out_of_sample_counts_fixed(tag: str) -> None:
|
||||
if tag not in FILES:
|
||||
pytest.skip("표본 밖 엑셀 없음")
|
||||
t = _est_tally(tag) if tag != "대흥공통" else _stmate_tally(tag)
|
||||
assert {k: t[k] for k in OOS_FIXED[tag]} == OOS_FIXED[tag]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", list(FLOOR_1))
|
||||
def test_unit_price_line_floor1_rate_does_not_drop(tag: str) -> None:
|
||||
if tag not in FILES:
|
||||
pytest.skip("실무 엑셀 없음")
|
||||
t = _est_tally(tag)
|
||||
hit, total = FLOOR_1[tag]
|
||||
assert t["단가줄"] >= total and t["버림1"] / t["단가줄"] >= hit / total, (
|
||||
t["버림1"],
|
||||
t["단가줄"],
|
||||
)
|
||||
|
||||
@@ -17,9 +17,9 @@ import helper_m02_hopyo_golden as golden
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_San_Basis_Index as bi
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
|
||||
@@ -275,10 +275,7 @@ def test_real_north_block_has_unique_candidate_and_flags(tmp_path) -> None:
|
||||
excel = json.loads(fh._path(tmp_path, "HP-000001").read_bytes())["엑셀"]
|
||||
found = None
|
||||
for no in sorted(fn.numbers(excel)):
|
||||
try:
|
||||
out = fh.emit(tmp_path, "HP-000001", no)
|
||||
except TypeError: # NativeEst 자리 dict 묶음(812-3 몫)
|
||||
continue
|
||||
out = fh.emit(tmp_path, "HP-000001", no)
|
||||
if "야면석채집및운반(계곡내)" in out["로직"]["이름"]:
|
||||
found = out
|
||||
break
|
||||
|
||||
@@ -190,7 +190,7 @@ def test_qty_pick_becomes_table_link(world) -> None:
|
||||
for no in range(1, 200):
|
||||
try:
|
||||
out = fh.emit(folder, KEY, no)
|
||||
except (store.StoreError, TypeError): # TypeError = NativeEst 자리 dict 묶음(812-3 몫)
|
||||
except store.StoreError:
|
||||
continue
|
||||
if "야면석채집및운반(계곡내)" in out["로직"]["이름"]:
|
||||
found = out
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
(a) 거울 — node `tools/est-test/import-estx.cjs` 결과와 표 이름 · 행 수 · 모든 칸 값 1:1(다름 0)
|
||||
(b) 블록 줄합(저장 결과만 · 셈 없음) = 제목 표 저장 금액(TC NC GC JC SC) 100 %
|
||||
(c) 거창 = 지금 엑셀 엔진(`M02_San_Book` · `M02_San_Est` · `M02_San_Eval`)과 대조 — 블록 수 · 금액 ·
|
||||
줄마다 갈래(단가 · 참조 수) · 다른 곳 = 엑셀 추정이 틀린 곳(고치지 않음 · 수만 지킴)
|
||||
(c) 엑셀 짝 대조는 812-1 엑셀 읽기기 골든(`test_812_1_xlsx_golden` · 옛 엑셀 엔진 걷음)
|
||||
원파일 · node 없으면 skip(다른 PC).
|
||||
"""
|
||||
|
||||
@@ -12,7 +11,6 @@ from __future__ import annotations
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -156,90 +154,3 @@ def test_structure_marks() -> None:
|
||||
assert [ln["비목"] for ln in b["줄"] if ln["합계"]] == ["L", "M", "S", "T"]
|
||||
sub = [ln for d in [parsed("거창")] for b in d["산근"] for ln in b["줄"] if ln["산근"]]
|
||||
assert sub and all(ln["자원"]["종류"] == "산근" for ln in sub)
|
||||
|
||||
|
||||
# ── (c) 엑셀 엔진 대조 ─────────────────────────────────────────────────
|
||||
def _key(s: str) -> str:
|
||||
num = r"\d[\d,]*\.?\d*|\.\d+"
|
||||
s = re.sub(num, lambda m: repr(float(m.group().replace(",", "").rstrip(".") or 0)), s)
|
||||
return re.sub(r"\s", "", s)
|
||||
|
||||
|
||||
def excel_diffs(tag: str = "거창") -> tuple[list[dict], dict]:
|
||||
"""엑셀 엔진 블록 · 줄 갈래 대 원파일 → (다른 곳 목록, 셈)."""
|
||||
sys.path.insert(0, str(ROOT / "resources" / "tester"))
|
||||
import helper_m02_hopyo_golden as golden
|
||||
|
||||
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
|
||||
|
||||
if not golden.FILES[tag].exists():
|
||||
pytest.skip("엑셀 없음")
|
||||
xl = book.load(golden.FILES[tag])
|
||||
by_no = {b["번"]: b for b in xl["블록"]}
|
||||
out, cnt = [], {"블록": 0, "금액같음": 0, "글같음": 0, "줄": 0, "갈래같음": 0}
|
||||
for no, b in enumerate(parsed(tag)["산근"], 1):
|
||||
x = by_no.get(no)
|
||||
if x is None:
|
||||
out.append({"블록": no, "코드": b["코드"], "항목": "엑셀에 블록 없음"})
|
||||
continue
|
||||
cnt["블록"] += 1
|
||||
want = xl["총괄"].get(no)
|
||||
lines = est.parse_block(x["글"], xl["단가"])
|
||||
chk = ev.est_check(lines, want) if want else {"금액": None}
|
||||
saved = {c: b["저장"].get(k) or 0 for c, k in (("L", "L"), ("M", "M"), ("S", "S"))}
|
||||
if chk["금액"] and all(abs(chk["금액"][c] - saved[c]) < 0.5 for c in "LMS"):
|
||||
cnt["금액같음"] += 1
|
||||
else:
|
||||
out.append(
|
||||
{
|
||||
"블록": no,
|
||||
"코드": b["코드"],
|
||||
"항목": "금액",
|
||||
"엑셀": chk["금액"],
|
||||
"원파일": saved,
|
||||
}
|
||||
)
|
||||
mine = [ln for ln in b["줄"] if ln["보임"].strip()]
|
||||
if len(mine) == len(x["글"]) and all(
|
||||
_key(a["보임"]) == _key(c) for a, c in zip(mine, x["글"])
|
||||
):
|
||||
cnt["글같음"] += 1
|
||||
for L, ln in zip(lines, mine):
|
||||
if not (ln["연결단가"] or ln["변수"] or ln["줄참조"] or L.get("꼴") == "돈"):
|
||||
continue
|
||||
cnt["줄"] += 1
|
||||
roles = [r["갈래"] for r in (L.get("역할") or {}).values()]
|
||||
hidden = len((L.get("숨은") or {}).get("줄") or []) # 값 줄 = 숨은 복사 · 합
|
||||
got = (roles.count("단가"), roles.count("앞줄") + hidden)
|
||||
truth = (len(ln["연결단가"]), len(ln["변수"]) + len(ln["줄참조"]))
|
||||
money = L.get("꼴") == "돈" or L.get("종류") == "값줄"
|
||||
auto = ln["합계"] or ln["표지"] == "소계" # 자동 합 · 소계 = 엑셀 숨은 합과 같은 뜻
|
||||
ok = money and got[0] == truth[0] and (auto or got[1] == truth[1])
|
||||
cnt["갈래같음"] += ok
|
||||
if not ok:
|
||||
out.append(
|
||||
{
|
||||
"블록": no,
|
||||
"코드": b["코드"],
|
||||
"행": ln["행"],
|
||||
"항목": "갈래",
|
||||
"글": ln["보임"].strip(),
|
||||
"엑셀": f"{L.get('꼴')} 단가 {got[0]} · 앞줄 {got[1]}",
|
||||
"원파일": f"단가 {truth[0]} · 변수 {len(ln['변수'])} · "
|
||||
f"줄참조 {len(ln['줄참조'])}",
|
||||
}
|
||||
)
|
||||
return out, cnt
|
||||
|
||||
|
||||
def test_excel_geochang() -> None:
|
||||
out, cnt = excel_diffs("거창")
|
||||
assert (cnt["블록"], cnt["금액같음"], cnt["글같음"]) == (61, 61, 61)
|
||||
assert [o for o in out if o["항목"] != "갈래"] == []
|
||||
assert (cnt["줄"], cnt["갈래같음"]) == EXCEL_LINES
|
||||
|
||||
|
||||
#: 거창 (원파일 · 엑셀 돈 줄 수, 엑셀 갈래 = 원파일) — 다른 곳 = `_목록_810-1_원파일대조.xlsx`
|
||||
EXCEL_LINES = (547, 282)
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
칸 · 산근 원문 1:1(다름 0) · 줄 풀이 = `M02_San_Stmate.parse_line`
|
||||
(b) 블록 다시 셈(`M02_San_Eval.stmate_block` · 차수 1 단가 · 낙찰 · 결과 조정 · 끝전 갈래 = 옵션
|
||||
SAN_TYP 1 비목버림 / 0 합버림_큰나머지) = COSTN 저장 금액(TAMT1 · LAMT1 · MAMT1 · SAMT1) 100 %
|
||||
(c) 엑셀 짝 다섯(`helper_m02_hopyo_golden`) = 지금 엑셀 엔진 블록 금액 · 숨은 G 열 원문 ·
|
||||
목록표 단가와
|
||||
대조 — 다른 곳 = 엑셀 추정이 틀린 곳(수만 지킴) · 엑셀에 안 찍힌 블록 수 기록
|
||||
(c) 엑셀 짝 대조는 812-1 엑셀 읽기기 골든(`test_812_1_xlsx_golden` · 옛 엑셀 엔진 걷음)
|
||||
원파일 없으면 skip(다른 PC).
|
||||
"""
|
||||
|
||||
@@ -117,71 +115,3 @@ def test_structure_line() -> None:
|
||||
assert [ln["자원"]["코드"] for ln in refs] == ["M00080", "M00002", "M00001", "L00001", "L00007"]
|
||||
assert refs[0]["자원"]["종류"] == "자재" and refs[3]["자원"]["종류"] == "노임"
|
||||
assert [ln["단"] for ln in b["줄"] if ln["합계"]] == [1, 1, 2]
|
||||
|
||||
|
||||
# ── (c) 엑셀 엔진 대조 ─────────────────────────────────────────────────
|
||||
PAIRED = ("봉화", "동부청영월", "울진대흥", "영덕계류", "울진소광")
|
||||
|
||||
|
||||
def excel_diffs(tag: str) -> tuple[list[dict], dict]:
|
||||
"""엑셀 엔진(숨은 G 열 · 목록표 단가) 대 원파일 → (다른 곳, 셈)."""
|
||||
import helper_m02_hopyo_golden as golden
|
||||
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
if not golden.FILES[tag].exists():
|
||||
pytest.skip("엑셀 없음")
|
||||
xl = book.load(golden.FILES[tag])
|
||||
d = parsed(tag)
|
||||
rule = RULE[d["옵션"]["SAN_TYP"]]
|
||||
mine = {b["코드"]: b for b in d["산근"]}
|
||||
out = []
|
||||
cnt = {"엑셀블록": len(xl["블록"]), "원파일블록": len(mine), "같이": 0, "금액같음": 0}
|
||||
cnt |= {"글같음": 0, "단가": 0, "단가같음": 0}
|
||||
for x in xl["블록"]:
|
||||
b = mine.get(x["코드"])
|
||||
if b is None:
|
||||
out.append({"코드": x["코드"], "항목": "원파일에 블록 없음"})
|
||||
continue
|
||||
cnt["같이"] += 1
|
||||
res = ev.stmate_block(x, stm.parse_block(x["몸"]), xl["단가"], rule)["금액"]
|
||||
if all(abs(res[c] - b["저장"][c]) < 0.5 for c in "TLMS"):
|
||||
cnt["금액같음"] += 1
|
||||
else:
|
||||
out.append({"코드": x["코드"], "항목": "금액", "엑셀": res, "원파일": b["저장"]})
|
||||
g = [ln["G"].strip() for ln in x["몸"] if ln["G"].strip()]
|
||||
if g == [ln["글"].strip() for ln in b["줄"] if ln["글"].strip()]:
|
||||
cnt["글같음"] += 1
|
||||
else:
|
||||
out.append({"코드": x["코드"], "항목": "원문 글"})
|
||||
for code, p in xl["단가"].items():
|
||||
q = d["단가"].get(code)
|
||||
cnt["단가"] += 1
|
||||
if q and all(abs((p[c] or 0) - q[c]) < 0.5 for c in "TLMS"):
|
||||
cnt["단가같음"] += 1
|
||||
else:
|
||||
out.append({"코드": code, "항목": "단가", "엑셀": p, "원파일": q})
|
||||
for code in sorted(set(mine) - {x["코드"] for x in xl["블록"]}):
|
||||
out.append({"코드": code, "항목": "엑셀에 안 찍힌 블록"})
|
||||
return out, cnt
|
||||
|
||||
|
||||
#: 엑셀 짝 → (엑셀 블록, 같이 있는 블록, 엑셀 단가 줄, 그중 같음, 엑셀에 안 찍힌 원파일 블록)
|
||||
#: 울진대흥 = 엑셀(신설사업 판)과 원파일(설계원본 판)이 다른 판 — 코드 겹침 14 · 단가 40 빠짐
|
||||
EXCEL = {
|
||||
"봉화": (65, 65, 203, 203, 0),
|
||||
"동부청영월": (69, 69, 201, 201, 0),
|
||||
"울진대흥": (47, 14, 153, 113, 32),
|
||||
"영덕계류": (15, 15, 54, 54, 20),
|
||||
"울진소광": (40, 40, 138, 138, 22),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", PAIRED)
|
||||
def test_excel_engine(tag: str) -> None:
|
||||
out, cnt = excel_diffs(tag)
|
||||
n_xl, both, n_price, same_price, extra = EXCEL[tag]
|
||||
assert (cnt["엑셀블록"], cnt["같이"], cnt["단가"]) == (n_xl, both, n_price)
|
||||
assert cnt["금액같음"] == cnt["같이"] and cnt["글같음"] == cnt["같이"]
|
||||
assert cnt["단가같음"] == same_price
|
||||
assert sum(o["항목"] == "엑셀에 안 찍힌 블록" for o in out) == extra
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
닫는 표지 2 칸 · 합계 표지 1 칸 · 소계 61 = 비목 칸만 — 거창 실제와 글 배치 같음(수 꼴 빼고) 먼저 확인) →
|
||||
읽기기 → 원파일 구조 줄과 같음(EST 1,849 · STC 371 블록) · 원파일 자체 엇갈림(같은 글 · 다른 표지)은
|
||||
`ODD` 로 고정
|
||||
③ 금액 — 실무 엑셀 11 = 읽기기 → `M02_Doc_FromNative` → 수식 문서 셈 = 엑셀 블록 금액(`ODD_MONEY` 고정)
|
||||
③ 금액 — 실무 엑셀 11 = 읽기기 → `M02_Doc_FromNative` → 수식 문서 셈 = 엑셀 블록 금액 100 %
|
||||
빠른 견본 기본 · 전수 = 환경 S8121_ALL=1 · 파일 없으면 skip(다른 PC).
|
||||
"""
|
||||
|
||||
@@ -52,20 +52,8 @@ ODD = {
|
||||
*(("main", 1308, r) for r in (8, 16, 26)), # 금액 칸 없는 「소 계 : 수 kg」 = 61(861 = 글)
|
||||
*(("main", 1441, r) for r in (64, 65, 66, 67)), # 단가가 기계표와 다른 세 줄 · 비목 없음
|
||||
}
|
||||
#: 금액 — 셈 ≠ 엑셀 블록(까닭 고정)
|
||||
ODD_MONEY = {
|
||||
(
|
||||
"북부청임도",
|
||||
33,
|
||||
), # 노무비 줄 「36,248 ÷ 0.096」 오타 — 절 소계 덧셈이 그 줄을 뺌(글 덧셈만 앎)
|
||||
(
|
||||
"북부청임도",
|
||||
3,
|
||||
), # 아래 넷 = `M02_Doc_NativeEst.triple` 자리 규칙 dict 묶음 TypeError(812-3 몫)
|
||||
("북부청임도", 54),
|
||||
("북부청임도", 55),
|
||||
("북부청사방", 24),
|
||||
}
|
||||
#: 금액 — 셈 ≠ 엑셀 블록(없음 · 생기면 까닭과 함께 고정)
|
||||
ODD_MONEY: set[tuple] = set()
|
||||
|
||||
|
||||
# ── 합성(경쟁사 내보내기 꼴) ─────────────────────────────────────────────
|
||||
@@ -396,12 +384,8 @@ def sweep(tag: str) -> dict:
|
||||
out["자원줄"] += bool(ln.get("자원"))
|
||||
for no, blk in fn.numbers(saved).items():
|
||||
out["블록"] += 1
|
||||
try:
|
||||
emitted = fn.emit(saved, no)
|
||||
_, _, got = nd.doc_and_money(emitted)
|
||||
except TypeError:
|
||||
out["다름"].add((tag, no))
|
||||
continue
|
||||
emitted = fn.emit(saved, no)
|
||||
_, _, got = nd.doc_and_money(emitted)
|
||||
mine = {c: got.get(k) for c, k in nd.COST}
|
||||
nag = (emitted["원본"].get("계약") or {}).get("낙찰률")
|
||||
if nag:
|
||||
|
||||
Reference in New Issue
Block a user