166 lines
8.0 KiB
Python
166 lines
8.0 KiB
Python
# -*- 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,
|
||
}
|