- JDL 줄 = 탭 구분 + \x05/\x07 덧붙임(검사기 0x504d20 · 고정폭 표는 죽은 고리 — 34 §17.9 바로잡음) · 단가 목록 7종·Main·GongJong 머리·Global.Ini 상수 키 · zip 받기 - 구버전 판 가리기(Chk_Data 0x459280) · 7.x/DOS 표별 필드 대응 · B→X 조건 = 단위 X - 엑셀 역등록 PRPT「속성」·PRIN「대상」·PROUT「저장」 = PR/RR 줄만 · 원본 BDQTY 4건 일치 - 자재 = 재료비 수량집계표 엑셀 내보내기(주요건설자재현황 시트) - 그림 코드 = COSTN I 코드 IFORM · SIZE = 그림 클래스 이름 - 지식DB 37번 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
345 lines
14 KiB
Python
345 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""구버전(DOS·7.x) → Win8 자료 변환 — ST_U_Convert ST_ConvToWin8(0x459928) 표 옮기기.
|
|
|
|
근거(지식DB REA 분석/37 §2): 7.x 0x459e68 · DOS 0x45a8d4 · 표마다 원본·대상 양쪽에 있는 필드만 값 복사
|
|
COSTN 7.x 0x4882a4(63쌍) · DOS 0x490cb8(35쌍 + COSTN1 M 코드 덧값) · 행 = CODE 첫 글자 MLSWBD
|
|
· 단위 첫 글자 LMST + 둘째 '%' 면 앞 5칸만 · GUN 'H…' → 'Z' · B 코드 + 단위(대문자·Trim) 'X' → 'X'+CODE[1:6]
|
|
BDQTY 0x489448 · PRJLST 7.x 0x48c890 / DOS 0x49379c · KONGJONG 0x48cbf0 · BOQ<PCODE> 7.x 0x48d58c / DOS 0x493e78 ·
|
|
BQM 0x48dc38 · Start8x.Ini(conv_win8_ini) · 결과 BDQTY 는 암호 표(DbfEncrypt)
|
|
틀(Win8 프로젝트)의 표 구조·요율·서식·설정을 씀(stc_project.new_project) · 원본 폴더는 읽기만.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from conv_win8_ini import b_list, detect, ini_from_7x, ini_from_dos
|
|
from dbf_crypt import covers
|
|
from dbf_memo import BLANK_PTR, SixMemo
|
|
from dbf_rw import DbfTable
|
|
from stc_project import new_project
|
|
from stc_save import SaveLog
|
|
from stc_save_extra import save_bdqty
|
|
|
|
_AMT = [("AMT", "TAMT1"), ("LAMT", "LAMT1"), ("MAMT", "MAMT1"), ("SAMT", "SAMT1")]
|
|
_AMT += [("D_AMT", "TAMT2"), ("D_LAMT", "LAMT2"), ("D_MAMT", "MAMT2"), ("D_SAMT", "SAMT2")]
|
|
_HEAD = [(k, k) for k in ("CODE", "NAME", "SIZE", "UNIT", "BQCODE")]
|
|
_FLAGS = [(k, k) for k in ("NAGCD", "UC", "K_S", "GUN", "JOJAK", "BOKSA")]
|
|
_BK = [(f"{c}AMT", f"{c}AMT1") for c in "BCDEFG"] + [("HAMT", "ZAMT1"), ("KAMT", "KAMT1")]
|
|
_BK += [(f"D_{c}AMT", f"{c}AMT2") for c in "BCDEFG"] + [("D_HAMT", "ZAMT2"), ("D_KAMT", "KAMT2")]
|
|
COSTN_DOS = _HEAD + _AMT + _FLAGS + _BK
|
|
_AP = [(f"AMT_{n}D", f"AMT_{n}1") for n in range(1, 6)] + [
|
|
(f"PG_{n}D", f"PG_{n}1") for n in range(1, 6)
|
|
]
|
|
_AP += [(f"AMT_{n}B", f"AMT_{n}2") for n in range(1, 6)] + [
|
|
(f"PG_{n}B", f"PG_{n}2") for n in range(1, 6)
|
|
]
|
|
_AP += [
|
|
("PG_J", "PG_J"),
|
|
("PG_D", "PG_1"),
|
|
("PG_B", "PG_2"),
|
|
("AMT_61", "AMT_1"),
|
|
("AMT_62", "AMT_2"),
|
|
]
|
|
_AP += [("AMT_6D", "AMT_61"), ("AMT_6B", "AMT_62")]
|
|
COSTN_7X = _HEAD + _AMT + _FLAGS + _AP + _BK + [("PP", "PP")]
|
|
COSTN1_M = [("AMT_A", "AMT_12"), ("AMT_B", "AMT_22"), ("AMT_C", "AMT_32"), ("AMT_D", "AMT_11")]
|
|
COSTN1_M += [
|
|
("AMT_E", "AMT_21"),
|
|
("AMT_F", "AMT_31"),
|
|
("PG_A", "PG_12"),
|
|
("PG_B", "PG_22"),
|
|
("PG_C", "PG_32"),
|
|
]
|
|
COSTN1_M += [("PG_D", "PG_11"), ("PG_E", "PG_21"), ("PG_F", "PG_31"), ("PG_J", "PG_J")]
|
|
BDQTY_MAP = [("BDCODE", "BDCODE"), ("CODE", "CODE"), ("T_NAME", "NAME1"), ("T_SIZE", "SIZE1")]
|
|
BDQTY_MAP += [("QTY", "QTY1"), ("BIMOK", "BIMOK"), ("DEVD", "DEVD1")]
|
|
_COST = ["MCOST", "LCOST", "SCOST"]
|
|
PRJ_DOS = [(k, k) for k in ("PNUM", "PNAME", "PCODE", "PCLASS")]
|
|
PRJ_DOS += [(c, f"{c}1") for c in _COST] + [("T_COST", "TCOST1"), ("COST_W", "COSTW1")]
|
|
PRJ_DOS += [(f"{c}1", f"{c}2") for c in _COST] + [("T_COST1", "TCOST2"), ("COST_WA", "COSTW2")]
|
|
PRJ_DOS += [(f"{c}2", f"{c}3") for c in _COST] + [("T_COST2", "TCOST3")]
|
|
PRJ_7X = PRJ_DOS[:4] + [("CCODE", "CCODE")] + PRJ_DOS[4:]
|
|
PRJ_7X += [(f"{c}3", f"{c}4") for c in _COST] + [("T_COST3", "TCOST4")]
|
|
PRJ_7X += [("XXXNM", "XX1NM"), ("IXXNM", "XX2NM"), ("JXXNM", "XX3NM"), ("KXXNM", "XX4NM")]
|
|
PRJ_7X += [("CHAMT", "CHAMT"), ("K_ORD", "K_ORD")]
|
|
_BOQ = [("QTY", "QTY1")] + [(a, b) for a, b in _AMT[:4]]
|
|
_BOQ += [
|
|
("T_AMOUNT", "T_AMT1"),
|
|
("L_AMOUNT", "L_AMT1"),
|
|
("M_AMOUNT", "M_AMT1"),
|
|
("S_AMOUNT", "S_AMT1"),
|
|
]
|
|
_BOQ += [("BDCODE", "BDCODE1"), ("QTY1", "QTY2")] + _AMT[4:]
|
|
_BOQ += [
|
|
("DT_AMOUNT", "T_AMT2"),
|
|
("DL_AMOUNT", "L_AMT2"),
|
|
("DM_AMOUNT", "M_AMT2"),
|
|
("DS_AMOUNT", "S_AMT2"),
|
|
]
|
|
_BOQ += [("QTY2", "QTY3"), ("T_AMOUNT2", "T_AMT3"), ("QTY3", "QTY4"), ("T_AMOUNT3", "T_AMT4")]
|
|
_BOQ += [(k, k) for k in ("SORTCODE", "ENDCODE", "BQCODE")]
|
|
BOQ_7X = [("ITNUM", "ITNUM"), ("NAME", "NAME1"), ("SIZE", "SIZE1"), ("UNIT", "UNIT1")] + _BOQ
|
|
BOQ_7X += [(k, k) for k in ("RL", "RM", "RS", "UC")]
|
|
BOQ_DOS = [("ITNUM", "ITNUM"), ("DESCRIP", "NAME1"), ("DESSIZE", "SIZE1"), ("UNIT", "UNIT1")] + _BOQ
|
|
BQM_7X = [
|
|
("ITNUM", "ITNUM"),
|
|
("NAME", "NAME1"),
|
|
("SIZE", "SIZE1"),
|
|
("UNIT", "UNIT1"),
|
|
("QTY", "QTY1"),
|
|
]
|
|
BQM_7X += [("AMT", "TAMT1"), ("T_AMOUNT", "T_AMT1"), ("BDCODE", "BDCODE1")]
|
|
BQM_7X += [(k, k) for k in ("RATE", "BIGO", "SORTCODE", "ENDCODE")]
|
|
KONG_DOS = [
|
|
"토 공",
|
|
"배 수 공",
|
|
"구 조 물 공",
|
|
"포 장 공",
|
|
"조 경 공",
|
|
]
|
|
KONG_DOS += ["부 대 공", "기 타 공 "]
|
|
UNMAPPED = [
|
|
"간접비 설정 이관(PRJ_MEMO — 7.x 0x48abb0 · DOS 0x4922e4) → 틀 설정 유지",
|
|
"RATE·RATE_RA·RATE_M 행 대응(KANJUB2 0x47a9f4 · JB_* 틀) → 틀 요율 유지",
|
|
"SYSINFO(암호 표) → 틀 유지 · IFORM 산출식(FORM.DBF · Tran_SAN_Format · DOS 0x4905d0) · IMG.DB 그림",
|
|
"원본 BDQTY 가 암호 표면 열쇠(전역 +0x12) 미확인 → 평문 BDQTY 만",
|
|
]
|
|
|
|
|
|
def _table(files: dict, name: str) -> DbfTable | None:
|
|
b = files.get(name.upper())
|
|
if not b:
|
|
return None
|
|
t = DbfTable(b)
|
|
return None if b[0] == 0x06 else t
|
|
|
|
|
|
def _rows(t: DbfTable | None) -> list[dict]:
|
|
if t is None:
|
|
return []
|
|
return [{n: t.get(i, n) for n in t.order} for i in range(len(t)) if not t.deleted(i)]
|
|
|
|
|
|
def _copy(src: dict, pairs: list[tuple[str, str]], have: set[str]) -> dict:
|
|
return {d: src[s] for s, d in pairs if s in src and d in have}
|
|
|
|
|
|
def _build(template: bytes, rows: list[dict]) -> bytes:
|
|
"""틀 필드로 새 표 — 새 레코드 = 숫자 0 · 논리·글 빈칸 · 주어진 칸만(원본·대상 둘 다 있는 칸)."""
|
|
t = DbfTable(template)
|
|
zero = bytearray(b" " * t.rlen)
|
|
for f in t.fields.values():
|
|
if f.type in ("N", "F"):
|
|
zero[f.off : f.off + f.len] = t.encode_value(f.name, 0)
|
|
t.recs, t.dirty = [], True
|
|
for i, r in enumerate(rows):
|
|
t.recs.append(bytearray(zero))
|
|
for k, v in r.items():
|
|
if k in t.fields and t.fields[k].type != "M" and v not in (None, ""):
|
|
t.set_raw(i, k, t.encode_value(k, v))
|
|
return t.to_bytes()
|
|
|
|
|
|
def _build_bdqty(template: bytes, rows: list[dict]) -> bytes:
|
|
"""암호 BDQTY — 틀 레코드를 옮겨(stc_save_extra.save_bdqty) 암호 표 안 칸은 AppendBlank 꼴(0·F·빈칸)로 채움 ·
|
|
표 밖 칸(NAME2 등)은 옮겨 온 틀 레코드 바이트(조건부)."""
|
|
t = DbfTable(template)
|
|
blank = {}
|
|
for f in t.fields.values():
|
|
if covers(f.off, f.len) and f.type != "M":
|
|
blank[f.name] = 0 if f.type in ("N", "F") else ("F" if f.type == "L" else "")
|
|
full = [blank | r for r in rows]
|
|
if not full:
|
|
t.recs, t.dirty = [], True
|
|
return t.to_bytes()
|
|
return save_bdqty(template, full, SaveLog(), strict=False) or template
|
|
|
|
|
|
def _is_t(v) -> bool:
|
|
return str(v).strip().upper() in ("T", "Y", "1", "TRUE")
|
|
|
|
|
|
def conv_costn(
|
|
src: list[dict], pairs, have: set[str], m_extra: dict[str, dict]
|
|
) -> tuple[list[dict], dict]:
|
|
"""COSTN 행 옮기기 — (행, 옛 B → 새 X)."""
|
|
out, b2x = [], {}
|
|
for r in src:
|
|
code = r.get("CODE", "").strip()
|
|
if not code or code[0] not in "MLSWBD":
|
|
continue
|
|
unit = r.get("UNIT", "")
|
|
if unit[:1] in "LMST" and unit[1:2] == "%":
|
|
row = _copy(r, _HEAD, have)
|
|
else:
|
|
row = _copy(r, pairs, have)
|
|
if str(r.get("GUN", "")).startswith("H") and "GUN" in have:
|
|
row["GUN"] = "Z"
|
|
if code[0] == "M" and code in m_extra:
|
|
row |= _copy(m_extra[code], COSTN1_M, have)
|
|
if code[0] == "B" and unit.strip().upper() == "X":
|
|
b2x[code] = "X" + code[1:6]
|
|
row["CODE"] = b2x[code]
|
|
out.append(row)
|
|
return out, b2x
|
|
|
|
|
|
def conv_bdqty(src: list[dict], costn_codes: set[str], b2x: dict, have: set[str]) -> list[dict]:
|
|
out = []
|
|
for r in src:
|
|
bd, c = r.get("BDCODE", "").strip(), r.get("CODE", "").strip()
|
|
if bd[:1] not in ("B", "D", "X") or bd not in costn_codes:
|
|
continue
|
|
if not (c in costn_codes or (c[:1] in "TEC" and c[1:6].strip().isdigit())):
|
|
continue
|
|
row = _copy(r, BDQTY_MAP, have)
|
|
row["BDCODE"], row["CODE"] = b2x.get(bd, bd), b2x.get(c, c)
|
|
if row["CODE"][:1] in "BS":
|
|
row |= {k: v for k, v in (("X_QTY1", r.get("QTY", 0)), ("X_HAL1", 100)) if k in have}
|
|
row |= {k: 100 for k in ("X_HAL2", "X_HAL3") if k in have}
|
|
if _is_t(r.get("T_EXIST", "")) and "EXIST1" in have:
|
|
row["EXIST1"] = 1
|
|
out.append(row)
|
|
return out
|
|
|
|
|
|
def conv_prjlst(src: list[dict], pairs, have: set[str], dos: bool) -> list[dict]:
|
|
out = []
|
|
for r in src:
|
|
row = _copy(r, pairs, have)
|
|
pc = int(float(r.get("PCLASS") or 0))
|
|
if not dos and "K_ORD" in have:
|
|
sc = int(float(r.get("SCLASS") or 0))
|
|
if pc not in (6, 7) and not (pc == 4 and sc == 1):
|
|
row["K_ORD"] = 0
|
|
out.append(row)
|
|
return out
|
|
|
|
|
|
def conv_boq(src: list[dict], pairs, have: set[str], b2x: dict, dos: bool) -> list[dict]:
|
|
out = []
|
|
for r in src:
|
|
row = _copy(r, pairs, have)
|
|
bd = r.get("BDCODE", "").strip()
|
|
if bd in b2x and "BDCODE1" in have:
|
|
row["BDCODE1"] = b2x[bd]
|
|
if _is_t(r.get("EXIST", "")) and "EXIST1" in have:
|
|
row["EXIST1"] = 1
|
|
if not dos:
|
|
row |= {k: r[k] for k in ("DES1", "DES2") if k in r and k in have}
|
|
if "UNIT1" in r and "UNIT2" in have:
|
|
row["UNIT2"] = r["UNIT1"]
|
|
out.append(row)
|
|
return out
|
|
|
|
|
|
def _fields(b: bytes) -> set[str]:
|
|
return set(DbfTable(b).fields)
|
|
|
|
|
|
def convert(old: dict, template: dict, pname: str = "") -> tuple[dict, dict]:
|
|
"""구버전 폴더 파일(대문자 이름 → 바이트) + Win8 틀 → (새 files, 보고)."""
|
|
old = {k.upper(): v for k, v in old.items()}
|
|
ver = detect(old)
|
|
if 130 <= ver <= 149:
|
|
raise ValueError("이미 STmate 2Y.m 자료로 변환된 데이타입니다")
|
|
if not (110 <= ver <= 129):
|
|
raise ValueError(f"구버전 STmate 자료가 아닙니다(판 {ver})")
|
|
dos = ver < 120
|
|
prj_src = _rows(_table(old, "PRJLST.DBF"))
|
|
name = pname or (prj_src[0].get("PNAME", "") if prj_src else "") or "변환 공사"
|
|
out = new_project(template, name)
|
|
notes: list[str] = []
|
|
conld = b_list(_rows(_table(old, "CONLD.DBF")))
|
|
contd = b_list(_rows(_table(old, "CONTD.DBF")))
|
|
m_extra = (
|
|
{r.get("CODE", "").strip(): r for r in _rows(_table(old, "COSTN1.DBF"))} if dos else {}
|
|
)
|
|
have = _fields(template["COSTN.DBF"])
|
|
costn_src = _rows(_table(old, "COSTN.DBF"))
|
|
costn, b2x = conv_costn(costn_src, COSTN_DOS if dos else COSTN_7X, have, m_extra)
|
|
out["COSTN.DBF"] = _build(template["COSTN.DBF"], costn)
|
|
codes = {r.get("CODE", "").strip() for r in costn_src}
|
|
bd_old = _table(old, "BDQTY.DBF")
|
|
if old.get("BDQTY.DBF") and bd_old is None:
|
|
notes.append("원본 BDQTY 암호 표 — 열쇠 미확인으로 구성 행 비움")
|
|
if "BDQTY.DBF" in template:
|
|
rows = conv_bdqty(_rows(bd_old), codes, b2x, set(DbfTable(template["BDQTY.DBF"]).fields))
|
|
rows += [
|
|
{"BDCODE": r.get("DCODE", "").strip(), "CODE": r.get("ICODE", "").strip()}
|
|
for r in _rows(_table(old, "IMGINFO.DBF"))
|
|
if r.get("DCODE", "").startswith("D")
|
|
]
|
|
out["BDQTY.DBF"] = _build_bdqty(template["BDQTY.DBF"], rows)
|
|
prj = conv_prjlst(prj_src, PRJ_DOS if dos else PRJ_7X, _fields(template["PRJLST.DBF"]), dos)
|
|
pt = DbfTable(out["PRJLST.DBF"])
|
|
has_memo = "PRJ_MEMO" in pt.fields
|
|
text = SixMemo(out.get("PRJLST.SMT")).read(pt.raw(0, "PRJ_MEMO")) if has_memo else b""
|
|
if prj:
|
|
pt = DbfTable(_build(out["PRJLST.DBF"], prj))
|
|
if has_memo: # 틀 설정을 공사마다 따로(간접비 이관 미확인)
|
|
memo = SixMemo(None)
|
|
for i in range(len(pt)):
|
|
pt.set_raw(i, "PRJ_MEMO", memo.write(BLANK_PTR, text))
|
|
out["PRJLST.SMT"] = memo.to_bytes()
|
|
out["PRJLST.DBF"] = pt.to_bytes()
|
|
if "KONGJONG.DBF" in template:
|
|
if dos:
|
|
kong = [{"CODE": f" {i}", "NAME": n} for i, n in enumerate(KONG_DOS, 1)]
|
|
else:
|
|
src_k = _rows(_table(old, "KONGJONG.DBF"))
|
|
kong = [
|
|
{"CODE": str(i), "NAME": r.get("DESCRIP", ""), "SIZE": r.get("DESSIZE", "")}
|
|
for i, r in enumerate(src_k, 1)
|
|
]
|
|
out["KONGJONG.DBF"] = _build(template["KONGJONG.DBF"], kong)
|
|
boq_tpl = template.get("BOQ11.DBF")
|
|
for k in [k for k in out if re.match(r"^BOQ\d+\.DBF$", k)]:
|
|
del out[k]
|
|
boq_names = []
|
|
for r in prj_src if boq_tpl else []:
|
|
pc = str(r.get("PCODE", "")).strip()
|
|
src_b = _table(old, f"BOQ{pc}.DBF")
|
|
if src_b is None:
|
|
notes.append(f"BOQ{pc}.DBF 없음 — 건너뜀")
|
|
continue
|
|
rows = conv_boq(_rows(src_b), BOQ_DOS if dos else BOQ_7X, _fields(boq_tpl), b2x, dos)
|
|
out[f"BOQ{pc}.DBF"] = _build(boq_tpl, rows)
|
|
boq_names.append(f"BOQ{pc}.DBF")
|
|
if not dos and _table(old, f"BQM{pc}.DBF") is not None:
|
|
notes.append(f"BQM{pc}.DBF 있음 — 틀에 BQM 구조가 없어 옮기지 않음")
|
|
if b2x:
|
|
conld = (
|
|
"".join(b2x.get(conld[i : i + 6], conld[i : i + 6]) for i in range(0, len(conld), 6))
|
|
if not dos
|
|
else conld
|
|
)
|
|
contd = (
|
|
"".join(b2x.get(contd[i : i + 6], contd[i : i + 6]) for i in range(0, len(contd), 6))
|
|
if not dos
|
|
else contd
|
|
)
|
|
if dos:
|
|
ini, n2 = ini_from_dos(old.get("START.MEM", b""), conld, contd)
|
|
notes += n2
|
|
else:
|
|
ini = ini_from_7x(old.get("START.INI", b""), conld, contd)
|
|
out["STINI.MEM"] = ini
|
|
if "MEMO.INI" in old:
|
|
out["MEMO.INI"] = old["MEMO.INI"]
|
|
out["START8X.INI"] = ini
|
|
report = {
|
|
"version": ver,
|
|
"kind": "DOS" if dos else "7.x",
|
|
"pname": name,
|
|
"costn_count": len(costn),
|
|
"bdqty_count": len(DbfTable(out["BDQTY.DBF"])) if "BDQTY.DBF" in out else 0,
|
|
"prj_count": len(prj),
|
|
"boq_tables": boq_names,
|
|
"b_to_x": b2x,
|
|
"notes": notes,
|
|
"unmapped": UNMAPPED,
|
|
}
|
|
return out, report
|