217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
"""STMATE 재현 앱 엑셀 가져오기 — 원본 STMATE 가 내보낸 xlsx(숨은 「〓 INITIAL 〓」·INFO 열)로 STC 를 되살림.
|
|
|
|
정답 = 같은 공사의 실무 STC(BOQ · COSTN · COSTN.SMT · BDQTY 복호 · PRJLST). 코드에 값을 박지 않음.
|
|
울진 = 신설사업 xlsx(착공내역서) ↔ 신설사업 STC 의 BOQ12(현재안 · 16번 §4.5).
|
|
쓰기는 임시 폴더에서만.
|
|
"""
|
|
|
|
import collections
|
|
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 bdqty import decode_bdqty # noqa: E402
|
|
from conv_xls_apply import import_boq, register # noqa: E402
|
|
from conv_xls_boq import boq_from_xlsx, fill_from_costn, load_book, read_initial # noqa: E402
|
|
from conv_xls_out import ( # noqa: E402
|
|
export_boq_xlsx,
|
|
export_jaje_xlsx,
|
|
import_jaje_xlsx,
|
|
)
|
|
from conv_xls_res import comp_from_san, comp_from_sheets, costn_from_lists, memos_from_sheet # noqa: E402
|
|
from dbf_rw import DbfTable # noqa: E402
|
|
from smt_parser import extract_costn_memos, parse_dbf_bytes # noqa: E402
|
|
from stc_project import write_output # noqa: E402
|
|
from stc_read import Source # noqa: E402
|
|
from stc_save import apply_save # noqa: E402
|
|
|
|
DOCS = ROOT / "resources/knowledge/original/실무문서"
|
|
SO = (
|
|
"25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외)"
|
|
)
|
|
YD = "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경)"
|
|
PAIRS = { # 이름: (STC, xlsx, 비교 표)
|
|
"봉화": (
|
|
"04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC",
|
|
"04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx",
|
|
"BOQ11.DBF",
|
|
),
|
|
"영월": (
|
|
"05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC",
|
|
"05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
|
"BOQ11.DBF",
|
|
),
|
|
"대흥": (
|
|
"2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC",
|
|
"2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx",
|
|
"BOQ11.DBF",
|
|
),
|
|
"울진": (SO + ".STC", SO + ".xlsx", "BOQ12.DBF"),
|
|
"영덕변경": (YD + ".STC", YD + ".xlsx", "BOQ11.DBF"),
|
|
}
|
|
F = (
|
|
"BDCODE1 ITNUM NAME1 SIZE1 UNIT1 QTY1 TAMT1 LAMT1 MAMT1 SAMT1 T_AMT1 L_AMT1 M_AMT1 S_AMT1 "
|
|
"EXIST1 SORTCODE ENDCODE EQCODE PRIN PROUT RCU KWNGP BL_AMT BQCODE"
|
|
).split()
|
|
F2 = "QTY2 TAMT2 LAMT2 MAMT2 SAMT2 T_AMT2 L_AMT2 M_AMT2 S_AMT2".split()
|
|
|
|
|
|
def same(a, b) -> bool:
|
|
try:
|
|
return abs(float(a or 0) - float(b or 0)) < 1e-6
|
|
except (TypeError, ValueError):
|
|
sa, sb = ("" if a is None else str(a)), ("" if b is None else str(b))
|
|
return sa.rstrip() == sb.rstrip() or {sa.strip(), sb.strip()} <= {"", "F"}
|
|
|
|
|
|
def _pair(name):
|
|
stc, xl, tb = PAIRS[name]
|
|
return Source(str(DOCS / stc)).files, DOCS / xl, tb
|
|
|
|
|
|
def _diff(rows, got, fields):
|
|
bad = [
|
|
(i, f, a.get(f), b.get(f))
|
|
for i, (a, b) in enumerate(zip(rows, got))
|
|
for f in fields
|
|
if f in a and not same(a.get(f), b.get(f))
|
|
]
|
|
return bad
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PAIRS))
|
|
def test_xlsx_boq_rows_reproduce_stc(name):
|
|
files, xl, tb = _pair(name)
|
|
got = boq_from_xlsx(xl)["rows"]
|
|
fill_from_costn(got, parse_dbf_bytes(files["COSTN.DBF"])[1])
|
|
rows = parse_dbf_bytes(files[tb])[1]
|
|
assert len(got) == len(rows)
|
|
fields = F + (F2 if name == "영덕변경" else []) # 변경 내역서만 2차 줄이 있음
|
|
bad = _diff(rows, got, fields)
|
|
assert not bad, f"{name} {len(bad)}칸 {bad[:5]}"
|
|
|
|
|
|
def _sem(t: DbfTable, i: int, f: str):
|
|
v = t.get(i, f)
|
|
if t.fields[f].type in ("N", "F"):
|
|
return round(float(v or 0), 6)
|
|
return (v or "F") if t.fields[f].type == "L" else v
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PAIRS))
|
|
def test_import_boq_writes_same_table_and_direct_cost(name):
|
|
files, xl, tb = _pair(name)
|
|
out, rep = import_boq(files, xl, tb)
|
|
assert rep["unknown_codes"] == []
|
|
a, b = DbfTable(files[tb]), DbfTable(out[tb])
|
|
assert len(a) == len(b)
|
|
skip = {"QTY2"} if name == "울진" else set() # 착공 엑셀에 없는 2차 수량
|
|
bad = [
|
|
(i, f)
|
|
for i in range(len(a))
|
|
for f in a.order
|
|
if a.fields[f].type != "M" and f not in skip and _sem(a, i, f) != _sem(b, i, f)
|
|
]
|
|
assert not bad, f"{name} {len(bad)}칸 {bad[:5]}"
|
|
assert out["PRJLST.DBF"] == files["PRJLST.DBF"] # 소계 다시 셈 = 저장 직접비(?COSTn) 그대로
|
|
|
|
|
|
@pytest.mark.parametrize("name", list(PAIRS))
|
|
def test_lists_memos_and_composition(name):
|
|
files, xl, _tb = _pair(name)
|
|
wb = load_book(xl)
|
|
info = read_initial(wb)
|
|
cm = {r["CODE"].strip(): r for r in parse_dbf_bytes(files["COSTN.DBF"])[1]}
|
|
got = costn_from_lists(wb, info)
|
|
assert got and all(r["CODE"] in cm for r in got)
|
|
bad = [(r["CODE"], k) for r in got for k, v in r.items() if not same(cm[r["CODE"]].get(k), v)]
|
|
assert not bad, bad[:5]
|
|
memo = extract_costn_memos(files["COSTN.DBF"], files["COSTN.SMT"])
|
|
mm = memos_from_sheet(wb, info)
|
|
nl = lambda t: t.replace("\r\n", "\n").rstrip() # noqa: E731
|
|
assert mm and all(nl(memo[c]["text"]) == nl(t) for c, t in mm.items())
|
|
gd = collections.defaultdict(list)
|
|
for r in decode_bdqty(files["BDQTY.DBF"]):
|
|
gd[r["BDCODE"]].append(r)
|
|
gc = collections.defaultdict(list)
|
|
for r in comp_from_sheets(wb, info) + comp_from_san(wb, info):
|
|
gc[r["BDCODE"]].append(r)
|
|
assert gc
|
|
for c, rows in gc.items():
|
|
assert [(r["CODE"], round(float(r["QTY1"]), 9)) for r in gd[c]] == [
|
|
(r["CODE"], round(float(r["QTY1"]), 9)) for r in rows
|
|
], c
|
|
for a, b in zip(gd[c], rows):
|
|
for k in ("DEVD1", "BL_AMT"):
|
|
if k in b:
|
|
assert (a.get(k) or "F") == (b[k] or "F"), (c, a["CODE"], k)
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["봉화", "영월", "대흥"])
|
|
def test_register_rebuilds_project(name, tmp_path):
|
|
files, xl, tb = _pair(name)
|
|
out, rep = register(files, xl)
|
|
pa, pb = DbfTable(files["PRJLST.DBF"]), DbfTable(out["PRJLST.DBF"])
|
|
for f in ("PNAME", "MCOST1", "LCOST1", "SCOST1", "TCOST1"):
|
|
assert pb.get(0, f) == pa.get(0, f), f
|
|
assert DbfTable(out["COSTN.DBF"]).recs == DbfTable(files["COSTN.DBF"]).recs or rep[
|
|
"costn_count"
|
|
] == len(DbfTable(files["COSTN.DBF"]))
|
|
d0 = collections.Counter(
|
|
(r["BDCODE"], r["CODE"], r["QTY1"]) for r in decode_bdqty(files["BDQTY.DBF"])
|
|
)
|
|
d1 = collections.Counter(
|
|
(r["BDCODE"], r["CODE"], r["QTY1"]) for r in decode_bdqty(out["BDQTY.DBF"])
|
|
)
|
|
assert d0 == d1
|
|
m0 = extract_costn_memos(files["COSTN.DBF"], files["COSTN.SMT"])
|
|
m1 = extract_costn_memos(out["COSTN.DBF"], out["COSTN.SMT"])
|
|
assert {c: v["text"].rstrip() for c, v in m0.items()} == {
|
|
c: v["text"].rstrip() for c, v in m1.items()
|
|
}
|
|
s = Source(str(DOCS / PAIRS[name][0]))
|
|
res = write_output(tmp_path, "reg", out, s.names, s.entries)
|
|
load = server.api_load(server.LoadReq(source_path=res["saved_stc"]))
|
|
assert (
|
|
load["boq_count"] == len(DbfTable(files[tb])) and load["costn_count"] == rep["costn_count"]
|
|
)
|
|
|
|
|
|
STCS = sorted(DOCS.rglob("*.STC"))
|
|
|
|
|
|
@pytest.mark.parametrize("stc", STCS, ids=[p.stem[:16] for p in STCS])
|
|
def test_own_boq_xlsx_roundtrip(stc):
|
|
files = Source(str(stc)).files
|
|
for tb in [k for k in ("BOQ11.DBF", "BOQ12.DBF") if k in files]:
|
|
got = boq_from_xlsx(export_boq_xlsx(files, tb))["rows"]
|
|
fill_from_costn(got, parse_dbf_bytes(files["COSTN.DBF"])[1])
|
|
rows = parse_dbf_bytes(files[tb])[1]
|
|
assert len(got) == len(rows)
|
|
bad = _diff(rows, got, F)
|
|
assert not bad, f"{tb} {bad[:5]}"
|
|
|
|
|
|
def test_jaje_xlsx_roundtrip_and_edit(tmp_path):
|
|
import openpyxl
|
|
|
|
files, _xl, _tb = _pair("봉화")
|
|
x = export_jaje_xlsx(files)
|
|
assert import_jaje_xlsx(files, x) == []
|
|
p = tmp_path / "j.xlsx"
|
|
p.write_bytes(x)
|
|
wb = openpyxl.load_workbook(p)
|
|
ws = wb.worksheets[0]
|
|
code = ws.cell(2, 1).value
|
|
old = float(ws.cell(2, 10).value or 0)
|
|
ws.cell(2, 10).value = old + 100
|
|
wb.save(p)
|
|
ups = import_jaje_xlsx(files, p.read_bytes())
|
|
assert ups == [{"CODE": code, "AMT_61": old + 100, "MAMT1": old + 100, "TAMT1": old + 100}]
|
|
out, log = apply_save(files, {"costn_updates": ups})
|
|
assert {x["field"] for x in log if x["table"] == "COSTN.DBF"} <= {"AMT_61", "MAMT1", "TAMT1"}
|