144 lines
5.5 KiB
Python
144 lines
5.5 KiB
Python
"""STMATE 재현 앱 조달청 XML 가져오기 — 실무 STC → report_pps 내보내기 → 가져오기 왕복 · XML 변환 설정.
|
|
|
|
정답 = 원본 STC(말단 내역 행 · PRJLST 차수 직접비 · 총공사비 · COSTN 코드). 코드에 값을 박지 않음.
|
|
설정 선택지·칸 차례는 공식 XSD(V1.0.5)에서 읽은 값과 견줌.
|
|
"""
|
|
|
|
import collections
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
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 conv_xml import read_xml, xml_to_project # noqa: E402
|
|
from conv_xml_cfg import apply_settings, building_fields, options, validate # noqa: E402
|
|
from dbf_rw import DbfTable # noqa: E402
|
|
from stc_project import write_output # noqa: E402
|
|
from stc_read import Source # noqa: E402
|
|
from stc_save import _leaf # noqa: E402
|
|
|
|
HELPER = Path(__file__).with_name("helper_stmate_report.cjs")
|
|
DOCS = ROOT / "resources/knowledge/original/실무문서"
|
|
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,
|
|
),
|
|
}
|
|
_cache: dict = {}
|
|
|
|
|
|
def _xml(name):
|
|
if name not in _cache:
|
|
rel, n = 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": n, "mode": "pps"}, ensure_ascii=False),
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
_cache[name] = json.loads(out)["xml"]
|
|
return _cache[name]
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(STCS))
|
|
def test_xml_roundtrip_reproduces_boq_and_costs(name, tmp_path):
|
|
rel, n = STCS[name]
|
|
s = Source(str(DOCS / rel))
|
|
xml = _xml(name)
|
|
data = read_xml(xml)
|
|
bt = DbfTable(s.files["BOQ11.DBF"])
|
|
|
|
def key(code, q, m, lab, sv):
|
|
return (
|
|
code,
|
|
round(float(q or 0), 4),
|
|
int(float(m or 0)),
|
|
int(float(lab or 0)),
|
|
int(float(sv or 0)),
|
|
)
|
|
|
|
want = collections.Counter(
|
|
key(
|
|
bt.get(i, "BDCODE1"),
|
|
*(bt.get(i, f"{c}{n}") for c in ("QTY", "M_AMT", "L_AMT", "S_AMT")),
|
|
)
|
|
for i in range(len(bt))
|
|
if _leaf(bt.get(i, "BDCODE1")) and float(bt.get(i, f"QTY{n}") or 0) != 0
|
|
)
|
|
got = collections.Counter(
|
|
key(r["BDCODE1"], r["QTY1"], r["M_AMT1"], r["L_AMT1"], r["S_AMT1"])
|
|
for r in data["boq"]
|
|
if _leaf(str(r["BDCODE1"])) and float(r["QTY1"] or 0) != 0
|
|
)
|
|
assert want == got, (
|
|
f"{name} 말단 행 다름 {list((want - got).items())[:3]} / {list((got - want).items())[:3]}"
|
|
)
|
|
codes = {
|
|
DbfTable(s.files["COSTN.DBF"]).get(i, "CODE")
|
|
for i in range(len(DbfTable(s.files["COSTN.DBF"])))
|
|
}
|
|
assert {r["CODE"] for r in data["costn"]} <= codes
|
|
out, rep = xml_to_project(s.files, xml)
|
|
pa, pb = DbfTable(s.files["PRJLST.DBF"]), DbfTable(out["PRJLST.DBF"])
|
|
for c in ("MCOST", "LCOST", "SCOST", "TCOST"):
|
|
assert pb.get(0, f"{c}1") == pa.get(0, f"{c}{n}"), c
|
|
assert pb.get(0, "COSTW1") == pa.get(0, f"COSTW{n}")
|
|
assert pb.get(0, "PNAME") == pa.get(0, "PNAME")
|
|
res = write_output(tmp_path, "xml", out, s.names, s.entries)
|
|
load = server.api_load(server.LoadReq(source_path=res["saved_stc"]))
|
|
assert load["costn_count"] == rep["costn_count"] and load["boq_count"] == rep["boq_count"]
|
|
|
|
|
|
def test_xml_settings_from_xsd():
|
|
o = options()
|
|
assert (
|
|
len(o["prj_types_xsd"]) == 9
|
|
and "국가유산수리공사" in o["prj_types"]
|
|
and len(o["prj_types"]) == 8
|
|
)
|
|
names = [f["name"] for f in building_fields()]
|
|
assert len(names) == 36 and names[0] == "연면적" and "공사현장지역코드" in names
|
|
region = next(f for f in building_fields() if f["name"] == "공사현장지역코드")["enum"]
|
|
assert len(region) > 200 and {s["code"] for s in o["sido"]} == {c[:2] for c in region}
|
|
xml = _xml("봉화2024")
|
|
y = apply_settings(xml, {"prj_type": "조경공사"})
|
|
assert (
|
|
validate(y)["ok"]
|
|
and "<주공사>조경공사</주공사>" in y
|
|
and "<공사구분>조경공사</공사구분>" in y
|
|
)
|
|
z = apply_settings(
|
|
xml,
|
|
{
|
|
"prj_type": "건축공사",
|
|
"building": {"연면적": 1234.5, "공사현장지역코드": region[5], "공사현장주소": "시험"},
|
|
},
|
|
)
|
|
v = validate(z)
|
|
assert v["ok"], v["errors"][:2]
|
|
assert "<건축공사사업내용작성여부>true" in z and "<연면적>1234.5</연면적>" in z
|
|
with pytest.raises(ValueError):
|
|
apply_settings(xml, {"prj_type": "건축공사", "building": {"공사현장지역코드": "99999"}})
|
|
with pytest.raises(ValueError):
|
|
apply_settings(xml, {"prj_type": "임도공사"})
|