- DBF 고친 칸만 다시 씀(한글 깨짐 · 2칸을 1칸으로 · 3칸 87.745% 덮어쓰기 없앰) · 단가 → 내역 행 → 소계 → 직공비 다시 셈 · 행 끼우기·지우기 - SIxMemo(COSTN.SMT · PRJLST.SMT) 블록 구조 유지 · 넘치면 뒤에 붙이고 가리키는 값 갱신 - STC 다시 묶기(같은 파일은 원본 바이트) · 검사 · 자동 백업·복원 · 새 공사 · 복사등록(차수 인계) · 해당분 압축 - BDQTY 역변환(쓰기 준비) · 모듈 dbf_rw · dbf_memo · dbf_crypt · stc_archive · stc_save · stc_read · stc_project · api_file · scr_file - 지킴 시험 test_stmate_file_roundtrip · file_project · file_payload — 실무 STC 7건 왕복 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
8.1 KiB
Python
174 lines
8.1 KiB
Python
"""STMATE 재현 앱 공사 단위 파일 작업 — STC 검사 · 해당분만 압축 · 복사등록(차수/계약 변환) · 새 공사 · 백업/복원.
|
|
|
|
정답은 실무 STC 자체 — 소광 신설(BOQ11+BOQ12 통합형) → 변경1차(분리형) 짝으로 차수 인계 규칙(16번 §4.6) 대조.
|
|
쓰기는 임시 폴더(server.WORK_DBF_ROOT 바꿈)에서만.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
|
|
import api_file # noqa: E402
|
|
import server # noqa: E402
|
|
from dbf_memo import SixMemo, ini_items # noqa: E402
|
|
from dbf_rw import DbfTable # noqa: E402
|
|
from stc_archive import build_stc, check_stc, files_of, read_entries # noqa: E402
|
|
from stc_project import copy_register, new_project, pack_selected # noqa: E402
|
|
from stc_save import _leaf, boq_spans # noqa: E402
|
|
|
|
DOCS = ROOT / "resources/knowledge/original/실무문서"
|
|
STCS = sorted(DOCS.rglob("*.STC"))
|
|
IDS = [p.stem[:16] for p in STCS]
|
|
SOGWANG = DOCS / "25년 산불진화임도(기번8)(울진소광)"
|
|
JOINT = next(SOGWANG.glob("2025년*.STC")) # BOQ11(당초) + BOQ12(현재안)
|
|
SPLIT = next(SOGWANG.glob("변경*.STC")) # 분리형 변경1차
|
|
|
|
|
|
def _files(p):
|
|
return files_of(read_entries(p.read_bytes()))
|
|
|
|
|
|
def _memo_text(files, i):
|
|
t = DbfTable(files["PRJLST.DBF"])
|
|
return SixMemo(files["PRJLST.SMT"]).read(t.raw(i, "PRJ_MEMO")).decode("cp949")
|
|
|
|
|
|
@pytest.fixture
|
|
def work(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(server, "WORK_DBF_ROOT", tmp_path)
|
|
return tmp_path
|
|
|
|
|
|
@pytest.mark.parametrize("stc", STCS, ids=IDS)
|
|
def test_check_stc_accepts_real_files(stc):
|
|
res = check_stc(stc.read_bytes())
|
|
assert res["ok"], res["problems"]
|
|
assert res["tables"]["PRJLST.DBF"]["nrec"] >= 1
|
|
assert not check_stc(b"PK-not-a-zip")["ok"]
|
|
|
|
|
|
def test_pack_selected_carries_current_plan_into_split_file():
|
|
src = _files(JOINT)
|
|
packed = pack_selected(src, "12")
|
|
assert packed["BOQ11.DBF"] == src["BOQ12.DBF"] and "BOQ12.DBF" not in packed
|
|
pt, st = DbfTable(packed["PRJLST.DBF"]), DbfTable(src["PRJLST.DBF"])
|
|
assert len(pt) == 1 and pt.get(0, "PCODE") == "11"
|
|
for f in pt.order:
|
|
if f not in ("PCODE", "PRJ_MEMO"):
|
|
assert pt.raw(0, f) == st.raw(1, f), f
|
|
assert _memo_text(packed, 0) == _memo_text(src, 1)
|
|
assert SixMemo(packed["PRJLST.SMT"]).next_free == -(-len(packed["PRJLST.SMT"]) // 64)
|
|
# 16번 §4.6 — 변경1차 파일의 차수1 직접비 = 앞 파일 BOQ12 공사 직접비
|
|
real = DbfTable(_files(SPLIT)["PRJLST.DBF"])
|
|
for c in ("MCOST1", "LCOST1", "SCOST1", "TCOST1"):
|
|
assert float(pt.get(0, c)) == float(real.get(0, c)), c
|
|
assert check_stc(build_stc(read_entries(JOINT.read_bytes()), packed))["ok"]
|
|
|
|
|
|
def test_copy_register_change_mode_opens_next_stage():
|
|
base = pack_selected(_files(JOINT), "12")
|
|
out, pcode, _log = copy_register(base, "11", "change")
|
|
assert pcode == "12" and out["BOQ11.DBF"] == base["BOQ11.DBF"]
|
|
a, b = DbfTable(base["BOQ11.DBF"]), DbfTable(out["BOQ12.DBF"])
|
|
assert len(a) == len(b)
|
|
for i in range(len(a)):
|
|
for f in ("QTY", "MAMT", "LAMT", "SAMT", "TAMT", "M_AMT", "T_AMT", "BDCODE"):
|
|
assert b.raw(i, f"{f}2") == a.raw(i, f"{f}1"), (i, f)
|
|
assert b.raw(i, f"{f}1") == a.raw(i, f"{f}1"), (i, f)
|
|
pt = DbfTable(out["PRJLST.DBF"])
|
|
real = DbfTable(_files(SPLIT)["PRJLST.DBF"])
|
|
assert len(pt) == 2 and pt.get(1, "PCODE") == "12"
|
|
assert pt.get(1, "K_ORD") == real.get(0, "K_ORD") == "1"
|
|
assert pt.get(1, "TCOST1") == pt.get(1, "TCOST2") == real.get(0, "TCOST1")
|
|
secs = {s for s, _k, _v in ini_items(_memo_text(out, 1))}
|
|
assert {"1_KANJUB", "2_KANJUB"} <= secs
|
|
one = {k[2:]: v for s, k, v in ini_items(_memo_text(out, 1)) if s == "1_KANJUB"}
|
|
two = {k[2:]: v for s, k, v in ini_items(_memo_text(out, 1)) if s == "2_KANJUB"}
|
|
assert one == two
|
|
assert _memo_text(out, 0) == _memo_text(base, 0) # 원래 공사 메모 그대로
|
|
|
|
|
|
def test_copy_register_contract_mode_applies_rates_except_excluded():
|
|
src = _files(STCS[0])
|
|
same, _, log = copy_register(src, "11", "contract", rates={"M": 100, "L": 100, "S": 100})
|
|
assert DbfTable(same["BOQ12.DBF"]).recs == DbfTable(src["BOQ11.DBF"]).recs and not log
|
|
out, pcode, _ = copy_register(src, "11", "contract", rates={"M": 87.745, "L": 90, "S": 0})
|
|
a, b = DbfTable(src["BOQ11.DBF"]), DbfTable(out[f"BOQ{pcode}.DBF"])
|
|
spans, excl = boq_spans(a)
|
|
for i in range(len(a)):
|
|
if not _leaf(a.get(i, "BDCODE1")):
|
|
continue
|
|
if i in excl:
|
|
assert b.recs[i] == a.recs[i]
|
|
continue
|
|
assert int(float(b.get(i, "MAMT1"))) == int(float(a.get(i, "MAMT1") or 0) * 0.87745 + 1e-9)
|
|
assert float(b.get(i, "SAMT1") or 0) == 0 # 0% = 공내역
|
|
for r, x, y, isx in spans:
|
|
tot = sum(
|
|
int(float(b.get(k, "T_AMT1") or 0))
|
|
for k in range(x, y + 1)
|
|
if _leaf(b.get(k, "BDCODE1")) and (isx or k not in excl)
|
|
)
|
|
assert int(float(b.get(r, "T_AMT1") or 0)) == tot
|
|
direct = sum(
|
|
int(float(b.get(k, "T_AMT1") or 0))
|
|
for k in range(len(b))
|
|
if _leaf(b.get(k, "BDCODE1")) and k not in excl
|
|
)
|
|
assert int(float(DbfTable(out["PRJLST.DBF"]).get(1, "TCOST1"))) == direct
|
|
|
|
|
|
def test_new_project_opens_empty(work):
|
|
out = new_project(_files(STCS[0]), "새 임도 공사", "2026-01")
|
|
p = work / "new.STC"
|
|
p.write_bytes(build_stc(read_entries(STCS[0].read_bytes()), out))
|
|
assert check_stc(p.read_bytes())["ok"]
|
|
load = server.api_load(server.LoadReq(source_path=str(p)))
|
|
assert load["costn_count"] == 0 and load["boq_count"] == 0
|
|
assert load["prjlst"]["PNAME"] == "새 임도 공사" and load["prjlst"]["PCODE"] == "11"
|
|
assert float(load["prjlst"]["TCOST1"] or 0) == 0
|
|
smt = load["prjlst_smt"]
|
|
assert smt and all(float(v) == 0 for k, v in smt.items() if k.startswith("1_C_"))
|
|
assert any(k.startswith("1_R_") and float(v) > 0 for k, v in smt.items()) # 요율 설정은 틀에서
|
|
|
|
|
|
def test_file_api_endpoints(work):
|
|
src = str(JOINT)
|
|
assert api_file.api_file_check(api_file.PathReq(source_path=src))["ok"]
|
|
r = api_file.api_file_pack(api_file.PackReq(source_path=src, pcode="12", target_name="pk"))
|
|
assert Path(r["saved_stc"]).parent == work and check_stc(Path(r["saved_stc"]).read_bytes())["ok"]
|
|
r2 = api_file.api_file_copy_register(
|
|
api_file.CopyRegReq(source_path=r["saved_stc"], mode="change", target_folder_name="cr")
|
|
)
|
|
assert r2["new_pcode"] == "12" and (work / "cr" / "BOQ12.DBF").exists()
|
|
load = server.api_load(server.LoadReq(source_path=str(work / "cr")))
|
|
assert sorted(load["boq_tables"]) == ["BOQ.DBF", "BOQ11.DBF", "BOQ12.DBF"]
|
|
r3 = api_file.api_file_new(
|
|
api_file.NewPrjReq(template_path=src, pname="새 공사", target_folder_name="nw")
|
|
)
|
|
assert Path(r3["saved_stc"]).exists()
|
|
api_file.api_file_new(api_file.NewPrjReq(template_path=src, pname="새 공사2", target_folder_name="nw"))
|
|
bks = api_file.api_file_backups("nw")["backups"]
|
|
assert len(bks) == 1
|
|
api_file.api_file_restore(api_file.RestoreReq(backup_path=bks[0]["path"], target_folder_name="nw"))
|
|
assert server.api_load(server.LoadReq(source_path=str(work / "nw")))["prjlst"]["PNAME"] == "새 공사"
|
|
with pytest.raises(Exception):
|
|
api_file.api_file_restore(api_file.RestoreReq(backup_path=src, target_folder_name="x"))
|
|
|
|
|
|
@pytest.mark.parametrize("stc", STCS, ids=IDS)
|
|
def test_load_reads_side_tables_and_all_settings(stc):
|
|
load = server.api_load(server.LoadReq(source_path=str(stc)))
|
|
assert load["kongjong_rows"] and all("CODE" in r for r in load["kongjong_rows"])
|
|
assert load["costa_count"] == 0 and load["qcode_rows"] == []
|
|
rows = load["prjlst_smt_all"]
|
|
assert len(rows) == len(load["prjlst_all"])
|
|
first = rows[0]
|
|
assert first["key_count"] >= len(load["prjlst_smt"]) > 100 and "KANJUB" in first["sections"]
|
|
for k, v in load["prjlst_smt"].items(): # 첫 공사 읽기(parse_prjlst_smt)와 같은 값
|
|
assert any(s.get(k) == v for s in first["sections"].values()), k
|
|
assert "START" in load["start_ini_sections"]
|