refactor(stmate-test): 원본에 없는 「자재 기준자료 엑셀 내보내기·불러오기」 걷음

- 원본 wM_JajeXLS = 재료비 수량집계표 엑셀 내보내기 한 방향뿐(REA 37 · TWM_JAJEXLS 0x5658bc) — 추측으로 만든 자재 기준자료 왕복 메뉴·API·시험 삭제
- 남김: 「재료비 수량집계표를 엑셀파일로 만들기」(conv_jaje · scr_jaje)
- 낡은 시험 test_jaje_xlsx_roundtrip_and_edit · conv_api 의 jaje 왕복 삭제 — 원본에 없는 기능

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-14 17:43:12 +09:00
co-authored by Claude Opus 5.5
parent f0ba52afa3
commit b9847c7db8
6 changed files with 3 additions and 134 deletions
+1 -6
View File
@@ -56,7 +56,7 @@ def test_boq_xlsx_download_reimports(work):
assert Source(r["saved_stc"]).files["PRJLST.DBF"] == Source(str(STC)).files["PRJLST.DBF"]
def test_code_and_split_and_jaje_api(work):
def test_code_and_split_api(work):
r = A.api_code_change(
A.CodeChgReq(source_path=str(STC), mapping={"M00001": "M99991"}, target_folder_name="cc")
)
@@ -68,10 +68,5 @@ def test_code_and_split_and_jaje_api(work):
assert p.get(0, "K_ORD") == "1" and p.get(0, "TCOST2") == p.get(0, "TCOST1") == _prj(
str(STC)
).get(0, "TCOST1")
x = A.api_jaje_xlsx(A.SrcReq(source_path=str(STC))).body
r3 = A.api_jaje_import(
A.JajeImportReq(source_path=str(STC), xlsx=A.FileIn(b64=base64.b64encode(x).decode()))
)
assert r3["count"] == 0
with pytest.raises(Exception):
A.api_jb_read(A.JbReq(path=str(STC))) # 설치 폴더 JB_* 만
-23
View File
@@ -19,15 +19,12 @@ 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 = (
@@ -196,26 +193,6 @@ def test_own_boq_xlsx_roundtrip(stc):
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"}
# 영월 BDQTY 는 복호 칸이 어긋남(UNIT1 깨짐 · TPRPT 전부 4) — 비교 제외
@pytest.mark.parametrize("name", ["봉화", "대흥", "울진", "영덕변경"])
def test_comp_rows_follow_xls_2_bdrec(name):
+1 -1
View File
@@ -212,7 +212,7 @@
- [x] F4-24 엑셀 → 내역 불러오기 — wM_From_XLS · 현재: 구현(conv_xls_boq · 원본 엑셀 5권 → STC 내역 행 전 칸 일치(test_stmate_conv_xls))
- [x] F4-25 엑셀 역등록(CONV_XLS_2_ST·INITIAL)·엑셀 → BDQTY — 24번 §3 · 현재: 구현(REA 37 — ST_P_XLS 0x5f1078 PRPT「속성」·PRIN「대상」·PROUT「저장」 = PR/RR 줄만 · 원본 BDQTY 대조 4건 일치)
- [x] F4-30 BDQTY.DBF 구성표 복호 읽기 — STC_난독화_상태 · 현재: 구현(bdqty.py 자리별 회전·더하기 표 283자리 · 사용자 승인 · 글자 칸 제외)
- [x] F4-26 자재 기준자료 XLS 입출력 — wM_JajeXLS · 현재: 구현(REA 37 — 원본 = 재료비 수량집계표 엑셀 내보내기만(TWM_JAJEXLS) · 주요건설자재현황 시트 · 웹 들여오기는 원본에 없음 · 파일 이름 기본값 미확인)
- [x] F4-26 자재 기준자료 XLS 입출력 — wM_JajeXLS · 현재: 구현(REA 37 — 원본 = 재료비 수량집계표 엑셀 내보내기만(TWM_JAJEXLS) · 주요건설자재현황 시트(conv_jaje·scr_jaje) · 원본에 없는 「자재 기준자료 엑셀 내보내기·불러오기」 걷음 · 파일 이름 기본값 미확인)
- [x] F4-27 기준 BOQ·코드 변경 엑셀 왕복 — 24번 §3 · ST_U_ChgCode · 현재: 구현(stc_codechg 내역 8표 엑셀 왕복 · 코드 변경 되돌리면 7건 원래 바이트)
- [x] F4-28 구버전 → Win8 자료 변환 — ST_U_Convert ST_ConvToWin8 · 현재: 구현(REA 37 — 판 가리기(Chk_Data 0x459280) · 7.x/DOS COSTN·BDQTY·PRJLST·KONGJONG·BOQ·Start8x.Ini 대응 · B→X 조건 = 단위 X · 합성 자료 시험 · 간접비 이관·RATE·SYSINFO·IFORM 미확인(틀 유지))
- [x] F4-29 자동백업·자료 복원 — wM_D_Restore · wM_ST_Restore · 현재: 구현(api_file 자동 백업·복원)
-21
View File
@@ -21,9 +21,7 @@ from conv_xls_apply import import_boq, register
from conv_xls_out import (
export_boq_xlsx,
export_code_xlsx,
export_jaje_xlsx,
import_code_xlsx,
import_jaje_xlsx,
)
from conv_xml import xml_to_project
from conv_xml_cfg import apply_settings, options, validate
@@ -118,11 +116,6 @@ class SrcReq(BaseModel):
table: str = ""
class JajeImportReq(BaseModel):
source_path: str
xlsx: FileIn
class CodeChgReq(BaseModel):
source_path: str
mapping: dict[str, str] = {}
@@ -256,20 +249,6 @@ def api_boq_xlsx(req: SrcReq):
return _xlsx(_guard(export_boq_xlsx, s.files, req.table), f"{s.path.stem}_기준BOQ.xlsx")
@router.post("/api/conv/jaje-xlsx")
def api_jaje_xlsx(req: SrcReq):
s = _src(req.source_path)
return _xlsx(_guard(export_jaje_xlsx, s.files), f"{s.path.stem}_자재기준자료.xlsx")
@router.post("/api/conv/jaje-import")
def api_jaje_import(req: JajeImportReq):
"""자재 기준자료 엑셀 → costn_updates(바뀐 칸만) — 화면이 /api/save 로 넘김."""
s = _src(req.source_path)
ups = _guard(import_jaje_xlsx, s.files, _blob(**req.xlsx.model_dump()))
return {"costn_updates": ups, "count": len(ups)}
@router.post("/api/conv/code-xlsx")
def api_code_xlsx(req: SrcReq):
s = _src(req.source_path)
+1 -60
View File
@@ -17,7 +17,7 @@ import io
import re
from conv_xls_boq import COLS, INITIAL, LEVEL_NAME, _numv, load_book
from smt_parser import parse_dbf_bytes, parse_ini_bytes
from smt_parser import parse_dbf_bytes
from stc_save import STRUCT_RE, pick_boq_table
BOQ_HEAD = [
@@ -146,63 +146,6 @@ def export_boq_xlsx(files: dict, table: str = "") -> bytes:
return buf.getvalue()
# ── 자재 기준자료 ──
JAJE_HEAD = ["코드", "명 칭", "규 격", "단위"]
def export_jaje_xlsx(files: dict) -> bytes:
"""COSTN M 코드 → 자재 기준자료 xlsx(출처 5칸 · 적용 단가)."""
ini = parse_ini_bytes(files.get("START8X.INI") or files.get("STINI.MEM") or b"")
src = [ini.get(f"JMOK{i}", f"출처{i}") for i in range(1, 6)]
wb = _wb()
ws = wb.active
ws.title = "자재기준자료"
ws.append(JAJE_HEAD + src + ["적용 단가", "BQCODE", "JDCODE"])
for r in parse_dbf_bytes(files["COSTN.DBF"])[1]:
if not r["CODE"].strip().startswith("M"):
continue
ws.append(
[r["CODE"].strip(), r["NAME"], r["SIZE"], r["UNIT"].strip()]
+ [_numv(r.get(f"AMT_{i}1")) for i in range(1, 7)]
+ [r.get("BQCODE", "").strip(), r.get("JDCODE", "").strip()]
)
buf = io.BytesIO()
wb.save(buf)
return buf.getvalue()
def import_jaje_xlsx(files: dict, src) -> list[dict]:
"""자재 기준자료 xlsx → 바뀐 칸만 담은 costn_updates(없는 코드는 새 행)."""
ws = load_book(src).worksheets[0]
cur = {r["CODE"].strip(): r for r in parse_dbf_bytes(files["COSTN.DBF"])[1]}
head = [str(v or "").strip() for v in next(ws.iter_rows(max_row=1, values_only=True))]
fields = (
["CODE", "NAME", "SIZE", "UNIT"] + [f"AMT_{i}1" for i in range(1, 7)] + ["BQCODE", "JDCODE"]
)
out = []
for r in ws.iter_rows(min_row=2, values_only=True):
if not r or not r[0]:
continue
vals = dict(zip(fields, r[: len(fields)]))
code = str(vals["CODE"]).strip()
old = cur.get(code)
upd = {"CODE": code}
for k, v in vals.items():
if k == "CODE" or k not in fields[: len(head)]:
continue
o = old.get(k) if old else None
if k.startswith("AMT_"):
if old is None or abs(_numv(o) - _numv(v)) > 1e-9:
upd[k] = _numv(v)
elif old is None or str(o or "").strip() != str(v or "").strip():
upd[k] = "" if v is None else str(v)
if "AMT_61" in upd: # 적용 단가 = 1차 재료비 단가
upd["MAMT1"] = upd["TAMT1"] = upd["AMT_61"]
if len(upd) > 1:
out.append(upd)
return out
# ── 코드 변경표 ──
def export_code_xlsx(files: dict) -> bytes:
wb = _wb()
@@ -234,8 +177,6 @@ def level_label(code: str) -> str:
__all__ = [
"INITIAL",
"export_boq_xlsx",
"export_jaje_xlsx",
"import_jaje_xlsx",
"export_code_xlsx",
"import_code_xlsx",
"level_label",
-23
View File
@@ -152,27 +152,6 @@
`${pname(ctx)}_기준BOQ.xlsx`,
);
},
async jajeXlsx(ctx) {
download(
await req("/api/conv/jaje-xlsx", { source_path: curPath(ctx) }, true),
`${pname(ctx)}_자재기준자료.xlsx`,
);
},
async jajeImport(ctx) {
const f = await pickFile(".xlsx");
const r = await req("/api/conv/jaje-import", {
source_path: curPath(ctx),
xlsx: { b64: f.b64 },
});
const rows = ctx.dbfData.costn_rows || (ctx.dbfData.costn_rows = []);
for (const u of r.costn_updates) {
const row = rows.find((x) => String(x.CODE).trim() === u.CODE);
if (row) Object.assign(row, u);
else rows.push(Object.assign({}, u));
}
app().cascadeAndRender && app().cascadeAndRender();
alert(`[자재 기준자료 불러오기] 바뀐 자재 ${r.count}건 — [저장]으로 반영`);
},
async codeXlsx(ctx) {
download(
await req("/api/conv/code-xlsx", { source_path: curPath(ctx) }, true),
@@ -236,8 +215,6 @@
["Act_ConvXlsBoq", "엑셀 → 내역 불러오기 (wM_From_XLS)", "xlsBoq"],
["Act_ConvXlsRegister", "엑셀 역등록 — 새 공사 (CONV_XLS_2_ST)", "xlsRegister"],
["Act_ConvBoqXlsx", "기준 BOQ 엑셀로 내보내기 (MK_BSCBOQ_EXL)", "boqXlsx"],
["Act_ConvJajeXlsx", "자재 기준자료 엑셀로 내보내기 (wM_JajeXLS)", "jajeXlsx"],
["Act_ConvJajeImport", "자재 기준자료 엑셀 불러오기 (wM_JajeXLS)", "jajeImport"],
["Act_ConvCodeXlsx", "코드 변경표 엑셀로 내보내기 (ST_U_ChgCode)", "codeXlsx"],
["Act_ConvCodeChange", "코드 일괄 변경 (ST_U_ChgCode)", "codeChange"],
["Act_ConvSplitChange", "분리형 변경 STC 만들기 — 차수 인계", "splitChange"],