Files
Aislo/tools/stmate-test/jdl_export.py
T

166 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""조달청 JDL TEXT 묶음 내보내기 — 프로젝트(STC/폴더) → 단가 목록 줄 · Main · GongJong 머리 · Global.Ini.
근거: ST_U_Convert.CPM Conv_ST8_Jodal 0x4b9f2c(지식DB REA 분석/37 §1)
· 계열: PRJLST PCLASS 0→1 · 3→2 · 4~7·9→1(0x4ba06e switch) · 그 밖은 앞 화면 계열 그대로(미확인 → 지정 필요)
· 코드 차례: COSTN 첫 글자 LMSWI(0x4bb622) 다음 BXD(0x4bb696) · 같은 표 안 차례는 열린 색인(미확인 → 물리 차례)
· 코드 기록(FUN_00431758): CODE · NAME · SIZE · UNIT(X 면 「시간」) · EQCODE · BQCODE ·
계열 s 의 노무 = M·S 코드면 0 아니면 LAMTs · 재료 = L·S 면 0 아니면 MAMTs · 경비 = L·M 면 0 아니면 SAMTs(0x432d7e~)
· 줄(FUN_004b0904 · 첫 글자 → 파일 L→L · M→M · S→E · W→W · B→Unit · X→JUnit · D→Danga · I→IMG.DBF):
단위 「$」→「$」 · L·M·S·W 의 「T%」→「T%*100」 · B·X·D 의 「L.S.」「P.S.」(대문자 비교) → 단위+「3」
· Main.TXT = PNUM · PNAME(탭→빈칸 · TrimRight) · 「02」 · GongJong 머리 줄 = PRIV_PR 거짓일 때만
· Global.Ini [BASIC] MAKER=STM · CDATE=yyyyMMdd · JDL VERSION=3.3 · [COST] L·M·E(=SCOST)·TCOST · [CPART] 상수 비트
"""
from __future__ import annotations
import datetime
from dbf_rw import DbfTable
from jdl_format import COUNTS, Raw, build_line, check_lines
from stc_read import Source
SERIES_BY_PCLASS = {0: 1, 3: 2, 4: 1, 5: 1, 6: 1, 7: 1, 9: 1}
FILE_OF = {"L": "L.Txt", "M": "M.Txt", "S": "E.Txt", "W": "W.Txt"}
FILE_OF |= {"B": "Unit.Txt", "X": "JUnit.Txt", "D": "Danga.Txt"}
CPART_CONST = [("PPBIT", "2C"), ("MQBIT", "4C"), ("MABIT", "0C"), ("MTBIT", "0C")]
CPART_CONST += [("UQBIT", "6C"), ("JQBIT", "6C"), ("DRBIT", "11R"), ("DTBIT", "0C")]
# 근거는 찾았으나 값 원천(설정 자리)을 못 푼 것 — 내보내지 않음
MISSING = [
"MainPart.Txt · GongJong.Txt 본문 · UnitPart.Txt · JPart.Txt(공종·구성 줄 — 0x4b6378 · 0x4b2a46~ 미해독)",
"CostAccount.Txt(잡비계산 — PRT.DBF I_TYP·T_RAT·T_AMT·S_DET·BIGO 0x4bfac4~ 미해독)",
"J.Txt(만드는 쪽 줄 없음 — 검사기 목록에만 있음)",
"DANGA.DBF · IMG.DBF(참고 표 — FORM 식 변환 0x452e78 미해독)",
"Global.Ini [BASIC] VERSION(프로그램 판 글) · [CPART] MCMETHOD·UABIT·UTBIT·UCMETHOD·JABIT·JTBIT·"
"JCMETHOD·DQBIT·DABIT·DCMETHOD · [PPART] · 환율·자리수 키(설정 자리 미해독)",
"W 코드 표준시장 덧붙임 칸(기록 +0x254≠0 때 · 0x4b2309) · 금액 칸 BAMT~ZAMT(필드 있을 때)",
"압축 <이름>_….JDL(ST_U_Archive 압축 형식 미확인)",
]
def _f(t: DbfTable, i: int, name: str) -> float:
if name not in t.fields:
return 0.0
try:
return float(t.get(i, name) or 0)
except ValueError:
return 0.0
def _s(t: DbfTable, i: int, name: str) -> str:
return t.get(i, name) if name in t.fields else ""
def _l(t: DbfTable, i: int, name: str) -> bool:
return name in t.fields and t.get(i, name).upper() in ("T", "Y")
def code_record(t: DbfTable, i: int, s: int) -> dict:
"""FUN_00431758 꼴 코드 기록 — 계열 s 의 노무·재료·경비."""
code = _s(t, i, "CODE")
k = code[:1]
rec = {"CODE": code, "NAME": _s(t, i, "NAME"), "SIZE": _s(t, i, "SIZE"), "UNIT": _s(t, i, "UNIT")}
if k == "X":
rec["UNIT"] = "시간"
rec |= {"EQCODE": _s(t, i, "EQCODE"), "BQCODE": _s(t, i, "BQCODE"), "UC": _l(t, i, "UC")}
rec["L"] = 0.0 if k in "MS" else _f(t, i, f"LAMT{s}")
rec["M"] = 0.0 if k in "LS" else _f(t, i, f"MAMT{s}")
rec["E"] = 0.0 if k in "LM" else _f(t, i, f"SAMT{s}")
rec["AMT"] = [_f(t, i, f"AMT_{n}{s}") for n in range(1, 6)]
rec["PG"] = [_s(t, i, f"PG_{n}{s}") for n in range(1, 6)]
rec["PG_J"] = _s(t, i, "PG_J")
rec["PG_S"] = _s(t, i, f"PG_{s}")
return rec
def code_line(rec: dict, gun_ok: bool, gun: str) -> tuple[str, bytes] | None:
"""FUN_004b0904 한 줄 — (파일, 줄) · I 코드·그 밖은 None."""
k = rec["CODE"][:1]
name = FILE_OF.get(k)
if not name:
return None
unit = rec["UNIT"].strip()
if unit == "$":
unit = "$"
if k in "LMSW" and unit == "T%":
unit = "T%*100"
if k in "BXD" and unit.upper() in ("L.S.", "P.S."):
unit = unit + "3"
head = [rec["CODE"], rec["NAME"], rec["SIZE"], unit]
last = Raw(gun if (gun_ok and k in "MS") else " ")
if k == "L":
return name, build_line(head + [rec["L"], rec["EQCODE"], ""], [rec["BQCODE"], rec["UC"], *rec["AMT"], last])
if k == "M":
pairs = [x for a, p in zip(rec["AMT"], rec["PG"]) for x in (a, p)]
ext = [rec["BQCODE"], rec["UC"], *rec["AMT"], *rec["PG"], rec["PG_J"], rec["PG_S"], last]
return name, build_line(head + [rec["M"], rec["EQCODE"], *pairs, "", "", ""], ext)
if k == "S":
return name, build_line(head + ["0", "0", rec["E"], rec["EQCODE"], ""], [rec["BQCODE"], rec["UC"], last])
prices = [rec["M"], rec["L"], rec["E"], rec["EQCODE"]]
if k == "W":
return name, build_line(head + prices + [""], [rec["BQCODE"], rec["UC"]])
return name, build_line(head + prices + ["", ""], [rec["BQCODE"], rec["UC"]])
def _tabless(s: str) -> str:
return s.replace("\t", " ").rstrip(" ")
def _ini(sections: list[tuple[str, list[tuple[str, str]]]]) -> bytes:
out = []
for sec, items in sections:
out.append(f"[{sec}]")
out += [f"{k}={v}" for k, v in items]
return ("\r\n".join(out) + "\r\n").encode("cp949", "replace")
def export_bundle(path: str, series: int | None = None, today: datetime.date | None = None) -> dict:
"""프로젝트 → {files: {이름: 바이트}, series, counts, checks, missing, warnings}."""
src = Source(path)
if "COSTN.DBF" not in src.files or "PRJLST.DBF" not in src.files:
raise ValueError("COSTN.DBF · PRJLST.DBF 가 없음")
t = DbfTable(src.files["COSTN.DBF"])
p = DbfTable(src.files["PRJLST.DBF"])
pclass = int(float(p.get(0, "PCLASS") or 0)) if "PCLASS" in p.fields else 0
s = series or SERIES_BY_PCLASS.get(pclass)
if not s:
raise ValueError(f"PCLASS {pclass} 의 계열 미확인 — series(1~4) 지정 필요")
gun_ok = all(f in t.fields for f in ("GUN", "BAMT", "CAMT", "DAMT", "EAMT", "FAMT", "KAMT", "ZAMT"))
files: dict[str, bytearray] = {}
warnings: list[str] = []
rows = [i for i in range(len(t)) if not t.deleted(i)]
for group in ("LMSWI", "BXD"):
for i in rows:
k = t.get(i, "CODE")[:1]
if k not in group:
continue
rec = code_record(t, i, s)
if k == "W" and "ACTTYP" in t.fields and _f(t, i, "ACTTYP"):
warnings.append(f"{rec['CODE']} 표준시장 덧붙임 칸 미확인 — 기본 꼴로 씀")
got = code_line(rec, gun_ok, _s(t, i, "GUN"))
if got:
files.setdefault(got[0], bytearray()).extend(got[1] + b"\r\n")
pnum, pname = _tabless(p.get(0, "PNUM")), _tabless(p.get(0, "PNAME"))
files["Main.Txt"] = bytearray(build_line([pnum, pname, Raw("02")]) + b"\r\n")
if not _l(p, 0, "PRIV_PR"):
head = build_line(["01", p.get(0, "PNAME"), 1, ""], ["", False, "", "", "", Raw("0")])
files["GongJong.Txt"] = bytearray(head + b"\r\n")
day = (today or datetime.date.today()).strftime("%Y%m%d")
cost = [(k, p.get(0, f"{f}{s}")) for k, f in (("LCOST", "LCOST"), ("MCOST", "MCOST"), ("ECOST", "SCOST"))]
cost.append(("TCOST", p.get(0, f"TCOST{s}")))
ini = [("BASIC", [("MAKER", "STM"), ("CDATE", day), ("JDL VERSION", "3.3")]), ("COST", cost)]
ini.append(("CPART", CPART_CONST))
out = {k: bytes(v) for k, v in files.items()}
out["Global.Ini"] = _ini(ini)
checks = {k: check_lines(k, v) for k, v in out.items() if k in COUNTS}
counts = {k: v.count(b"\r\n") for k, v in out.items() if k != "Global.Ini"}
return {
"files": out,
"series": s,
"pclass": pclass,
"counts": counts,
"checks": checks,
"missing": MISSING,
"warnings": warnings,
}