- ST_Prt 0x44fc34 서식 번호 → 출력 함수 표 · 0609 = Prn_Won_A · 0608 = Prn_Wonga - IFORM = VTS·XLS 파일 바이트 · .XLS·.VTS · 16MB 거절 · ReadDefName 「이름=값」 · QTYn 칸 - 사·관급내역서 관급 구역 품목 줄 누락 수정 - 장부 F3-23 · F3-26 · F5-22 구현 · 지식DB 35번 · stmate 시험 732건 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.7 KiB
Python
129 lines
5.7 KiB
Python
"""STMATE 재현 앱 수량산출서 파일(IFORM) — REA 35(STmateQty8 TwQtyList) 규칙 대조.
|
|
|
|
규칙: IFORM = 수량산출서 파일 바이트(Formula One VTS · 엑셀 XLS) 그대로 · 불러오기 확장자 .XLS·.VTS ·
|
|
16MB(0x1000000) 넘으면 거절 · 모두 내보내기 = 「수량산출서」 폴더 <CODE>.VTS ·
|
|
ReadDefName = 셀별명(인쇄 영역·내역 전송 일곱 이름 빼고) → XCODE 「이름=FloatToStr(값)」CRLF · QTYn 이름 → QTYn 칸 ·
|
|
내역 전송 = 내역시작·내역__끝·내역코드가 한 시트에 있어야.
|
|
"""
|
|
|
|
import base64
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
import api_qty # noqa: E402
|
|
import qty_qcode # noqa: E402
|
|
import qty_sheetfile # noqa: E402
|
|
import server # noqa: E402
|
|
from test_stmate_qty_sheet import STC, call # noqa: E402
|
|
|
|
XLS = sorted((ROOT / "resources/knowledge/original/실무문서").rglob("4.1 수량산출(1공구).xls"))
|
|
|
|
|
|
def test_upload_checks_follow_original_messages():
|
|
assert qty_sheetfile.check_upload("a.txt", b"x") == qty_sheetfile.EXT_MSG
|
|
assert qty_sheetfile.check_upload("a.vts", b"x") is None
|
|
assert qty_sheetfile.check_upload("A.XLS", b"\0" * (0x1000000 + 1)) == qty_sheetfile.SIZE_MSG
|
|
assert qty_sheetfile.check_upload("A.XLS", b"\0" * 0x1000000) is None
|
|
with pytest.raises(HTTPException) as e:
|
|
api_qty.api_qty_sheet_file(api_qty.SheetFileReq(filename="a.csv", data_b64=""))
|
|
assert e.value.detail == qty_sheetfile.EXT_MSG
|
|
assert qty_sheetfile.kind_of(b"") == "" and qty_sheetfile.kind_of(b"VTS!") == "vts"
|
|
|
|
|
|
@pytest.mark.skipif(not XLS, reason="실무 수량산출 엑셀 없음")
|
|
def test_real_excel_quantity_sheet_parses_and_flags_biff8():
|
|
data = XLS[0].read_bytes()
|
|
res = api_qty.api_qty_sheet_file(
|
|
api_qty.SheetFileReq(filename=XLS[0].name, data_b64=base64.b64encode(data).decode())
|
|
)
|
|
wb = res["workbook"]
|
|
assert res["kind"] == "xls" and res["size"] == len(data)
|
|
# 엑셀 97 이후(BIFF8) = 원본 Formula One 이 받는 5.0/95 가 아님 — 원본 알림 글
|
|
assert wb["biff"] == 80 and not wb["compatible"] and res["warning"] == qty_sheetfile.XLS_MSG
|
|
assert len(wb["sheets"]) > 5 and any(s["cells"] for s in wb["sheets"])
|
|
assert all(n["addr"] is None for n in wb["names"] if n["name"].upper() == "PRINT_AREA")
|
|
|
|
|
|
def test_iform_bytes_round_trip_and_export(tmp_path, monkeypatch):
|
|
blob = qty_sheetfile.OLE_MAGIC + bytes(range(256)) * 3
|
|
rows = [
|
|
{"CODE": "Q00001", "NAME": "토공", "IFORM_B64": base64.b64encode(blob).decode()},
|
|
{"CODE": "Q00002", "NAME": "빈 것"},
|
|
]
|
|
files = server.read_project_files(str(STC[0]))
|
|
dbf, smt = qty_qcode.build_qcode(rows, files)
|
|
back = qty_qcode.read_qcode({**files, "QCODE.DBF": dbf, "QCODE.SMT": smt})["rows"]
|
|
assert base64.b64decode(back[0]["IFORM_B64"]) == blob
|
|
assert back[0]["IFORM_KIND"] == "xls" and back[0]["IFORM_SIZE"] == len(blob)
|
|
assert back[1]["IFORM_B64"] == "" and back[1]["IFORM_KIND"] == ""
|
|
monkeypatch.setattr(server, "WORK_DBF_ROOT", tmp_path)
|
|
saved = api_qty.api_qty_qcode_save(
|
|
api_qty.QcodeSaveReq(source_path=str(STC[0]), target_folder_name="qty파일", rows=rows)
|
|
)
|
|
out = api_qty.api_qty_sheet_export(
|
|
api_qty.SheetExportReq(source_path=saved["saved_stc"], target_folder_name="qty파일")
|
|
)
|
|
assert out["files"] == ["Q00001.VTS"]
|
|
assert (Path(out["saved_dir"]) / "Q00001.VTS").read_bytes() == blob
|
|
assert Path(out["saved_dir"]).name == "수량산출서"
|
|
|
|
|
|
def test_read_def_name_rule_and_workbook_roles():
|
|
wb = {
|
|
"sheets": [
|
|
{
|
|
"name": "수량",
|
|
"rows": 8,
|
|
"cols": 6,
|
|
"cells": {
|
|
"A1": 12.5,
|
|
"B1": "=A1*2",
|
|
"A3": "B00001",
|
|
"B3": "토공",
|
|
"C3": 4,
|
|
"D3": 100,
|
|
"A4": "",
|
|
"A5": "M00002",
|
|
"B5": "자갈",
|
|
"C5": 2,
|
|
"D5": 50,
|
|
},
|
|
}
|
|
],
|
|
"names": [
|
|
{"name": "Print_Area", "sheet": 0, "addr": "A1"},
|
|
{"name": "Qty0", "sheet": 0, "addr": "B1"},
|
|
{"name": "절토", "sheet": 0, "addr": "A1"},
|
|
{"name": "내역코드", "sheet": 0, "addr": "A3"},
|
|
{"name": "내역명칭", "sheet": 0, "addr": "B3"},
|
|
{"name": "내역수량", "sheet": 0, "addr": "C3"},
|
|
{"name": "자재단가", "sheet": 0, "addr": "D3"},
|
|
{"name": "내역시작", "sheet": 0, "addr": "A3"},
|
|
{"name": "내역__끝", "sheet": 0, "addr": "A5"},
|
|
],
|
|
}
|
|
conv, nums = call(("fromWorkbook", wb, 0), ("floatToStr", 0.1 + 0.2))
|
|
sh = conv["sheet"]
|
|
assert conv["sendable"] and sh["start"] == 2 and sh["end"] == 4
|
|
assert sh["roles"] == {"code": "A", "name": "B", "qty": "C", "mamt": "D"}
|
|
assert "Print_Area" not in sh["aliases"] and sh["aliases"]["절토"] == "A1"
|
|
asg, boq, f2, f3, f4 = call(
|
|
("assignFromSheet", [{"CODE": "Q00001"}], sh, "Q00001"),
|
|
("sheetToBoq", sh, "code"),
|
|
("floatToStr", 1e-6),
|
|
("floatToStr", 1.5e15),
|
|
("floatToStr", 180),
|
|
)
|
|
xc = asg["rows"][0]["XCODE"]
|
|
# 내역 전송 일곱 이름 · 인쇄 영역은 빠지고 자재단가(전송 이름 아님)는 남음 · 줄마다 CRLF
|
|
assert xc == "Qty0=25\r\n절토=12.5\r\n자재단가=100\r\n"
|
|
assert asg["rows"][0]["QTY0"] == "25"
|
|
assert [i["code"] for i in boq["items"]] == ["B00001", "M00002"]
|
|
assert nums == "0.3" and f2 == "1E-6" and f3 == "1.5E15" and f4 == "180"
|