- 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>
118 lines
5.3 KiB
Python
118 lines
5.3 KiB
Python
"""810-1 지킴 — STmate 원파일 파서(`M02_San_Stc`) · 실무 .STC 일곱(읽기만).
|
|
|
|
(a) 거울 — `tools/stmate-test/smt_parser.py`(parse_dbf_bytes · extract_costn_memos)와
|
|
COSTN · BOQ11 행 ·
|
|
칸 · 산근 원문 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) 엑셀 짝 대조는 812-1 엑셀 읽기기 골든(`test_812_1_xlsx_golden` · 옛 엑셀 엔진 걷음)
|
|
원파일 없으면 skip(다른 PC).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
for p in (ROOT, ROOT / "tools" / "stmate-test", ROOT / "resources" / "tester"):
|
|
if str(p) not in sys.path:
|
|
sys.path.insert(0, str(p))
|
|
|
|
from M02_MasterTemplete import M02_San_Eval as ev # noqa: E402
|
|
from M02_MasterTemplete import M02_San_Stc as S # noqa: E402
|
|
from M02_MasterTemplete import M02_San_Stmate as stm # noqa: E402
|
|
|
|
K = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
|
SO = K / "25년 산불진화임도(기번8)(울진소광)"
|
|
FILES = {
|
|
"봉화": K / "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC", # noqa: E501
|
|
"동부청영월": K / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", # noqa: E501
|
|
"울진대흥": K / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC", # noqa: E501
|
|
"영덕계류": K / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC", # noqa: E501
|
|
"울진소광": SO / "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", # noqa: E501
|
|
"소광22": SO / "1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC", # noqa: E501
|
|
"소광변경1": SO / "변경 1차분-2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", # noqa: E501
|
|
} # fmt: skip
|
|
#: 파일 → (산근 블록, 일위 블록 · 구성 못 읽음, SAN_TYP)
|
|
WANT = {
|
|
"봉화": (65, 45, "1"),
|
|
"동부청영월": (69, 23, "0"),
|
|
"울진대흥": (46, 34, "1"),
|
|
"영덕계류": (35, 13, "1"),
|
|
"울진소광": (62, 37, "1"),
|
|
"소광22": (40, 25, "1"),
|
|
"소광변경1": (54, 39, "1"),
|
|
}
|
|
RULE = {"1": "비목버림", "0": "합버림_큰나머지"}
|
|
|
|
|
|
@lru_cache(maxsize=2)
|
|
def raw(tag: str) -> bytes:
|
|
path = FILES[tag]
|
|
if not path.exists():
|
|
pytest.skip(f"원파일 없음 {path.name}")
|
|
return path.read_bytes()
|
|
|
|
|
|
@lru_cache(maxsize=2)
|
|
def parsed(tag: str) -> dict:
|
|
return S.read(raw(tag))
|
|
|
|
|
|
def _members(tag: str) -> dict[str, bytes]:
|
|
z, names = S._zip(raw(tag))
|
|
return {k: z.read(v) for k, v in names.items()}
|
|
|
|
|
|
@pytest.mark.parametrize("tag", list(FILES))
|
|
def test_mirror_smt_parser(tag: str) -> None:
|
|
import smt_parser as sp
|
|
|
|
m = _members(tag)
|
|
for name in ("COSTN.DBF", "BOQ11.DBF"):
|
|
_, theirs, _ = sp.parse_dbf_bytes(m[name])
|
|
mine = S.dbf(m[name])
|
|
assert len(mine) == len(theirs), name
|
|
for a, b in zip(mine, theirs, strict=True):
|
|
b = {k: v for k, v in b.items() if k != "_deleted"}
|
|
if "IFORM" in a:
|
|
assert a.pop("IFORM").hex() == b.pop("IFORM_HEX")
|
|
b.pop("IFORM")
|
|
assert a == b
|
|
memos = sp.extract_costn_memos(m["COSTN.DBF"], m["COSTN.SMT"])
|
|
d = parsed(tag)
|
|
blocks = {b["코드"]: b for b in d["산근"]}
|
|
assert sorted(blocks) == sorted(memos)
|
|
for code, mm in memos.items():
|
|
assert [ln["글"] for ln in blocks[code]["줄"]] == mm["text"].splitlines()
|
|
assert all(ln["풀이"] == stm.parse_line(ln["글"]) for ln in blocks[code]["줄"])
|
|
|
|
|
|
@pytest.mark.parametrize("tag", list(FILES))
|
|
def test_saved_amounts(tag: str) -> None:
|
|
d = parsed(tag)
|
|
n_cost, n_head, san = WANT[tag]
|
|
assert (len(d["산근"]), len(d["일위"]), d["옵션"]["SAN_TYP"]) == (n_cost, n_head, san)
|
|
assert all(b["표지"] == [S.UNREAD[0]] and not b["줄"] for b in d["일위"])
|
|
bad = []
|
|
for b in d["산근"]:
|
|
got = ev.stmate_block(b, [ln["풀이"] for ln in b["줄"]], d["단가"], RULE[san])["금액"]
|
|
if any(abs(got[c] - b["저장"][c]) >= 0.5 for c in "TLMS"):
|
|
bad.append((b["코드"], got, b["저장"]))
|
|
assert bad == []
|
|
assert all(not b["표지"] for b in d["산근"])
|
|
|
|
|
|
def test_structure_line() -> None:
|
|
"""봉화 D00014 — 참조 줄 자원 · 비목 · 계 · 산근 블록 머리."""
|
|
b = next(b for b in parsed("봉화")["산근"] if b["코드"] == "D00014")
|
|
assert (b["이름"], b["단위"], b["저장"]["T"]) == ("수평규준틀(변곡점)", "개소", 26144)
|
|
refs = [ln for ln in b["줄"] if ln["자원"]]
|
|
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]
|