- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면 - 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면 - 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림 - 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표 - 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0) - DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인) - 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
"""STMATE 재현 앱 요율 조회 — 제비율표(RATE_R · JB_R) 좌표 규칙으로 실무 7건 저장 요율(n_R_* · n_C_ANJUN_G)을 되찾음.
|
|
|
|
표 = 그 STC 안 RATE_R 또는 저장 시각(PRJLST.SMT 압축 시각) 이전에 나온 기준 JB_R(설치 폴더에 있으면) 중 가장 잘 맞는 것.
|
|
사용자가 표를 거치지 않고 고친 요율은 지식DB 15번 §4 대로 수동값 — 아래 MANUAL 에 까닭과 함께 적음(그 밖 불일치는 실패).
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
|
|
import server # noqa: E402
|
|
from smt_parser import parse_dbf_bytes # noqa: E402
|
|
from test_stmate_kanjub_stc import DOCS, STCS # noqa: E402
|
|
|
|
HELPER = Path(__file__).with_name("helper_stmate_kanjub_rate.cjs")
|
|
JB_DIR = Path("C:/경영정보(주)/System/ST_BSC")
|
|
# 조회 항목 → 저장키
|
|
ITEMS = {
|
|
"KANNO": "R_KANNO",
|
|
"KITA": "R_T_KITA",
|
|
"ILBAN": "R_ILBAN",
|
|
"IYUN": "R_IYUN",
|
|
"SANBO": "R_SANBO",
|
|
"KUNBO": "R_KUNBO",
|
|
"YUNBO": "R_YUNBO",
|
|
"SLVBO": "R_SLVBO",
|
|
"KOBO": "R_KOBO",
|
|
"ANJUN": "R_ANJUN",
|
|
"ANJUN1": "R_ANJUN1",
|
|
"ANJUN2": "R_ANJUN2",
|
|
"HWAN": "R_HWAN",
|
|
"EQIP": "R_EQIPSU",
|
|
}
|
|
# 표를 거치지 않은 수동 요율(15번 §4 · 이번 대조)
|
|
MANUAL = {
|
|
# 공사기간 0(6개월 이하)인데 7~12개월 칸 값 · 고용 5등급인데 7등급 값 · 환경 조경(13)인데 기타토목 값 — 다른 공사 설정 복사
|
|
"대흥2024": {"KANNO", "KITA", "KOBO", "HWAN"},
|
|
# RATE_R 10행에 사용자가 손으로 넣은 16.5/5.2(같은 행 나머지 칸 0) — 공사기간 칸과 어긋남
|
|
"울진2025": {"KANNO", "KITA"},
|
|
"소광원설계": {"KANNO", "KITA"},
|
|
# 위와 같음 + 이윤 13.4355%(표에 없음 · 15번 §4)
|
|
"소광변경1차": {"KANNO", "KITA", "IYUN"},
|
|
# 이윤 13.887%(표에 없음) · 건설기계 구분 5 인데 일반공사 0.4%
|
|
"영덕변경": {"IYUN", "EQIP"},
|
|
}
|
|
|
|
|
|
def _jb_tables(before: datetime) -> list[tuple[str, list]]:
|
|
out = []
|
|
for p in sorted(JB_DIR.glob("*.JB_R")) if JB_DIR.exists() else []:
|
|
m = re.match(r"(\d{4})\((?:상|하)_(\d{2})(\d{2})", p.name)
|
|
if m and datetime(int(m[1]), int(m[2]), int(m[3])) <= before:
|
|
out.append((p.name, parse_dbf_bytes(p.read_bytes())[1]))
|
|
return out
|
|
|
|
|
|
def _cases(name: str):
|
|
src = DOCS / STCS[name]
|
|
loaded = server.api_load(server.LoadReq(source_path=str(src)))
|
|
kv, prj = loaded["prjlst_smt"], loaded["prjlst"]
|
|
with zipfile.ZipFile(src) as z:
|
|
saved = datetime(
|
|
*next(i.date_time for i in z.infolist() if i.filename.upper().endswith("PRJLST.SMT"))
|
|
)
|
|
tables = [("STC RATE_R", loaded["rate_tables"]["RATE_R.DBF"])] + _jb_tables(saved)
|
|
for n in sorted({k[0] for k in kv if k[1:] == "_R_KANNO"}):
|
|
g = lambda k: float(kv.get(f"{n}_{k}") or 0) # noqa: E731
|
|
opts = {
|
|
"kongType": g("I_KONG_TYPE"),
|
|
"kikan": g("I_KONG_KIKAN"),
|
|
"scale": float(prj[f"TCOST{n}"]),
|
|
"anjunType": g("I_ANJUN_TYPE"),
|
|
"anjunScale": float(prj[f"MCOST{n}"]) + float(prj[f"LCOST{n}"]) + g("C_KN_SA"),
|
|
"koboType": g("I_KOBO_TYPE"),
|
|
"hwan": g("I_HWAN"),
|
|
"eqip": g("I_EQIP"),
|
|
"tbugum": g("I_TBUGUM"),
|
|
}
|
|
want = {c: g(k) for c, k in ITEMS.items()} | {"C_ANJUN_G": g("C_ANJUN_G")}
|
|
yield n, opts, want, tables
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(STCS))
|
|
def test_rates_come_from_table(name):
|
|
for n, opts, want, tables in _cases(name):
|
|
req = [{"matrix": rows, "opts": opts} for _, rows in tables]
|
|
res = json.loads(
|
|
subprocess.check_output(
|
|
["node", str(HELPER)], input=json.dumps(req), text=True, encoding="utf-8"
|
|
)
|
|
)
|
|
scored = []
|
|
for (tname, _), r in zip(tables, res):
|
|
got = r["rates"] | r["amounts"]
|
|
bad = {c for c, v in want.items() if abs(float(got.get(c, 0)) - v) > 1e-6}
|
|
scored.append((len(bad), tname, bad))
|
|
scored.sort(key=lambda x: x[0])
|
|
best = scored[0]
|
|
# 불일치 = 정확히 수동 목록(더 많으면 규칙 오류 · 적으면 MANUAL 이 버그를 숨김)
|
|
assert best[2] == MANUAL.get(name, set()), f"{name} {n}차수 표 {best[1]} 불일치 {best[2]}"
|