Files
Aislo/resources/tester/test_stmate_report_pps.py
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 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>
2026-11-13 19:12:04 +09:00

85 lines
3.4 KiB
Python

"""STMATE 재현 앱 조달청 공사원가호환규정 XML — 공식 XSD(V1.0.5) 검증 · 금액은 STC 저장값과 1원 대조.
XSD = 지식DB STmate/old/30_원자료/조달청_공사원가호환규정.xsd(STMATE 설치본 Valid_Xml.vsl 사본).
정답 = STC 안 PRJLST.DBF 차수별 저장값(MCOST·LCOST·SCOST·COSTW). 코드에 박지 않음.
"""
import json
import subprocess
import sys
from pathlib import Path
import pytest
from lxml import etree
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
import server # noqa: E402
HELPER = Path(__file__).with_name("helper_stmate_report.cjs")
DOCS = ROOT / "resources/knowledge/original/실무문서"
XSD = (
ROOT
/ "resources/knowledge/original/경쟁사 프로그램/STmate/old/30_원자료/조달청_공사원가호환규정.xsd"
)
STCS = {
"봉화2024": (
"04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC",
1,
),
"영월2024": ("05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC", 1),
"대흥2024": (
"2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC",
1,
),
"울진2025": (
"25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC",
1,
),
"영덕변경": (
"20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC",
2,
),
}
def pps(name):
rel, stage = STCS[name]
load = server.api_load(server.LoadReq(source_path=str(DOCS / rel)))
out = subprocess.check_output(
["node", str(HELPER)],
input=json.dumps({"load": load, "stage": stage, "mode": "pps"}, ensure_ascii=False),
text=True,
encoding="utf-8",
)
return load, stage, etree.fromstring(json.loads(out)["xml"].encode("utf-8"))
@pytest.mark.parametrize("name", list(STCS))
def test_pps_xml_is_valid_and_totals_match_stc(name):
load, n, doc = pps(name)
schema = etree.XMLSchema(etree.parse(str(XSD)))
assert schema.validate(doc), f"{name} XSD 위반 {[str(e) for e in list(schema.error_log)[:3]]}"
def amount(code):
for it in doc.iter("제경비항목"):
if it.findtext("세부공종코드") == code:
return float(it.findtext("산출금액"))
return None
prj = load["prjlst"]
assert abs(amount("700") - float(prj[f"COSTW{n}"])) < 0.5, f"{name} 총공사비"
sums = {"재료비": 0.0, "노무비": 0.0, "경비": 0.0}
for g in doc.iter("내역서"):
if g.findtext("공종구분") != "일반공종":
continue
for it in g.findall("세부공종"):
for k in sums:
sums[k] += float(it.findtext(k))
for k, col in (("재료비", "MCOST"), ("노무비", "LCOST"), ("경비", "SCOST")):
assert abs(sums[k] - float(prj[f"{col}{n}"])) < 0.5, f"{name} 직접{k} {sums[k]}"
codes = {r["CODE"].strip()[0] for r in load["costn_rows"]}
for tag, p in (("일위대가", "B"), ("기계경비", "X"), ("자재", "M"), ("노무", "L")):
if p in codes:
assert doc.find(f".//{tag}") is not None, f"{name} {tag} 없음"