- 변경 차수 상한 4차(K_ORD>4 거부) · 4차 = 5번째 칸(MCOST5·XX5NM·QTY5) - 할증 계수 float32 — 70%·115% 1원 적음 - 손료 E0~E3 = 조달청 XML 구분값뿐 · 계수 곱 없음 - jdl_format.py — 13종 자르는 자리 · 단가 목록 8종 열 이름 · 줄 만들기·읽기·검사 - 지식DB 34번 §17 · REA_MCP_보강 근거 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
4.0 KiB
Python
105 lines
4.0 KiB
Python
"""STMATE 재현 앱 — 조달청 JDL TEXT 고정폭 형식(지식DB REA 34 「REA MCP 보강」) 지킴 시험 · 할증율 4바이트 계수 · 변경 4차 칸.
|
|
|
|
근거: JDLCHK.Tab_33.dll 파일마다 자르기 자리 표(0x405dd8 뒤) · 열 개수 검사(0x405bfc 뒤 cmp) · FieldDefs(0x491dec)
|
|
M.Txt 검사 함수 0x4ea818 Ghidra 의사코드 — 열 0 F_CODE · 1 F_DES · 2 F_SIZE · 3 F_UNIT · 4 F_MPRICE · 5 F_SCODE · 6~17 가격·쪽 · 18 F_BIGO
|
|
할증율 = ST_Boq 0x4e9b20 fstp dword · 0x4e9bde fmul dword · 변경 4차 = PRJLST MCOST5·XX5NM · ST_Prt 0x4b3183
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
|
|
import jdl_format as J # noqa: E402
|
|
from kanjub_cfg import stage_slots # noqa: E402
|
|
|
|
# 검사기 cmp 값(열 개수) — 자르기 자리 + 1 과 같아야 함
|
|
CMP = {
|
|
"Main.Txt": 3,
|
|
"M.Txt": 0x13,
|
|
"L.Txt": 7,
|
|
"E.Txt": 9,
|
|
"W.Txt": 9,
|
|
"J.Txt": 9,
|
|
"GongJong.Txt": 4,
|
|
}
|
|
CMP |= {
|
|
"Unit.Txt": 10,
|
|
"JUnit.Txt": 10,
|
|
"Danga.Txt": 10,
|
|
"MainPart.Txt": 0x1F,
|
|
"UnitPart.Txt": 0x1C,
|
|
"JPart.Txt": 0x13,
|
|
}
|
|
|
|
|
|
def test_cut_tables_match_column_checks():
|
|
for name, n in CMP.items():
|
|
assert len(J.CUTS[name]) + 1 == n, name
|
|
if name in J.COLUMNS:
|
|
assert len(J.COLUMNS[name]) == n, name
|
|
|
|
|
|
def test_price_widths_follow_field_defs():
|
|
"""단가 목록 너비 = FieldDefs 크기(코드 12 · 명칭·규격 30 · 단위 6 · 단가 12 · 쪽 6 · 손료번호 4)."""
|
|
size = {"F_CODE": 12, "F_DES": 30, "F_SIZE": 30, "F_UNIT": 6, "F_MPRICE": 12, "F_LPRICE": 12}
|
|
size |= {"F_EPRICE": 12, "F_SCODE": 12, "F_TFNO": 4} | {f"F_PRICE{i}": 12 for i in range(1, 7)}
|
|
size |= {f"F_PAGE{i}": 6 for i in range(1, 7)}
|
|
for name, cols in J.COLUMNS.items():
|
|
for col, w in zip(cols, J.widths(name)):
|
|
assert w == size.get(col), (name, col, w)
|
|
|
|
|
|
def test_line_roundtrip_and_checks():
|
|
row = {
|
|
"F_CODE": "M00001",
|
|
"F_DES": "레미콘 25-21-120",
|
|
"F_SIZE": "",
|
|
"F_UNIT": "M3",
|
|
"F_MPRICE": "93000",
|
|
}
|
|
row |= {"F_SCODE": "", "F_PRICE1": "92500", "F_PAGE1": "123", "F_BIGO": "비고"}
|
|
b = J.build_line("M.Txt", row)
|
|
assert b[:12] == b"M00001 " and len(b) == 210 + len("비고".encode("cp949"))
|
|
back = J.split_line("M.Txt", b)
|
|
assert back[0] == "M00001" and back[1] == "레미콘 25-21-120" and back[6:8] == ["92500", "123"]
|
|
assert back[18] == "비고"
|
|
assert J.check_lines("M.Txt", b + b"\r\n") == []
|
|
errs = J.check_lines(
|
|
"L.Txt", J.build_line("L.Txt", ["l0001", "x", "", "", "1", "", ""]) + b"\r\nL00002"
|
|
)
|
|
assert any("코드 글자" in e for e in errs) and any("Line : 2" in e for e in errs)
|
|
long = J.build_line("L.Txt", ["L00001", "가" * 20, "", "", "", "", ""])
|
|
assert len(long[12:42]) == 30 and long[12:42].decode("cp949").strip() == "가" * 15
|
|
|
|
|
|
def _f32(x):
|
|
return struct.unpack("<f", struct.pack("<f", x))[0]
|
|
|
|
|
|
def test_hal_rate_float32_factor():
|
|
"""할증 계수 4바이트 실수 — 70%·115% 에서 십진 계산보다 1원 적음."""
|
|
helper = Path(__file__).with_name("helper_stmate_kanjub_rea.cjs")
|
|
rows = [{"BDCODE1": "M00001", "_recno": 1, "SORTCODE": "1", "ENDCODE": "0"}]
|
|
for pct, base in ((70, 1000), (115, 1000), (107.5, 2000)):
|
|
want = math.trunc(base * _f32(pct / 100))
|
|
out = json.loads(
|
|
subprocess.check_output(
|
|
["node", str(helper)],
|
|
input=json.dumps({"mode": "halbase", "rows": rows, "pct": pct, "base": base}),
|
|
text=True,
|
|
encoding="utf-8",
|
|
)
|
|
)
|
|
assert out == [want] * 3, (pct, out)
|
|
assert math.trunc(1000 * _f32(0.7)) == 699 and math.trunc(1000 * _f32(1.15)) == 1149
|
|
|
|
|
|
def test_fifth_series_slot():
|
|
"""변경 4차 = 계열 5 — 저장 칸 5 를 차수로 읽음."""
|
|
assert stage_slots({"1_C_KANNO": 1, "5_C_KANNO": 2}) == [1, 5]
|