Merge remote-tracking branch 'origin/dev' into 안티그래비티
This commit is contained in:
@@ -75,7 +75,8 @@ def pack(got: dict, name: str) -> dict[str, Any]:
|
||||
}
|
||||
for b in got["산근"]
|
||||
]
|
||||
out["일위"] = [{k: b.get(k) for k in head if k in b} for b in got["일위"]]
|
||||
keep = dict.fromkeys((*head, "줄", "줄합", "표지")) # 828-1 일위 구성 줄(`M02_San_Ilwi` 꼴)
|
||||
out["일위"] = [{k: b.get(k) for k in keep if k in b} for b in got["일위"]]
|
||||
res = got.get("자원") or {}
|
||||
out["단가"] = {
|
||||
code: {**p, **{k: (res.get(code) or {}).get(k) for k in ("이름", "규격", "단위")}}
|
||||
@@ -185,8 +186,10 @@ _LIST_COST = ("M", "L", "S", "T")
|
||||
|
||||
|
||||
def hopyo(saved: dict) -> dict[str, Any]:
|
||||
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리)만 · 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
|
||||
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
|
||||
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리) · 호표 = STmate 구성표 일위 줄(828-1 · 엑셀 일위대가표
|
||||
길과 같은 꼴) · EST 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
|
||||
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": _ilwi(saved)}
|
||||
out["검산"] = {"어긋남": []}
|
||||
out["단산"] = [
|
||||
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
|
||||
for n, b in numbers(saved).items()
|
||||
@@ -194,6 +197,23 @@ def hopyo(saved: dict) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def _ilwi(saved: dict) -> list[dict]:
|
||||
"""STmate 일위 구성 줄 → 호표(행 = 격자 「일위대가표」 행 · 줄 재현 · 덮음 = `M02_Hopyo_UnitLine`)."""
|
||||
if saved.get("꼴") != "STmate" or not saved.get("일위"):
|
||||
return []
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as ul
|
||||
from M02_MasterTemplete import M02_San_Ilwi as il
|
||||
|
||||
out, at = il.hopyo(saved["일위"]), 2
|
||||
for h in out:
|
||||
h["행"] = [at, at + len(h["줄"])]
|
||||
for i, line in enumerate(h["줄"], 1):
|
||||
line["행"] = at + i
|
||||
at += len(h["줄"]) + 2
|
||||
ul.check_hopyo(h)
|
||||
return out
|
||||
|
||||
|
||||
def grid(saved: dict) -> dict[str, Any]:
|
||||
"""원파일 → 단가산출표 한 시트 격자(`M02_Hopyo_Grid.read` 꼴) — 산출근거 머리(코드 · 이름 · 규격 ·
|
||||
단위 · 저장 금액) 한 줄씩 · 행 = 1 머리 + 블록 차례."""
|
||||
@@ -213,7 +233,7 @@ def grid(saved: dict) -> dict[str, Any]:
|
||||
sheet = {"id": "s1", "이름": LIST, "칸": cells, "틀고정": {"행": 1}}
|
||||
sheet["열"] = {c: {"폭": w} for c, w in zip(cols, (10, 40, 18, 6, 12, 12, 12, 12))}
|
||||
book = {"종류": "통합문서", "판": 1, "열": "", "서식": [{}], "시트": [sheet], "활성": "s1"}
|
||||
return {
|
||||
out = {
|
||||
"책": book,
|
||||
"호표": [],
|
||||
"단산": rows,
|
||||
@@ -222,6 +242,18 @@ def grid(saved: dict) -> dict[str, Any]:
|
||||
"목록": {"호표": [], "단산": [{"번": r["번"], "행": r["행"][0]} for r in rows]},
|
||||
"줄": [],
|
||||
}
|
||||
if hp := _ilwi(saved): # 828-1 STmate 구성표 일위대가 = 「일위대가표」 시트(앞)
|
||||
from M02_MasterTemplete import M02_San_Ilwi as il
|
||||
|
||||
book["시트"].insert(0, il.sheet(hp, "s2"))
|
||||
book["활성"] = "s2"
|
||||
out["호표"] = [{"번": h["번"], "이름": h["이름"], "행": h["행"]} for h in hp]
|
||||
out["보기"][il.SHEET] = "s2"
|
||||
out["원본"][il.SHEET] = il.SHEET
|
||||
out["줄"] = [
|
||||
{"번": h["번"], "차례": ln["차례"], "행": ln["행"]} for h in hp for ln in h["줄"]
|
||||
]
|
||||
return out
|
||||
|
||||
|
||||
def _dec(v: Any) -> int | float | None:
|
||||
|
||||
@@ -41,6 +41,8 @@ interface MatKind {
|
||||
단서: string[];
|
||||
별도계상: boolean;
|
||||
블록: number[];
|
||||
/** 일위대가 구성 줄이 있는 호표(828-1 · 코드 숫자) */
|
||||
호표?: number[];
|
||||
줄: number;
|
||||
고름?: MatCand;
|
||||
}
|
||||
@@ -133,11 +135,12 @@ export function createMatPick(opt: { scope: HopyoScope; key: string; target: Hop
|
||||
|
||||
function kindRow(k: MatKind): HTMLElement {
|
||||
const what = `${label(k)}${k.단위 ? ` (${k.단위})` : ""}` || "—";
|
||||
const sub = [k.방법, `${k.블록.length} 블록 · ${k.줄} 줄`, ...k.단서]
|
||||
const hp = k.호표?.length ? ` · 일위대가 ${k.호표.length}` : "";
|
||||
const sub = [k.방법, `${k.블록.length} 블록${hp} · ${k.줄} 줄`, ...k.단서]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
const badge = createBadge(k.별도계상 ? `별도계상 · ${k.판정}` : k.판정, TONE[k.판정] ?? "warning", { size: 18 }); // prettier-ignore
|
||||
const row = createListRow({ label: what, sub, trailing: badge, size: 36, title: `단산 ${k.블록.join(" · ")}` }); // prettier-ignore
|
||||
const row = createListRow({ label: what, sub, trailing: badge, size: 36, title: `단산 ${k.블록.join(" · ")}${hp && ` · 호표 ${k.호표?.join(" · ")}`}` }); // prettier-ignore
|
||||
const box = el("div", { attrs: { "data-mat-kind": k.종 }, children: [row, controls(k)] });
|
||||
if (edits.has(k.종)) box.dataset.matEdited = "";
|
||||
return box;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""경쟁사 STmate 원파일 구성표(BDQTY.DBF) 읽기(PLAN 828-1 · 요구 813 · 814 · 읽기만 · 서버 단독).
|
||||
|
||||
짝 = `tools/stmate-test/bdqty.py`(`decode_bdqty` · 변환 표 그대로 · 시험 `test_828_1_stc_composition.py` 가
|
||||
실무 STC 일곱에서 두 답 1:1 대조) · 새로 암호를 풀지 않음 · 쓰기(`dbf_crypt.py`)는 옮기지 않음.
|
||||
읽는 칸 = 믿을 칸만(`TRUSTED` — 부모 · 자식 코드 · 1 차 수량 · 나누기 · 존재 · 율 밑줄 표시 · 율 밑수 ·
|
||||
대상 비목 · 관급 · BL_AMT). 못 믿는 칸(QTY2~4 · BIMOK · EQCODE · X_CODE · NAME2 · SIZE2 · 글자 칸)은 안 읽음.
|
||||
스스로 검사(`check`) — ① 자식 코드 = 자원 표에 있음 · 제어 코드(PR · RR · T · C · 빈) ② 부모 머리 ∈ B · X · D
|
||||
③ DEVD1 ∈ {F · 빈} — 하나라도 어긋나면 구성표를 안 씀(까닭 글 · `M02_San_Stc.read` 가 옛 표지 길로 물러남).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import struct
|
||||
|
||||
_TABLE = (
|
||||
"1A3.53 2A0.238 3A7.233 4A4.228 5A7.17 6A3.133 7A7.33 8A5.43 9A6.170 10A5.123 11A4.156 "
|
||||
"12A1.167 133X4.229 134X0.87 135A7.198 136A5.155 137A2.254 138A7.217 139A5.123 140A7.193 "
|
||||
"141B1.167 142A4.132 143A3.133 144B0.8 145B3.150 146B1.155 147A1.63 148B7.226 149A6.130 "
|
||||
"150A3.253 151B2.85 152A5.235 153B6.91 154X0.231 155X0.231 156X4.0 157X1.223 158X6.130 "
|
||||
"159A5.27 160B3.245 161B1.107 162X5.200 163B3.230 164B5.74 165X6.94 166X1.151 167B0.207 "
|
||||
"168B5.10 169B7.206 170A0.166 171A7.198 172X6.98 173X1.63 174X0.103 175X0.233 176X1.223 "
|
||||
"177X3.107 178A3.173 179B0.120 180X5.145 181B5.24 182X2.225 183A7.119 184X0.144 185A2.239 "
|
||||
"186X4.27 187X4.180 188X4.229 189A5.241 190A2.41 191A5.232 192A6.154 193X3.143 194A7.70 "
|
||||
"195X2.62 196X3.6 197X0.215 198A0.232 199B1.207 200X5.136 201A5.179 202B7.78 203X2.33 "
|
||||
"204A6.154 205B3.214 206B5.74 207A1.176 208B6.174 209B6.110 210X5.249 211A6.154 212B0.194 "
|
||||
"213B7.99 214A2.174 215A5.107 216B7.206 217X7.228 218B1.239 219B3.181 220X7.173 221A2.209 "
|
||||
"222A1.155 223A1.155 224A1.191 225B2.62 226X4.128 227A6.209 228B6.64 229X0.105 230X6.167 "
|
||||
"231X0.215 232A3.128 233B6.107 234A2.209 235A6.90 236B5.154 237A5.107 238A1.185 239B6.20 "
|
||||
"240B2.85 241X1.22 242X1.166 243B7.66 244B7.114 245X7.22 246X3.167 247A2.70 248B3.167 "
|
||||
"249B6.168 250A1.39 251X2.0 252A5.147 253A3.85 254A6.106 255A3.93 256X0.208 257A7.65 "
|
||||
"258A5.203 259A7.41 260A3.117 261A6.34 262A3.181 263B5.101 264A7.177 265A7.1 266A1.55 "
|
||||
"267B1.151 268B7.133 269A0.175 270X6.98 271B1.151 272B1.139 273A5.203 274A7.113 275A2.238 "
|
||||
"276X7.193 277A6.74 278A5.83 279A7.97 280B5.154 281B3.174 282A2.70 283A7.233 284B6.104 "
|
||||
"285B5.24 286X1.63 287B2.94 288B5.92 289B4.201 290A3.197 291B6.168 292B1.203 293B5.26 "
|
||||
"294A5.123 295A3.253 296X6.178 297X2.241 298X5.136 299A3.197 300A4.12 301B5.89 302B6.15 "
|
||||
"303A3.183 304X6.167 305B0.112 306B5.209 307A4.165 308X0.215 309B7.142 310B1.49 311A3.253 "
|
||||
"312A2.74 313X7.230 314X2.161 315A1.231 316B3.165 317A0.230 318B7.14 319A4.115 320A3.128 "
|
||||
"321B5.74 322A2.41 323A1.159 324A4.100 325A7.17 326B3.4 327X6.159 328A0.175 329B1.239 "
|
||||
"330A0.38 331B1.239 332X1.93 333X2.58 334X5.63 335X3.96 336X0.247 337X6.183 338X1.102 "
|
||||
"339A0.150 340X0.96 341X2.55 342A5.125 343A2.209 344B4.137 345A3.119 346B6.247 347X5.54 "
|
||||
"348A6.26 349X2.225 350X5.127 351A5.177 352B2.103 353A6.99 354B2.85 355B6.183 356X6.167 "
|
||||
"357X3.136 358A6.238 359A6.154 360X1.38 361A4.124 362X0.41 374A1.239 375A4.92 376X6.55 "
|
||||
"377B2.62 378X7.49 379B7.226 380B2.129 381A7.145 382X1.214 383A6.94 384B4.222 385B5.152 "
|
||||
"386A7.17 387B2.125 388A3.64 389A6.222 390A7.198 391A1.119 392B1.99 393A3.213 394A5.134 "
|
||||
"395B5.81 396A7.198 397B4.95 398A5.107 399B7.178 400X2.55 401X5.74 402B3.174 403A0.134 "
|
||||
"404A3.53 405A1.191 406X4.229 407X1.63 408X5.182 409B7.178 410B6.42 411B6.137 412A2.218 "
|
||||
"413A5.235 414B7.162 "
|
||||
)
|
||||
#: 믿을 칸(분석 813 1-1 · 엑셀 메타 대조 100 %)
|
||||
TRUSTED = ("BDCODE", "CODE", "QTY1", "DEVD1", "EXIST1", "PRPT", "PRIN", "PROUT", "KWNGP", "BL_AMT")
|
||||
_CONTROL = re.compile(r"^(?:PR|RR|[TC]\d)?$")
|
||||
|
||||
|
||||
def _params() -> dict[int, tuple[str, int, int]]:
|
||||
out = {}
|
||||
for tok in _TABLE.split():
|
||||
i = next(j for j, ch in enumerate(tok) if ch in "ABX")
|
||||
r, k = tok[i + 1 :].split(".")
|
||||
out[int(tok[:i])] = (tok[i], int(r), int(k))
|
||||
return out
|
||||
|
||||
|
||||
PARAMS = _params()
|
||||
|
||||
|
||||
def _ror(v: int, r: int) -> int:
|
||||
return ((v >> r) | (v << (8 - r))) & 0xFF if r else v
|
||||
|
||||
|
||||
def _dec(c: int, p: tuple[str, int, int]) -> int:
|
||||
kind, r, k = p
|
||||
if kind == "A":
|
||||
return _ror((c - k) & 0xFF, r)
|
||||
if kind == "B":
|
||||
return (_ror(c, r) - k) & 0xFF
|
||||
return _ror(c ^ k, r)
|
||||
|
||||
|
||||
def decode(b: bytes) -> list[dict[str, str]]:
|
||||
"""BDQTY.DBF → 행(레코드 차례) · 믿을 칸 가운데 표 안 칸만 · 글 = CP949 strip."""
|
||||
if len(b) < 32:
|
||||
return []
|
||||
n, hlen, rlen = struct.unpack_from("<IHH", b, 4)
|
||||
fields, o, off = [], 32, 1
|
||||
while o + 17 <= min(hlen, len(b)) and b[o] != 0x0D:
|
||||
name = b[o : o + 11].split(b"\x00")[0].decode("ascii", "ignore")
|
||||
if name in TRUSTED:
|
||||
fields.append((name, off, b[o + 16]))
|
||||
off += b[o + 16]
|
||||
o += 32
|
||||
rows = []
|
||||
for i in range(n):
|
||||
rec = b[hlen + i * rlen : hlen + (i + 1) * rlen]
|
||||
if len(rec) < rlen:
|
||||
break
|
||||
row = {}
|
||||
for name, foff, ln in fields:
|
||||
span = range(foff, foff + ln)
|
||||
if all(j in PARAMS for j in span):
|
||||
raw = bytes(_dec(rec[j], PARAMS[j]) for j in span)
|
||||
row[name] = raw.decode("cp949", "replace").strip()
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def check(rows: list[dict[str, str]], codes: set[str]) -> str | None:
|
||||
"""스스로 검사 ① ~ ③ — 어긋남 까닭 글(첫 것) · 통과 = None."""
|
||||
if not rows:
|
||||
return "구성 행 없음"
|
||||
for r in rows:
|
||||
parent, child = r.get("BDCODE") or "", r.get("CODE") or ""
|
||||
if parent[:1] not in ("B", "X", "D"):
|
||||
return f"부모 코드 머리 다름 「{parent[:8]}」"
|
||||
if child not in codes and not _CONTROL.match(child):
|
||||
return f"자식 코드가 자원 표에 없음 「{child[:8]}」"
|
||||
if (r.get("DEVD1") or "") not in ("F", ""):
|
||||
return f"나누기 칸 값 다름 「{(r.get('DEVD1') or '')[:4]}」"
|
||||
return None
|
||||
@@ -415,7 +415,9 @@ class _Stm:
|
||||
if not one_ and len(sole) == 1: # 비목 꼬리 없어도 값 든 비목 하나면 단가 줄(셈 같음)
|
||||
one_, to = sole[0], None
|
||||
res |= {"비목": one_, "옮김": to} if one_ else {}
|
||||
parts = machine_parts(res) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
parts = (
|
||||
machine_parts(res, pr.get("같은구성")) if self.keys else []
|
||||
) # 812-9 · 816-2 같은 파일 구성(828-1)
|
||||
if parts:
|
||||
res["찾기"], link, many = parts[0], parts[0]["키"], None
|
||||
res |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
"""STmate 구성표 → 일위대가 구성 줄 · 다시 셈 · 중기 구성(PLAN 828-1 · 요구 814 ㉯ ㉰ · 서버 단독).
|
||||
|
||||
셈 짝 = `tools/stmate-test/smt_eval.js` `evaluateComposition`(+ 시험 `resources/tester/helper_stmate_ilwi.cjs`
|
||||
설정) — 닫기(C*) 뒤 PR 줄 밑수 · 대상 「전체」(4 · 4) = 앞 금액 전체 × % 만 더함(분석 813 1-3 B00038).
|
||||
일위 구성 줄 꼴(원파일 · 엑셀 같은 꼴 — 읽기기 답 `일위` 블록 `줄`):
|
||||
{차례, 코드(자원 · PR · RR · C1 · T1 · 빈), 갈래(M L S X B D W · 율 · 닫기 · 구분 · 글), 이름, 규격, 단위,
|
||||
수량(1 차), 단가 {T L M S}?(셈에 쓴 자원 단가 · 중기 조종원 = 시간 환산), 금액 {T L M S}?(줄 금액),
|
||||
율? {밑, 대상(M L S T), 곱(RR)}, 밑줄?(율 밑수에 듦), 관급?, 별도계상?(자재 이름 · 규격 「별도계상」 · 「별산」)}
|
||||
블록 = {코드, 이름, 규격, 단위, 저장, 줄, 줄합 {T L M S}(다시 셈 · 끝전 뒤), 표지}
|
||||
율 줄 이름 = 지은 글(「노무비의 3%」 · 원문 이름 칸은 못 읽음).
|
||||
HP 호표(`hopyo`) = 엑셀 일위대가표 읽기(`M02_Hopyo_Stmate.read`)와 같은 꼴 — 글 · 구분 · 닫기 줄 뺌.
|
||||
중기 구성(`makes`) = 엑셀 「중기사용료」(`M02_San_MyMachine._stm_sheet`)와 같은 꼴.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
COMP = {"1": "M", "2": "L", "3": "S", "4": "T"}
|
||||
WORD = {"M": "재료비", "L": "노무비", "S": "경비", "T": "전체"}
|
||||
KIND = {"M", "L", "S", "X", "B", "D", "W"}
|
||||
MISMATCH = "구성 다시 셈 ≠ 저장 단가"
|
||||
_SBMOK = {"1": "M", "2": "L", "3": "S", "M": "M", "L": "L", "S": "S"}
|
||||
_HP = {"T": "계", "L": "노무비", "M": "재료비", "S": "경비"}
|
||||
|
||||
|
||||
def _f(v: Any) -> float:
|
||||
try:
|
||||
return float(v or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _tr(v: float, d: int = 0) -> float:
|
||||
f = 10**d
|
||||
s = v * f
|
||||
return math.trunc(s + (1e-9 if s >= 0 else -1e-9)) / f
|
||||
|
||||
|
||||
def _half(v: float, d: int = 0) -> float:
|
||||
f = 10**d
|
||||
r = math.floor(abs(v) * f + 0.5 + 1e-9) / f
|
||||
return -r if v < 0 else r
|
||||
|
||||
|
||||
def _wage(daily: float, expr: str, digits: int = 0) -> float:
|
||||
acc = daily
|
||||
for op, n in re.findall(r"([*/])?(\d+(?:\.\d+)?)", re.sub(r"\s+", "", expr or "")):
|
||||
acc = acc / float(n) if op == "/" else acc * float(n)
|
||||
return _tr(acc, digits)
|
||||
|
||||
|
||||
def options(parent: str, ini: dict, sbmok: str | None) -> dict[str, Any]:
|
||||
"""`helper_stmate_ilwi.cjs` 설정 — 총액우선(일위 · CHBNM 0) · 차례 OPTNM · 중기 조종원 시간 환산(CONLD · RXXNM)."""
|
||||
return {
|
||||
"총액우선": parent[:1] == "B" and str(ini.get("CHBNM", "1")) == "0",
|
||||
"차례": str(ini.get("OPTNM") or "SML").upper(),
|
||||
"비목": _SBMOK.get(str(sbmok or "").strip()),
|
||||
"환산": set(re.findall(r".{1,6}", str(ini.get("CONLD") or "")))
|
||||
if parent[:1] == "X"
|
||||
else set(),
|
||||
"환산식": str(ini.get("RXXNM") or ""),
|
||||
"달러": _f(ini.get("EXLNM1")),
|
||||
}
|
||||
|
||||
|
||||
def _finish(raw: dict[str, float], opt: dict) -> dict[str, float]:
|
||||
out = {k: _tr(raw[k]) for k in "MLS"}
|
||||
if opt["총액우선"]:
|
||||
order = list(opt["차례"])
|
||||
frac = {k: _half(raw[k] - out[k], 6) for k in order}
|
||||
rest = _tr(raw["T"]) - sum(out.values())
|
||||
while rest > 0:
|
||||
pick = None
|
||||
for k in order:
|
||||
if frac[k] < 1 and (pick is None or frac[k] > frac[pick]):
|
||||
pick = k
|
||||
if pick is None:
|
||||
break
|
||||
out[pick] += 1
|
||||
frac[pick] += 1
|
||||
rest -= 1
|
||||
if to := opt["비목"]:
|
||||
out = {**dict.fromkeys("MLS", 0.0), to: sum(out.values())}
|
||||
return {**out, "T": out["M"] + out["L"] + out["S"]}
|
||||
|
||||
|
||||
def _rate_line(row: dict, q: float) -> dict[str, Any]:
|
||||
base, to = COMP.get(row.get("PRIN") or ""), COMP.get(row.get("PROUT") or "")
|
||||
out: dict[str, Any] = {"이름": "", "규격": "", "단위": "%"}
|
||||
if base and to:
|
||||
mul = row.get("CODE") == "RR"
|
||||
out["이름"] = f"{WORD[base]}의 {q:g}{'배' if mul else '%'}"
|
||||
out["규격"] = f"{WORD[base]}의 {'배' if mul else '%'}"
|
||||
out["단위"] = "" if mul else "%"
|
||||
out["율"] = {"밑": base, "대상": to, "곱": mul}
|
||||
return out
|
||||
|
||||
|
||||
def lines(rows: list[dict], res: dict[str, dict], opt: dict) -> tuple[list[dict], dict[str, float]]:
|
||||
"""구성 행(부모 하나) → (구성 줄 꼴, 다시 셈 {T L M S})."""
|
||||
s = {"M": 0.0, "L": 0.0, "S": 0.0, "T": 0.0}
|
||||
base = {"M": 0.0, "L": 0.0, "S": 0.0}
|
||||
closed, out = False, []
|
||||
for i, row in enumerate(rows):
|
||||
code, q = row.get("CODE") or "", _f(row.get("QTY1"))
|
||||
head = res.get(code) or {}
|
||||
if code in ("PR", "RR"):
|
||||
kind = "율"
|
||||
elif re.fullmatch(r"C\d", code):
|
||||
kind, closed = "닫기", True
|
||||
elif re.fullmatch(r"T\d", code):
|
||||
kind = "구분"
|
||||
else:
|
||||
kind = code[:1] if code[:1] in KIND and head else "글"
|
||||
one: dict[str, Any] = {"차례": i, "코드": code, "갈래": kind, "수량": q}
|
||||
one |= {k: str(head.get(k) or "") for k in ("이름", "규격", "단위")}
|
||||
if kind == "율":
|
||||
one |= _rate_line(row, q)
|
||||
if str(row.get("PRPT") or "0") not in ("0", ""):
|
||||
one["밑줄"] = True
|
||||
if row.get("KWNGP") == "1":
|
||||
one["관급"] = True
|
||||
if kind == "M" and re.search(
|
||||
r"별도계상|별산", one["이름"] + one["규격"]
|
||||
): # 엑셀 비고 「별산…」 짝
|
||||
one["별도계상"] = True
|
||||
out.append(one)
|
||||
before = dict(s)
|
||||
if kind == "율":
|
||||
rate = one.get("율")
|
||||
if not rate:
|
||||
continue
|
||||
if closed and rate["밑"] == rate["대상"] == "T": # 닫기 뒤 전체 % (B00038)
|
||||
for k in "MLST":
|
||||
s[k] = _tr(s[k] * q / 100, 1)
|
||||
else:
|
||||
bv = sum(base.values()) if rate["밑"] == "T" else base[rate["밑"]]
|
||||
v = _tr(bv * q, 1) if rate["곱"] else _tr(bv * q / 100, 1)
|
||||
s["M" if rate["대상"] == "T" else rate["대상"]] += v
|
||||
s["T"] += v
|
||||
one["단가"] = {"T": bv}
|
||||
one["금액"] = {k: s[k] - before[k] for k in "TLMS"}
|
||||
continue
|
||||
if kind not in KIND:
|
||||
continue
|
||||
saved = (
|
||||
head.get("원단가") or head.get("저장") or {}
|
||||
) # 낙찰률 줄 = 2 칸(smt_engine costSlot)
|
||||
unit = one["단위"].strip()
|
||||
own = "L" if code[0] == "L" else "S" if code[0] == "S" else "M"
|
||||
pct = re.fullmatch(r"([MLST])%", unit)
|
||||
if pct and closed:
|
||||
for k in "MLST":
|
||||
s[k] = _tr(s[k] * q / 100, 1)
|
||||
elif pct:
|
||||
bv = s["M"] + s["L"] + s["S"] if pct.group(1) == "T" else s[pct.group(1)]
|
||||
v = _tr(bv * q / 100, 1)
|
||||
s[own] += v
|
||||
s["T"] += v
|
||||
one["단가"] = {"T": bv}
|
||||
else:
|
||||
p = {k: _f(saved.get(k)) for k in "MLS"}
|
||||
if code in opt["환산"]:
|
||||
p = {"M": 0.0, "L": _wage(p["L"], opt["환산식"]), "S": 0.0}
|
||||
if unit == "$": # 달러 → 천원(엑셀 「중기사용료」 값 · 원 미만 버림)
|
||||
p = {k: _tr(v * opt["달러"] / 1000) for k, v in p.items()}
|
||||
elif unit in ("€", "¥", "¥"):
|
||||
p = dict.fromkeys("MLS", 0.0)
|
||||
one["단가"] = {"T": p["M"] + p["L"] + p["S"], **p}
|
||||
s["T"] += _tr(q * one["단가"]["T"], 1)
|
||||
for k in "MLS":
|
||||
a = _tr(q * p[k], 1)
|
||||
s[k] += a
|
||||
if one.get("밑줄"):
|
||||
base[k] += a
|
||||
one["금액"] = {k: round(s[k] - before[k], 6) for k in "TLMS"}
|
||||
return out, _finish(s, opt)
|
||||
|
||||
|
||||
def same(got: dict[str, float], saved: dict | None) -> bool:
|
||||
return all(abs(got[k] - _f((saved or {}).get(k))) < 0.5 for k in "MLS")
|
||||
|
||||
|
||||
# ── 중기 구성(㉰) ─────────────────────────────────────────────────────
|
||||
def make_of(name: str, spec: str, rows: list[dict]) -> dict[str, Any]:
|
||||
"""중기 구성 줄 → `M02_San_MyMachine` 구성 꼴(손료 계수 × 10⁴ · 가격 천원 × 1000 · 조종원 인 · 주연료 ℓ ·
|
||||
잡재료율 % · 전력 kWh) — 엑셀 「중기사용료」 `_stm_sheet` 와 같은 값."""
|
||||
make: dict[str, Any] = {"이름": name, "규격": spec, "손료": []}
|
||||
for one in rows:
|
||||
g, q, unit = one["갈래"], one["수량"], one["단위"].strip().upper()
|
||||
if g == "S" and unit in ("천원", "$") and q and (one.get("단가") or {}).get("S"):
|
||||
make["손료"].append([q * 10_000, one["단가"]["S"] * 1000, False])
|
||||
elif g == "L" and q > 0 and not make.get("조종원"):
|
||||
make["조종원"], make["조종원직종"] = q, one["이름"].strip()
|
||||
elif g == "M" and unit in ("L", "ℓ") and q > 0 and make.get("주연료") is None:
|
||||
make["주연료종류"], make["주연료"] = one["이름"].strip(), q
|
||||
elif g == "M" and unit == "KWH" and q > 0:
|
||||
make["전력"] = q
|
||||
elif (g == "율" or unit.endswith("%")) and make.get("잡재료율") is None:
|
||||
make["잡재료율"] = q
|
||||
return make
|
||||
|
||||
|
||||
def peer_of(make: dict) -> dict[str, Any]:
|
||||
"""같은 파일 구성 대조 꼴(`M02_Machine_Find.peers` — 손료 [[계수, 가격]] · 주연료 · 조종원)."""
|
||||
loss = [x[:2] for x in make["손료"]]
|
||||
return {"손료": loss, "주연료": make.get("주연료"), "조종원": bool(make.get("조종원"))}
|
||||
|
||||
|
||||
def compose(
|
||||
rows: list[dict], res: dict[str, dict], ini: dict, sbm: dict
|
||||
) -> tuple[dict[str, dict], dict[str, dict], list[str]]:
|
||||
"""구성 행 → ({일위 코드: {줄, 줄합, 표지}}, {중기 코드 · 「이름|규격」: 구성}, 파일 표지) ·
|
||||
다시 셈 ≠ 저장(값 있는 호표) = 호표 표지(일위) · 파일 표지(중기) — 가져오기는 됨."""
|
||||
from M02_MasterTemplete.M02_San_MyMachine import key_of
|
||||
|
||||
groups: dict[str, list[dict]] = {}
|
||||
for r in rows:
|
||||
groups.setdefault(r.get("BDCODE") or "", []).append(r)
|
||||
blocks, makes, marks = {}, {}, []
|
||||
for parent, group in groups.items():
|
||||
head = res.get(parent)
|
||||
if parent[:1] not in ("B", "X") or not head:
|
||||
continue
|
||||
got, tot = lines(group, res, options(parent, ini, sbm.get(parent)))
|
||||
saved = head.get("원단가") or head.get("저장") or {}
|
||||
ok = not _f(saved.get("T")) or same(tot, saved)
|
||||
if parent[0] == "B":
|
||||
blocks[parent] = {"줄": got, "줄합": tot, "표지": [] if ok else [MISMATCH]}
|
||||
continue
|
||||
if not ok:
|
||||
marks.append(f"중기 {parent} {MISMATCH}")
|
||||
make = make_of(str(head.get("이름") or ""), str(head.get("규격") or ""), got)
|
||||
if make["손료"]:
|
||||
makes[parent] = makes[key_of(make["이름"], make["규격"])] = make
|
||||
return blocks, makes, marks
|
||||
|
||||
|
||||
# ── HP 호표(일위대가 불러오기 길) ───────────────────────────────────────
|
||||
def _hp(m: dict | None) -> dict[str, Any]:
|
||||
return {_HP[k]: (round(v, 6) if isinstance(v, float) else v) for k, v in (m or {}).items()}
|
||||
|
||||
|
||||
def hopyo(blocks: list[dict]) -> list[dict[str, Any]]:
|
||||
"""일위 블록(줄 있음) → HP 호표 `{번, 코드, 이름, 규격, 단위, 금액, 줄}`(번 = 코드 숫자 · 줄 = 자원 · 율 줄)."""
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as he
|
||||
|
||||
out = []
|
||||
for b in blocks:
|
||||
if not b.get("줄"):
|
||||
continue
|
||||
rows = []
|
||||
for one in b["줄"]:
|
||||
if one["갈래"] in ("글", "구분", "닫기"):
|
||||
continue
|
||||
code = one["코드"]
|
||||
line = {"차례": len(rows), **{k: one[k] for k in ("이름", "규격", "수량", "단위")}}
|
||||
line |= {"단가": _hp(one.get("단가")), "금액": _hp(one.get("금액")), "코드": code}
|
||||
line["비고"] = "별산" if one.get("별도계상") else ""
|
||||
for k in ("계", "노무비", "재료비", "경비"):
|
||||
line["단가"].setdefault(k, 0)
|
||||
line["금액"].setdefault(k, 0)
|
||||
line["갈래"] = he.kind_of(line)
|
||||
line["참조"] = (
|
||||
{"종류": "단산", "번": int(code[1:]), "코드": code}
|
||||
if code[:1] == "D" and code[1:].isdigit()
|
||||
else {"종류": "일위", "번": int(code[1:]), "코드": code}
|
||||
if code[:1] == "B" and code[1:].isdigit()
|
||||
else None
|
||||
)
|
||||
rows.append(line)
|
||||
head = {"번": int(b["코드"][1:]), "코드": b["코드"]}
|
||||
head |= {k: str(b.get(k) or "") for k in ("이름", "규격", "단위")}
|
||||
out.append({**head, "금액": _hp(b.get("저장")), "줄": rows})
|
||||
return out
|
||||
|
||||
|
||||
SHEET = "일위대가표"
|
||||
_COLS = "ABCDEFGHI"
|
||||
_HEAD = ("품명", "규격", "수량", "단위", "재료비", "노무비", "경비", "합계", "코드")
|
||||
|
||||
|
||||
def sheet(hps: list[dict], sid: str) -> dict[str, Any]:
|
||||
"""HP 호표(행 있음) → 격자 「일위대가표」 시트(머리 줄 = 「제N호표」 · 금액 = 저장 · 줄 금액 = 다시 셈)."""
|
||||
cells: dict[str, dict] = {f"{c}1": {"값": h} for c, h in zip(_COLS, _HEAD)}
|
||||
|
||||
def put(at: int, vals: list) -> None:
|
||||
for c, v in zip(_COLS, vals):
|
||||
if v not in (None, ""):
|
||||
cells[f"{c}{at}"] = {"값": v}
|
||||
|
||||
for h in hps:
|
||||
money = [h["금액"].get(k) for k in ("재료비", "노무비", "경비", "계")]
|
||||
put(
|
||||
h["행"][0],
|
||||
[f"제{h['번']}호표 {h['이름']}", h["규격"], None, h["단위"], *money, h["코드"]],
|
||||
)
|
||||
for ln in h["줄"]:
|
||||
money = [ln["금액"].get(k) for k in ("재료비", "노무비", "경비", "계")]
|
||||
put(ln["행"], [ln["이름"], ln["규격"], ln["수량"], ln["단위"], *money, ln["코드"]])
|
||||
widths = (36, 22, 10, 6, 12, 12, 12, 12, 10)
|
||||
return {
|
||||
"id": sid,
|
||||
"이름": SHEET,
|
||||
"칸": cells,
|
||||
"틀고정": {"행": 1},
|
||||
"열": {c: {"폭": w} for c, w in zip(_COLS, widths)},
|
||||
}
|
||||
|
||||
|
||||
def materials(saved: dict):
|
||||
"""일위 구성 자재 줄(M) → (블록, 줄, 자원 {이름 · 규격 · 단위 · 코드 · 재료찾기?}) — 재료 찾기 = 산근 줄과 같은
|
||||
길(827-2 `M02_San_Link.m01` · 파일 단서 · 머리 = 부모 일위 이름 규격) · 유가 · 환율 줄 = 재료찾기 없음."""
|
||||
from M02_MasterTemplete import M02_San_Link as link
|
||||
from M02_MasterTemplete import M02_San_MatFind as mf
|
||||
|
||||
for b in saved.get("일위") or []:
|
||||
head = f"{b.get('이름') or ''} {b.get('규격') or ''}"
|
||||
for ln in b.get("줄") or []:
|
||||
if ln.get("갈래") != "M":
|
||||
continue
|
||||
res = {k: ln.get(k) or "" for k in ("이름", "규격", "단위", "코드")} | {"종류": "자재"}
|
||||
with mf.clues_on(saved.get("단서"), head):
|
||||
link.m01("재료", res, "")
|
||||
yield b, ln, res
|
||||
@@ -179,7 +179,8 @@ def entry(item: dict, sym: str, excel: Any) -> dict | None:
|
||||
|
||||
# ── 판(그 파일 재료 종) ──────────────────────────────────────────────────
|
||||
def census(folder: str | Path, key: str) -> list[dict]:
|
||||
"""HP 블록마다 재료 줄 판정 → 종 [{종, 이름, 규격, 단위, 판정, 방법, 키, 후보, 단서, 별도계상, 블록, 줄}]."""
|
||||
"""HP 블록마다 재료 줄 판정 → 종 [{종, 이름, 규격, 단위, 판정, 방법, 키, 후보, 단서, 별도계상, 블록, 줄,
|
||||
호표?(828-1 일위 구성 줄 · 코드 숫자)}]."""
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
|
||||
|
||||
@@ -216,6 +217,22 @@ def census(folder: str | Path, key: str) -> list[dict]:
|
||||
g["줄"] += 1
|
||||
if no not in g["블록"]:
|
||||
g["블록"].append(no)
|
||||
from M02_MasterTemplete import M02_San_Ilwi as il
|
||||
|
||||
for b, _, res in il.materials(src) if src else (): # 828-1 일위 구성 줄(호표 = 코드 숫자)
|
||||
if not (found := res.get("재료찾기")):
|
||||
continue
|
||||
k = kind_of(res)
|
||||
g = groups.setdefault(
|
||||
k,
|
||||
{"종": k, **{x: _text(res.get(x)) for x in ("이름", "규격", "단위")},
|
||||
**{x: found.get(x) for x in ("판정", "방법", "키", "후보", "단서")},
|
||||
"별도계상": _apart(res), "블록": [], "줄": 0},
|
||||
) # fmt: skip
|
||||
g["줄"] += 1
|
||||
hp = g.setdefault("호표", [])
|
||||
if (n := int(str(b["코드"])[1:])) not in hp:
|
||||
hp.append(n)
|
||||
out_ = sorted(groups.values(), key=lambda g: (KINDS.index(g["판정"]) if g["판정"] in KINDS else 9, -len(g["블록"]), g["이름"])) # fmt: skip
|
||||
atomic_write_json(path, {**saved, CENSUS: out_})
|
||||
return out_
|
||||
|
||||
@@ -151,7 +151,7 @@ def _stm_sheet(rows: list[list]) -> dict[str, dict]:
|
||||
cur["손료"].append([_num(v[2]) * 10_000, _num(v[4]) * 1000, False])
|
||||
elif len(v) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"):
|
||||
cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip()
|
||||
elif first == "잡품" and len(v) > 2 and cur.get("잡재료율") is None:
|
||||
elif first in ("잡품", "잡재료", "잡재료비") and len(v) > 2 and cur.get("잡재료율") is None:
|
||||
cur["잡재료율"] = _num(v[2])
|
||||
elif len(v) > 3 and str(v[3]).upper() in ("L", "ℓ"):
|
||||
_fuel(cur, v[0], v[2])
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
|
||||
.stc = ZIP · COSTN.DBF(자원 · 산근 머리 · 저장 금액) · COSTN.SMT(SIxMemo · 산근 원문 · IFORM =
|
||||
<HII 꼬리표 · 길이 · 블록 번호>) · BOQ11.DBF(내역).
|
||||
BDQTY.DBF(일위대가 구성) · SYSINFO = 난독화 → 표지.
|
||||
BDQTY.DBF(일위대가 · 중기 구성) = 828-1 `M02_San_Bdqty`(믿을 칸 · 스스로 검사 · 어긋나면 물러남 = 일위 줄 없음 +
|
||||
표지) → `M02_San_Ilwi`(일위 구성 줄 · 다시 셈 · 중기 구성 `기계구성` · 같은 파일 구성 대조 `같은구성`).
|
||||
SYSINFO = 난독화 → 표지. 금액 칸 = 차수 1(산근 · 자원 = 엑셀 단가산출근거와 같음) · 낙찰률 줄(NAGCD > 0) 2 칸 =
|
||||
`원단가`(828-1 ㉱ — 구성 셈 · 일위 머리 = 엑셀 일위대가표와 같음 · 2~4 차 수량은 못 읽음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,10 +27,12 @@ from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
KIND = {"L": "노임", "M": "자재", "S": "경비", "E": "기계", "X": "기계", "D": "산근", "B": "일위"}
|
||||
#: 금액 칸(차수 1) → 비목 글자
|
||||
AMT = {"TAMT1": "T", "LAMT1": "L", "MAMT1": "M", "SAMT1": "S"}
|
||||
AMT2 = {"TAMT2": "T", "LAMT2": "L", "MAMT2": "M", "SAMT2": "S"} # 낙찰률 줄 원단가(828-1 ㉱)
|
||||
BOQ_AMT = {"T_AMT1": "T", "L_AMT1": "L", "M_AMT1": "M", "S_AMT1": "S"}
|
||||
#: 엑셀 숨은 J 열 메타 이름 ← COSTN 칸(`M02_San_Eval.stmate_block` 이 읽음)
|
||||
META = {"UNT": "UNIT", "UC": "UC", "NAG": "NAGCD", "SBM": "SBMOK", "ADJ": "S_ADJ"}
|
||||
UNREAD = ("BDQTY.DBF 난독화 — 일위대가 구성 줄 못 읽음", "SYSINFO.DBF 난독화")
|
||||
FALLBACK = "BDQTY 구성표 안 씀 — " # + 까닭(828-1 스스로 검사)
|
||||
|
||||
|
||||
class StcError(ValueError):
|
||||
@@ -164,16 +169,26 @@ def read(data: bytes) -> dict[str, Any]:
|
||||
"단위": r.get("UNIT"),
|
||||
"저장": saved,
|
||||
}
|
||||
if _f(r.get("NAGCD")) > 0: # 낙찰률 줄 원단가(828-1 ㉱ — 구성 셈 · 일위 머리)
|
||||
res[code]["원단가"] = _amt(r, AMT2)
|
||||
prices[code] = {**saved, "meta": {k: r.get(f) for k, f in META.items()}}
|
||||
out: dict[str, Any] = {"꼴": "STmate", "산근": [], "일위": [], "자원": res, "단가": prices}
|
||||
ini = z.read(names["START8X.INI"]).decode("cp949", "replace") if "START8X.INI" in names else ""
|
||||
out["옵션"] = dict(re.findall(r"^\s*(\w+)\s*=[ \t]*(.*?)\s*$", ini, re.M | re.A))
|
||||
comp, why = _composition(z, names, costn, out)
|
||||
for r in costn:
|
||||
code, text = r.get("CODE") or "", memo(r.get("IFORM") or b"", smt)
|
||||
if not code or (text is None and code[:1] != "B"):
|
||||
continue
|
||||
tags: list[str] = []
|
||||
head = {k: res[code][k] for k in ("이름", "규격", "단위", "저장")}
|
||||
if text is None and code in comp: # 일위 머리 = 원단가(엑셀 일위대가표와 같음)
|
||||
head["저장"] = res[code].get("원단가") or head["저장"]
|
||||
out["일위"].append({"코드": code, **head, **comp[code]})
|
||||
continue
|
||||
if text is None:
|
||||
out["일위"].append({"코드": code, **head, "줄": [], "줄합": None, "표지": [UNREAD[0]]})
|
||||
mark = ["BDQTY 구성 행 없음"] if why is None else [UNREAD[0], FALLBACK + why]
|
||||
out["일위"].append({"코드": code, **head, "줄": [], "줄합": None, "표지": mark})
|
||||
continue
|
||||
lines = [stc_line(i, ln, res, tags) for i, ln in enumerate(text.splitlines(), 1)]
|
||||
_, tot = ev.run_stmate([ln["풀이"] for ln in lines], prices)
|
||||
@@ -193,14 +208,40 @@ def read(data: bytes) -> dict[str, Any]:
|
||||
}
|
||||
for r in dbf(z.read(names["BOQ11.DBF"]))
|
||||
]
|
||||
ini = z.read(names["START8X.INI"]).decode("cp949", "replace") if "START8X.INI" in names else ""
|
||||
out["옵션"] = dict(re.findall(r"^\s*(\w+)\s*=[ \t]*(.*?)\s*$", ini, re.M | re.A))
|
||||
|
||||
odd = sorted({c[:1] for c, v in res.items() if v["종류"] is None})
|
||||
read_ok = "BDQTY.DBF 구성표 읽음 — 글자 칸 · 2~4 차 수량은 못 읽음(이름 · 규격 = 자원 표)"
|
||||
out["표지"] = [
|
||||
*UNREAD,
|
||||
"금액 = 차수 1 칸(TAMT1 · LAMT1 · MAMT1 · SAMT1)",
|
||||
*([read_ok] if why is None else [UNREAD[0], FALLBACK + why]),
|
||||
UNREAD[1],
|
||||
*out.pop("구성표지", []),
|
||||
"금액 = 차수 1 칸(TAMT1 · LAMT1 · MAMT1 · SAMT1) · 일위 머리 · 구성 셈의 낙찰률 줄 = 2 칸(원단가)",
|
||||
"블록 끝전 갈래 = 옵션 SAN_TYP 후보(뜻 미확정)",
|
||||
*(f"모르는 코드 머리 {c}" for c in odd),
|
||||
]
|
||||
return out
|
||||
|
||||
|
||||
def _composition(
|
||||
z: zipfile.ZipFile, names: dict, costn: list[dict], out: dict
|
||||
) -> tuple[dict, str | None]:
|
||||
"""BDQTY 구성표 → ({일위 코드: {줄, 줄합, 표지}}, 안 쓴 까닭 | None) · 중기 구성 = `out["기계구성"]` ·
|
||||
같은 파일 구성 대조 = `out["단가"][X]["같은구성"]`(816-2 `M02_Machine_Find.peers`)."""
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
from M02_MasterTemplete import M02_San_Bdqty as bq
|
||||
from M02_MasterTemplete import M02_San_Ilwi as il
|
||||
|
||||
if "BDQTY.DBF" not in names:
|
||||
return {}, "BDQTY.DBF 없음"
|
||||
try:
|
||||
rows = bq.decode(z.read(names["BDQTY.DBF"]))
|
||||
except Exception as e: # noqa: BLE001 — 못 읽음 = 물러남(오류 아님)
|
||||
return {}, f"못 읽음 {type(e).__name__}"
|
||||
if why := bq.check(rows, set(out["자원"])):
|
||||
return {}, why
|
||||
sbm = {r.get("CODE"): r.get("SBMOK") for r in costn}
|
||||
blocks, makes, marks = il.compose(rows, out["자원"], out["옵션"], sbm)
|
||||
out["기계구성"], out["구성표지"] = makes, marks
|
||||
xs = {c: (m["이름"], m["규격"], il.peer_of(m)) for c, m in makes.items() if c[:1] == "X"}
|
||||
for code, got in mfind.peers(xs).items():
|
||||
out["단가"][code]["같은구성"] = got
|
||||
return blocks, None
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# 813 BDQTY 복호 · STmate 분석엔진 · 엑셀 읽기 개선 정밀 분석 (읽기 전용)
|
||||
|
||||
세기 스크립트 = `tmp/brain_813/*.py`(load · evid · cmp_* · replay* · blines* · mach* · m131 · make_tsv) · 줄별 표 = `tmp/brain_813/STC_구성표로_풀리는줄.tsv`(240 줄 · 단가 없음). 코드 · 자료 · storage · `tools/` 고치지 않음. 복호는 `tools/stmate-test/bdqty.py` 를 읽기 전용으로 불러 씀. 줄 = 산근(D) 블록 안 자재 줄(지금 census 와 같은 단위 · 소광 3 판 중복 세짐).
|
||||
|
||||
## 0. 요약 열 줄
|
||||
1. 복호 맞음 = 실무 STC 7 건에서 일위대가(B) 216 + 중기(X) 176 = 392 호표 중 391 재계산이 저장 단가와 같음(1 건 = 복호는 정상 · 시험 셈 규칙 구멍 · 아래 1-3). 엑셀과도 따로 맞음 — 일위대가표 B 호표 136/136 · 중기사용료 X 118/118 이 코드 차례 · 수량 100 % 같고 숨은 메타 플래그도 100 % 같음.
|
||||
2. 읽히는 것 = 부모 · 자식 코드 · 1 차 수량 · 나누기 · 비율 줄 표시 등 약 20 칸. 못 읽는 것 = 이름 · 규격 · 단위(글자 칸) · 2~4 차 수량 · 비목 글자(어긋남) · 중기 코드.
|
||||
3. 글자 칸에 든 정보는 얕음 — 엑셀로 견주면 줄 이름 · 규격을 덮어쓴 것은 479 줄 중 5 줄(1 %)뿐, 나머지는 자원 표 이름 그대로.
|
||||
4. 자식 코드는 자원 표에 100 % 이어짐(3,510 자원 줄 중 빠짐 0) — 글자 칸 없이도 이름 · 규격 · 단위 채워짐. 안 이어지는 것은 제어 줄 1,380 개(구분 T · 닫기 C · 율 PR/RR · 빈 머리줄)뿐.
|
||||
5. STC 만으로 B 일위대가 구성 복원 가능 — 엑셀 일위대가표와 호표 136 개 · 706 줄 다른 줄 0(영덕 · 소광 STC 는 엑셀에 없는 호표 16 개 더 가짐).
|
||||
6. 재료 이음률: 일위대가 안 자재 줄 196 줄(율 줄 4 빼고)을 STC 에 더하면 분모 127 → 323, 이음 87 → 194, 68.5 % → 60.1 %(분모가 커져 내려감 · 새 줄 이음 54.6 %). 엑셀도 같은 몫(≈ 62 %).
|
||||
7. 산근 「규격 없음 4 · 후보 여럿 29 · 우리 표에 없음 7」 40 줄은 구성표 단서(구성 줄 · 부모 일위대가 이름)를 넣어도 0 줄 풀림. 별도계상 자리표 ↔ 실제 품목은 BDQTY 에 없고 내역서에만 있음(영월 파형강관 M00865 ↔ M01092).
|
||||
8. 기계: STC 기계 X 176 행 모두 손료 · 연료 · 조종원 · 잡품 구성이 읽힘(엑셀 중기사용료와 118/118 같음). 지금도 673 줄 100 % 이어짐이라 「같은 파일 구성 대조」 이득은 0 줄(별칭 끄고 재도 0) · 이득 = 내 기계 씨앗 공급처(STC 쪽 `기계구성` 없음).
|
||||
9. 엑셀 쪽 17 줄(규격 없음 4 · 후보 여럿 13)도 일위대가표 패턴으로 0 줄 풀림 — 같은 줄이고 엑셀은 이미 일위대가표를 단서로 씀. EST 출력 엑셀은 해당 없음.
|
||||
10. 제품에 넣을 때 = 표 283 자리가 7 건에서 맞춘 값 · 모듈이 `tools/` 에 있음(제품은 `smt_parser.py` 를 거울로 두는 길이 이미 있음) · 깨짐 검사는 「자식 코드가 자원 표에 있는가」 한 줄로 싸게 가능.
|
||||
|
||||
## 1. 복호로 읽게 된 것
|
||||
### 1-1 레코드 · 칸
|
||||
- 파일 = 7 건 · 레코드 4,890 · 호표(부모) B 216 · X 176 · D 367 · 전 레코드 삭제표시 0x45(`bdqty.py:2`) · 길이 415(봉화 · 영월 · 대흥) / 426(영덕 · 소광 3 판 · 끝에 SPRPT C11) · 읽는 시간 883 줄 0.08 초.
|
||||
- 영월은 칸 차례가 다름(NAME1 · NAME2 · SIZE1 · SIZE2 · UNIT1 순) — 표가 자리(바이트 위치) 기준이라 같은 표로 읽힘.
|
||||
- 표 = 283 자리(1~12 · 133~362 · 374~414 · `bdqty.py:14-47`). 표 밖 = 13~132(NAME1 · SIZE1 · UNIT1) · 363~373 · 414 뒤. 칸이 표 밖에 걸치면 조용히 빠짐(오류 없음 · `bdqty.py:94`).
|
||||
|
||||
| 칸 | 뜻(추정 · 엑셀 메타 대조) | 믿을 만한가 |
|
||||
|---|---|---|
|
||||
| BDCODE · CODE | 부모 호표 · 자식 자원 코드(M 자재 · L 노무 · S 경비 · X 중기 · B 일위 · D 산근 · W 일식) | 예 — 재계산 391/392 |
|
||||
| QTY1 | 1 차 수량 | 예 |
|
||||
| QTY2~4 | 2~4 차 수량 | 아니오 — 반복 쓰레기값(PLAN 도 「차수 2~4 수량 일부」 재현 불가로 적음) |
|
||||
| DEVD1~4 · EXIST1 · PRPT · PRIN · PROUT · KWNGP · BL_AMT | 나누기 · 존재 · 율 밑줄 표시 · 율 밑수 비목 · 대상 비목 · 관급 | 예 — 엑셀 메타 EXI 706/706 · DVD · IPR 666/666 · KWN 140/140 · PRI · PRO 16/16 · BLA 665/666 |
|
||||
| BIMOK | 비목(중기 3 줄 노 · 재 · 경) | 아니오 — 6 파일 L/K/Q · 영월 L/M/S · 순서 같음(K=M · Q=S 로 어긋남 · 추정) |
|
||||
| EQCODE | 율 줄 율 코드(엑셀 `EQC|A01`) | 아니오 — 「An1」로 나옴(0 → n) |
|
||||
| X_QTY1~3 · X_HAL1~3 · S_RATE · X_RATE · SBMOK | 할증 전 수량 · 할증률 · 적용 표시 | 값은 나오나 재계산에 안 쓰임 — 확인 못 함 |
|
||||
| X_CODE · NAME2 · SIZE2 · UNIT1(영월) | — | 아니오 — 모든 줄이 같은 쓰레기값 |
|
||||
| NAME1 · SIZE1 · UNIT1 | 줄 이름 · 규격 · 단위 덮어쓰기 | 읽기 불가 |
|
||||
|
||||
### 1-2 줄 갈래 분포(7 건 합 · 레코드 수)
|
||||
| 부모 | 호표 | 줄 | 자식 갈래 |
|
||||
|---|---|---|---|
|
||||
| B 일위대가 | 216 | 1,110(호표당 중앙 5 · 최대 17) | D 456 · M 200 · B 142 · L 140 · 빈 머리줄 70 · X 46 · PR 36 · W 15 · C 4 · S 1 |
|
||||
| X 중기 | 176 | 667(중앙 4 · 최대 5) | M 203(연료 159 + 잡품 % 44) · S 195(손료 · 천원) · L 154(조종원) · PR 115(잡품 %) |
|
||||
| D 산근 | 367 | 3,113 | X 1,422(중기 한 대에 3 줄 · 비목 노 · 재 · 경) · T 구분 1,037 · L 344 · M 128 · PR 78 · S 55 · C 35 · D 9 · RR 5 |
|
||||
- D 부모 줄은 `COSTN.SMT` 산근 글과 겹침 — 371 산근 블록 중 371 에서 코드 집합 같음 → D 쪽은 새 정보 없음. 새 정보 = B · X 구성.
|
||||
- 빈 코드 70 줄 = 일위대가 머리 설명줄(엑셀 「건설품셈 공통 6-3-3」 같은 품셈 근거 글 · 글자 칸에 들었을 것으로 봄).
|
||||
|
||||
### 1-3 복호가 맞다는 증거의 범위
|
||||
| 잣대 | 범위 | 결과 |
|
||||
|---|---|---|
|
||||
| 시험 `test_stmate_ilwi_eval.py:50-63`(B · X 재계산 = COSTN 저장 단가 1 원) | 원설계 5 + 영덕 변경 = 6 건 | 통과 |
|
||||
| 같은 시험 `:66-74`(기초단가만 두고 연쇄 재계산) | 원설계 5 건(영덕 · 소광변경1 은 안 돌림) | 바뀐 단가 0 · 총공사비 같음 |
|
||||
| 이번 재측정(`evid.py` · 같은 헬퍼) | 7 건 B 216 + X 176 | 391 같음 · 1 불일치(소광변경1 B00038 · 아래 한계) |
|
||||
| 엑셀 일위대가표 대조(`cmp_ilwi.py`) | 봉화 45 · 영월 23 · 영덕 9 · 소광 25 · 대흥 34(코드 새로 매겨져 이름 짝) | 호표 136/136 코드 차례 · 수량 같음 |
|
||||
| 엑셀 중기사용료 대조(`cmp_x.py`) | 5 건 X 118 호표 444 줄 | 118/118 같음 |
|
||||
- 불일치 1 건(B00038) = 복호 탓 아님: 줄이 `C1`(닫기) 다음 `PR 90`(PRIN 4 · PROUT 4 = 전체 비목 90 %)인데 시험 프로그램 셈(`smt_eval.js` `evaluateComposition`)이 PR 줄의 「닫기 뒤 전체 비율」을 안 함 — 저장값 = 닫기 앞 합 × 0.9(6509 · 46685 · 10696 손계산 일치). 7 건 중 이 꼴은 이 1 호표뿐.
|
||||
- 한계: 변경 차수 호표(NAGCD>0 · 영덕 10 · 소광 12 · 소광변경1 23)는 시험이 해당 칸(2)의 저장 단가와 맞췄고 2~4 차 수량(QTY2~4)은 못 읽음 — 구성 수량이 차수마다 달라지는 파일에서는 재현 불가.
|
||||
|
||||
## 2. 자원 표와 엮기 · 별도계상
|
||||
- 자식 코드 → COSTN(이름 · 규격 · 단위 · 단가): 자원 줄 3,510 중 빠짐 0 = 100 %. 안 이어지는 것 = 제어 줄 1,380(T1~T3 구분 1,037 · 빈 머리줄 70 · PR 229 · RR 5 · C 39) — 이름 없음이 정상.
|
||||
- 「별도계상」 = BDQTY 칸이 아니라 COSTN 자재 줄 SIZE 글 「별도계상」(엑셀 비고 「별산M…」 · 「별산자재」와 1:1 — 영월 17/17 · 대흥 16/16 · 소광 9/9 · 엑셀 쪽 영월 S 1 줄 예외).
|
||||
- 구성 안 별도계상 자재 줄 59(레미콘 · 이형철근 · 시멘트 · 모래 · 파형강관 · 깬돌 …) — 자리표 줄이 곧 품목 줄. 규격이 있는 곳:
|
||||
| 어디 | 줄 | 하나로 정해짐 |
|
||||
|---|---|---|
|
||||
| 이름 속(파형강관(800mm) · 이형철근 D13 · 깬돌(L3=45cm)) | 13 | 지름까지만 · 두께 · 관급/일반은 안 정해짐 |
|
||||
| 부모 이름 머리 「D=800mm」 | 파형강관 8(+이름에 지름 없는 대흥 「68X13,1RS」 1 줄은 머리로 D1200 후보가 앞섬) | 지름까지 |
|
||||
| 어디에도 없음(레미콘 · 이형철근 · 시멘트 · 모래 · 자갈) | 레미콘 15 · 이형철근 15 · 시멘트 6 · 모래 6 · 자갈 1 | 안 됨 — 단, 시멘트 6 · 레미콘 5 줄은 규격 없이도 지금 엔진이 이어짐 |
|
||||
- 산근 줄의 자리표(영월 M00865 → 산근 D00071 · D00072 · D01932)와 실제 품목(M01092 · M01112 · M01110): 실제 품목은 어느 BDQTY 부모에도 없음(조회 0) · 내역서 줄에만 있음(같은 수량 224 · 34 · 15 · 지름 같음). 규칙 「자리표 = 같은 이름 · 지름 같은 내역서 줄」로 정해지는 건수 = 파형강관 11(산근 3 + 구성 8)/11 · 이미 엔진이 내역서 단서로 씀 · 남는 갈림 = 관급형/일반형(사람).
|
||||
- 구성표로 새로 정해지는 자리표 → 품목 건수 = 0(BDQTY 는 자리표 · 품목 연결을 안 담음).
|
||||
|
||||
## 3. STmate 원파일 엔진 개선 몫(정량)
|
||||
### ㉮ STC 만으로 일위대가 구성
|
||||
- 엑셀 일위대가표와 줄 단위 대조(`cmp_ilwi.py`): 봉화 45/45 · 영월 23/23 · 대흥 34/34(이름 짝) · 영덕 9/9 · 소광 25/25 호표 → 줄 706 줄 일치 · 다른 줄 0. STC 에만 있는 호표 = 영덕 4 · 소광 12(둘 다 내역서에서 부름 — 엑셀 일위대가표에는 안 찍힘 · 판 다름 가능).
|
||||
- 줄 이름 · 규격 · 단위는 자원 표 이름과 같음(정규화 후 479 줄 중 5 줄만 덮어쓰기) · 금액은 자원 단가 × 수량으로 재계산 가능(위 391/392).
|
||||
- 지금 STC 읽기는 `M02_San_Stc.read` 가 B 를 「줄 없음 · 구성 못 읽음」으로 둠(`M02_San_Stc.py:30` · 148~).
|
||||
|
||||
### ㉯ 일위대가 안에서만 쓰이는 자재
|
||||
- 자재 M 318 행 중 D · 내역 밖 B 또는 X 안 = 131 종(B 안 109 · X 안만 22) — 811-2 의 131 와 같음.
|
||||
- 131 종을 `M02_San_Link.m01`(= 재료 찾기 `find`)에 넣은 판정: 이어짐 53 · 우리 표에 없음 35 · 후보 여럿 10 · 규격 없음 8 · 유가 15(중기 연료) · 율 줄 10(잡품 · 공구손료). 자재 106 종 중 이어짐 50.0 %.
|
||||
- 줄 단위(B 부모 × M 자식 200 줄 · 율 4 줄 빼면 196 · `blines.py`): 이어짐 107 · 우리 표에 없음 40 · 규격 없음 30(전부 별도계상) · 후보 여럿 19.
|
||||
- STmate 원파일 재료 이음률: 지금 87/127 = 68.5 % → 구성 줄 더하면 194/323 = 60.1 %(분모 +196 · 새 줄만은 54.6 %). 「이어짐」은 늘되 비율은 내려감 — 새 줄이 더 어려운 품목(별도계상 · 규격 없는 구조용강관 · 모래 등)이라서.
|
||||
- 새 줄에서 못 이은 것의 까닭(40 + 49): 우리 자재표에 없음(구조용강관 5 · PE관 · 깬돌 별산 · 모래 · 자갈 · 떼 · 못 …) · 규격 없음(레미콘 · 이형철근 · 파형강관) · 후보 여럿(면목 9 · 와이어매쉬 4 · 결속선 3) — 엔진 문제 아닌 표 · 규격 문제가 대부분.
|
||||
|
||||
### ㉰ 산근 「규격 없음 4 · 후보 여럿 29」 단서
|
||||
- 구성표 단서 둘을 넣어 `emit` 을 다시 돌림(`replay.py` · `replay2.py` · 기준 재현 87/29/7/4 정확히 같음): ① 같은 코드 구성 줄 + 부모 B 머리 ② 산근 머리에 부모 일위대가 이름 덧붙임.
|
||||
- 결과 = 바뀐 줄 0/40(규격 없음 4 · 후보 여럿 29 · 우리 표에 없음 7 모두 그대로). 까닭: 종자 · 결속선 · 낙석방지책 형식 · 드릴샹크 · 지주는 구성표 어디에도 규격이 없음(811-2 와 같은 결론) · 부모 일위대가 이름은 산근 머리(이미 씀)와 같은 글. 줄별 표 = TSV 「기존 산근 줄」 40 줄(풀림 칸 전부 「안 풀림」).
|
||||
|
||||
### ㉱ 기계
|
||||
- 현재(census): STC 기계 673 줄 · 86 종 · 모두 이음(이어짐 591 · 별칭 32 · 조합 가름 50).
|
||||
- BDQTY X 176 행(86 종 이름 · 규격) 모두 손료 줄 있음(1 줄 157 · 2 줄 조합 19) · 연료 159 · 조종원 154 · 잡품 159. 꼴 = 손료(계수 · 가격 천원) · 조종원 1 인 · 연료 ℓ · 잡품 % — EST `makes`(`M02_San_Estx.py:makes`)와 같은 꼴이고 `M02_Machine_Find.peers`(`:332`)가 받는 `{손료 [[계수, 가격]], 주연료, 조종원}` 로 바로 변환됨(`mach.py`).
|
||||
- 별칭을 끈 단독 `find_all` 기준: 이어짐 159 · 못 이음 17(후보 여럿 4 · 못 찾음 13 · 9 종). 별칭이 푸는 것 = 13 행(7 종).
|
||||
- 같은 파일 구성 대조(`peers` → `narrow`): 풀림 0 행 · 같은 답 0 · 다른 답 0 — 까닭 = 못 이은 단독 기계와 구성이 같은 다른 단독 기계가 같은 파일에 없음(같은 구성 짝이 있는 X 는 19 행 · 그중 조합 15).
|
||||
- 손료 줄 이름(S 코드 「굴삭기(무한궤도) 0.7㎥」)을 기계 이름 대신 찾기: 별칭이 푼 13 행 중 같은 답 3 · 다른 답 0 · 답 없음 10 · 별칭 끈 못 이음 17 행 중 +6 행 풀림 · 반대로 3 행 새로 못 찾음(순이득 +3).
|
||||
- 내 기계 씨앗: 제품 STC 길엔 기계 구성 공급처가 없음(`est_makes` = EST · `xlsx_makes` = 엑셀만 · STC 원파일 `기계구성` 없음 — `M02_Sangeun_FromHopyo.py:133`). BDQTY X → `{이름 · 규격 · 손료 · 주연료 · 조종원 · 잡품 %}` 변환은 엑셀 `_stm_sheet`(`M02_San_MyMachine.py:123`)와 같은 값(118/118). 지금 7 건에서 「못 찾음 → 내 기계」 대상 0 종이라 이득은 새 파일 · 별칭 없는 환경 몫.
|
||||
|
||||
## 4. 엑셀 엔진 개선 몫 · 패턴 대응표
|
||||
| STmate 패턴 | STC(BDQTY) | 엑셀 | 지금 읽기기 |
|
||||
|---|---|---|---|
|
||||
| 호표 머리 · 줄 범위 | 부모 BDCODE · 코드 B/X | 일위대가표 「제 N 호표」 + 숨은 N 열 `BDCOD|B00011 POSS|7 POSE|12` · O 열 코드 | `M02_San_MatFind.from_book`(`:63`)이 머리 이름 · 규격만 씀 |
|
||||
| 자식 줄 코드 · 수량 | CODE · QTY1 | N 열 `COD|M00131` · 수량 칸 · 비고 칸(코드 · 출처 · 별산 · 자재 N — 공사 설정마다 다름) | M 줄 코드 · 이름 · 규격만(수량 · 단가 안 씀) |
|
||||
| 플래그 | DEVD · EXIST1 · BIMOK · PRPT · KWNGP · BL_AMT | `DVD · EXI · BMK · IPR · KWN · BLA`(BDQTY 와 706 줄 전부 같음) | 안 읽음 |
|
||||
| 율 줄(공구손료 · 잡재료) | CODE PR/RR + PRIN · PROUT · EQCODE | `COD|PR` `PRI|1 PRO|1 EQC|A01 UNI|%` · 이름 「…의 %」 | 안 읽음(율은 산근 글 쪽 `parse_line`) |
|
||||
| 닫기 · 일식 | C1 · C2 + W 줄(켜쌓기적용 「전체금액의 %」) | 빈 `COD` · W 코드 줄 | 안 읽음 |
|
||||
| 별도계상 | 자재 줄 SIZE 「별도계상」(BDQTY 아님) | 단가 0 · 비고 「별산M…」/「별산자재」 | SIZE 글로 판정(`M02_San_Link.apart`) · 비고 표기 안 씀 |
|
||||
| 차수 | QTY1~4 · EXIST1~4 | O 열 `M00131_1`(차수 번호 꼬리) | 안 읽음 · 금액은 차수 1 칸만 |
|
||||
| 중기 구성 차례 | 손료 S(천원 계수) → (조합이면 둘째 S) → 조종원 L 1 인 → 연료 M ℓ → 잡품(PR % 또는 M%) | 중기사용료 「제 N 호표」 같은 차례 | `_stm_sheet` 읽음(`M02_San_MyMachine.py:123`) |
|
||||
| 중기 부름 | D 부모 아래 X 3 줄(노 · 재 · 경) | 단가산출근거 세 줄 | 읽음(세 줄 로직) |
|
||||
| 코드 머리글자 | L 노임 M 자재 S 경비(손료 천원) X 중기 B 일위 D 산근 W 일식 T 구분 | 숨은 메타 `COD` | `KIND` 표(`M02_San_Stc.py:24`)에 B · D · X · M · L · S 있음 · W · C · T 없음 |
|
||||
| 총수량 | (BDQTY × 내역 수량 집계 — 시험 F3-24) | 재료비수량금액집계표(별도계상 총수량 영월 360 m = 224+87+34+15) | 버림(`STM_SHEETS` 밖) |
|
||||
- 쓰는 것 = 자원 목록표 · 단가산출근거 G 열 원문 · 일위대가표(자재 줄 단서만) · 중기사용료(기계 구성).
|
||||
- 안 쓰는 것 = 일위대가 줄 전체(수량 · 율 줄 · 플래그) · 설계내역서(`내역 = None` · `M02_San_Xlsx_Stmate.py:72`) · 재료비수량금액집계표 · 자재단가대비표.
|
||||
- 엑셀 쪽 「규격 없음 4 + 후보 여럿 13」 17 줄(종자 · 결속선 · 낙석방지책 · 드릴샹크 · 지주 · 블레이드 · 파형강관 · 이형철근): 위 패턴으로 풀리는 줄 0. 이유 = STC 실험과 같은 줄이며 엑셀은 일위대가표를 이미 단서로 쓰고 있음(`M02_San_Xlsx_Stmate.py:73`).
|
||||
- 엑셀 쪽 얻을 것 = 일위대가 구성 줄을 「재료 줄」로 세는 것(5 공사 M 줄 140 · 분모 102 → 234 · 이음 78 → 145 ≈ 62 %) · 율 줄 · 별산 표기 · 플래그를 구조로 읽기 — 일위대가 불러오기 쪽(`호표 길`)이 이미 같은 정보를 필요로 하면 재사용.
|
||||
- EST 출력 엑셀: 해당 없음(BDQTY · 숨은 `COD|` 메타 · 별산 표기 · 코드 머리글자는 STmate 전용 · EST 는 기계경비 · 단가산출 시트 꼴이 따로 읽힘).
|
||||
|
||||
## 5. 제품에 넣을 때 걸리는 것(사실)
|
||||
- 표 = 실무 7 건에서 맞춘 283 자리 값 · 새 판에서 레코드 길이 · 칸 자리가 바뀌면: ① 칸이 표 밖에 걸침 → 그 칸이 `row` 에서 조용히 빠짐 ② 자리는 같으나 문자가 표 밖(예: BIMOK K/Q · EQCODE n) → 쓰레기값이 값처럼 나옴 · 오류 없음. 영월(칸 차례 다름)에서 문제없었음 = 자리 기준이라서.
|
||||
- 키스트림 판간 동일성: SYSINFO 7 파일 100 % 같음(`STC_난독화_상태.md` §2) · BDQTY 83~100 %(값 열 흔들림 — 키 상이 증거 아님) → 새 STmate 판에서 키가 바뀔 가능성은 이 자료로는 닫혀 있지 않음.
|
||||
- 스스로 알아채는 검사(싼 것부터): ① 모든 자식 코드가 자원 표에 있거나 제어 코드(PR · RR · T · C · 빈) · 7 건 4,890 줄 전부 통과 ② 부모 코드 머리글자가 B · X · D ③ DEVD1 값 ∈ {F · 빈}(7 건 4,890 줄 전부) ④ B · X 재계산 = 저장 단가(1 원 · 7 건 392 중 391). ④는 `smt_eval.js` `evaluateComposition`(JS) 자리 값 필요 — 제품에 같은 셈은 산근 쪽(`M02_San_Eval`)뿐.
|
||||
- 자리: `tools/stmate-test/bdqty.py`(98 줄 · 표준 라이브러리만) · 제품 `M02_San_Stc.py` 는 이미 `tools/stmate-test/smt_parser.py` 를 「거울」로 두고 시험 `test_810_1_stc.py` 로 1:1 대조 — 같은 방식(CLAUDE.md 계산 자리 ① 짝 + 거울 시험)이 가능. `dbf_crypt.py`(쓰기)는 불필요.
|
||||
- 표 없는 자리 · 못 읽는 칸: 글자 칸 없이 코드 · 수량 · 플래그만 → 이름은 자원 표에서(100 %). 줄 이름 덮어쓰기(1 %)는 놓침.
|
||||
- 읽기 실패 때 물러날 자리: `M02_San_Stc.read` 의 「일위」 줄 없음 + 표지 `UNREAD`(`:30`) 길 — 검사 ①~③ 중 하나라도 실패하면 표지 유지 · 엑셀 길(일위대가표) · 산근만 길 그대로.
|
||||
- 경쟁사 암호를 제품이 푸는 일이 되는 점은 사용자 판단(지금은 시험 프로그램만 사용자 승인으로 사용).
|
||||
|
||||
## 6. 시험 프로그램이 이미 풀어 놓은 것 중 쓸 만한 것(`tools/stmate-test/PLAN.md` 구현)
|
||||
| 항목 | 내용 | 불러오기와 닿는 곳 |
|
||||
|---|---|---|
|
||||
| F2-12 일위대가 B 구성 조립(행 소수 1 버림 · CHBNM 총액우선) | 7 건 1 원 | B 줄 금액 검산 · 「틀린 줄」 판정 |
|
||||
| F2-08 / F2-03 중기 시간당 사용료(손료 · 운전원 시간 환산 CONLD · RXXNM · 연료 · 잡품) | 7 건 1 원 | 내 기계 씨앗의 조종원 · 환산(제품은 「×16/12 ×25/20 안 가져옴」 처리 — 대조 근거) |
|
||||
| F2-14 PR/RR 율 줄(PRPT 표시 행 밑수 PRIN → PROUT · C* 뒤 % = 전체) | 구현 | 일위대가 율 줄 · 켜쌓기적용 읽기 |
|
||||
| F2-13 호표 참조 위상 순서 · 순환(실무 7 건 순환 0) | 구현 | 일위대가 → 산근 → 중기 계산 차례 |
|
||||
| F2-10 · F3-02~12 산근 식 언어(변수 `;;` · 구간 `(=)(==)(-=)` · 라벨 `:=A01` · `&PA01:M&` · IF) | 282 개 1 원 | 제품은 이미 블록 100 % 대조(`test_810_1_stc`) — 새 이득 작음 · 함수 · 조건 범위 확인용 |
|
||||
| F3-18 낙찰률 단가(원단가 × 율 버림 · 127 개) | 구현 | 제품 낙찰률 값과 맞대기 |
|
||||
| F2-60 연쇄 재계산 · F4-09 차수 4 벌(NAGCD > 0 → 2 차 칸) | 원설계 5 건 | 변경설계 파일에서 제품은 1 차 칸만 읽음(`M02_San_Stc.AMT`) — 변경 STC(영덕 · 소광변경1)의 현행 단가 칸 |
|
||||
| F2-06 자재 후보 5 칸 단가 · 최저가 자동채택 | 7 건 변동 0 | 출처 칸 의미(적용 단가 선택) |
|
||||
| F3-24 · F3-28 내역 ↔ BDQTY 수량 집계(CON_QTY) | 실무 BOQ 대조 | 자재 총수량(별도계상 총수량 등 엑셀 집계표 값)을 STC 에서 산출 |
|
||||
| F4-04 · F4-05 DBF 03/E5/06 변형 읽기 · 무손실 왕복 | 7 건 | 제품 `dbf()` 는 삭제표시 `*` 만 건너뜀 — BDQTY 0x45 도 그대로 읽힘(내용은 암호) |
|
||||
|
||||
## 개선 후보 목록(권고 차례 · 결정 아님)
|
||||
| 차례 | 후보 | 좋아지는 것 | 필요한 것 | 위험 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | STC 기계 구성 공급(`stc_makes`: BDQTY X → `{손료 · 주연료 · 조종원 · 잡품}`) → 내 기계 씨앗 · 같은 파일 구성 대조 | 지금 7 건 이음 673/673 = 변화 0 · 새 파일에서 못 찾음 기계(별칭 끈 환경 17/176 = 9.7 %)의 내 기계 자동 생성 길이 STC 에도 열림 | `bdqty.py` 짝 복제 + 거울 시험 · `기계구성` 칸 채움 | 표 밖 칸 조용히 빠짐 · 새 판 깨짐 — 자식 코드 자원 표 검사로 막음 |
|
||||
| 2 | STC 일위대가(B) 구성 줄 읽기 → 재료 줄에 더하기 | 재료 줄 +196 · 이음 +107 · 일위대가 호표가 STC 에서 줄로 보임(706 줄 엑셀과 같음) · 「구성 못 읽음」 표지 소멸 | 위 모듈 + `M02_San_Stc.read` 일위 줄 채움 | 이음률 68.5 → 60.1 % 로 내려 보임(분모 증가) — 지표 설명 필요 · 새 못 이음 89 줄(우리 자재표 없음 40 · 규격 없음 30 · 후보 19) |
|
||||
| 3 | 엑셀 일위대가표를 줄(재료 · 율 · 별산)로 읽기 | 엑셀 재료 줄 102 → 234 · 이음 78 → 145 ≈ 62 % · STC 와 같은 줄 | `M02_San_Book` 일위대가표 줄 읽기(이미 시트 보관) | 일위대가 불러오기 길과 중복 읽기 — 한 곳에서만 |
|
||||
| 4 | 우리 자재표 보강(구조용강관 · PE관 · 모래 · 자갈 · 깬돌 별산 · 떼 · 못) | 새 줄 못 이음 「우리 표에 없음」 40 줄 중 상당수 | M01 자재 값 · 별칭(사람 확인) | 값 · 규격은 사용자 협의 |
|
||||
| 5 | 별도계상 자리표 ↔ 내역서 실제 품목 규칙(이름 core + 지름 수 · 수량 같음) 명문화 | 파형강관 11 줄 지름 · 두께 하나로(지금도 내역서 단서로 후보 순서) · 남는 것은 관급/일반 사람 몫 | 규칙 문서 · 표지 글 | 이름 · 지름 규칙이 파형강관 밖(이형철근 범위 「D13~D32」)에선 안 됨 |
|
||||
| 6 | 변경설계 STC 차수 2 칸(NAGCD > 0) 읽기 | 변경 파일 현행 단가 일치 | `AMT` 칸 분기 · 영덕 · 소광변경1 시험 | 2~4 차 수량 못 읽음 — 금액 칸만 |
|
||||
| 7 | BDQTY 글자 칸(NAME1 · SIZE1 · UNIT1) 복호 | 줄 이름 덮어쓰기 5/479(1 %) 정도 | 암호 새로 풀기(이번 범위 밖 — 안 함) | 얻을 것이 적음 · 권고 안 함 |
|
||||
@@ -1099,3 +1099,17 @@ ABC는 잠시대기, 내 질문에 대한 대답부터 단가산출의 항목,
|
||||
|
||||
1. 자재 매칭에서 3개걸음 가는건 동의하나 복수 자재에 대해서는 별칭 지정이 부적합함. 찾는데 없는 경우에 별칭을 사용하려는 목적임. 왜냐면 다른걸로도 변경될수 있으니. 예를들어서 모르터르, 몰탈, ... 이런 형식으로 영문을 한글화 하는 과정에서 발생되는 문제를 해결하기 위함.
|
||||
2. 같은기계 묶어보이기는 보류 내가 설명한 내용과 너가 인식한 내용이 서로 다른것같음. 위에 수정 다끝나고 전역 깃 싱크 후 서버 재실행 4173 재 빌딩. 이후 화면을 다시보고 판단하겠음
|
||||
|
||||
## 요구 813 (11-13 · 사용자 · 재료 매칭율 확인 · STmate 암호화 재료 DB 는 데스크탑 안티그래비티 워크트리에서 분석함 — 바로 쓰기는 어렵지만 패턴 · 정보를 참고해 엑셀 · STmate 분석 엔진 개선 방법을 정밀 분석)
|
||||
|
||||
재료 매칭율 확인
|
||||
|
||||
일단 stmate 암호화 재료 db는 데스크탑의 안티그래비티 워크트리에서 분석작업을 했어. 물런 경쟁사 정보다보니 우리프로그램에서 바로 사용하기는 힘들기는 한데 어떤 패턴이나 어떤정보가 있는지는 참고할 수는 있어보여. 일단 한번 보고 어떻게 엑셀과 stmate 분석엔진을 개선할수 있는지 정밀 분석해보자.
|
||||
|
||||
## 요구 814 (11-13 · 사용자 · 813 분석의 ㉮ 엑셀 일위대가표 숨은 칸 · ㉯ 원파일만으로 일위대가 · ㉰ 원파일 기계 구성 → 내 기계 · ㉱ 변경설계 2 차 칸 모두 반영 · 전체 매칭율 다시 계산 출력)
|
||||
|
||||
가나다라를 반영해주고 전체 매칭율 다시 계산출력
|
||||
|
||||
## 요구 815 (11-13 · 사용자 · 다 하면 연결 못 하는 자재 목록을 구글 스프레드시트에 별도 탭으로)
|
||||
|
||||
추가로 다하면 어떤 자재들이 연결을 못하는지 구글 스프레드 시트에 별도 탭으로 추가
|
||||
|
||||
@@ -37,7 +37,7 @@ FILES = {
|
||||
"소광22": SO / "1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC", # noqa: E501
|
||||
"소광변경1": SO / "변경 1차분-2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC", # noqa: E501
|
||||
} # fmt: skip
|
||||
#: 파일 → (산근 블록, 일위 블록 · 구성 못 읽음, SAN_TYP)
|
||||
#: 파일 → (산근 블록, 일위 블록(828-1 구성표 줄), SAN_TYP)
|
||||
WANT = {
|
||||
"봉화": (65, 45, "1"),
|
||||
"동부청영월": (69, 23, "0"),
|
||||
@@ -78,7 +78,8 @@ def test_mirror_smt_parser(tag: str) -> None:
|
||||
mine = S.dbf(m[name])
|
||||
assert len(mine) == len(theirs), name
|
||||
for a, b in zip(mine, theirs, strict=True):
|
||||
b = {k: v for k, v in b.items() if k != "_deleted"}
|
||||
# 거울 판(stmate-test)은 글 칸 앞 빈칸을 남김 — 제품은 양끝을 걷음(이름 맞춤 그대로)
|
||||
b = {k: v.strip() if isinstance(v, str) else v for k, v in b.items() if k != "_deleted"}
|
||||
if "IFORM" in a:
|
||||
assert a.pop("IFORM").hex() == b.pop("IFORM_HEX")
|
||||
b.pop("IFORM")
|
||||
@@ -97,7 +98,7 @@ def test_saved_amounts(tag: str) -> None:
|
||||
d = parsed(tag)
|
||||
n_cost, n_head, san = WANT[tag]
|
||||
assert (len(d["산근"]), len(d["일위"]), d["옵션"]["SAN_TYP"]) == (n_cost, n_head, san)
|
||||
assert all(b["표지"] == [S.UNREAD[0]] and not b["줄"] for b in d["일위"])
|
||||
assert all(b["줄"] and not b["표지"] for b in d["일위"]) # 828-1 구성표 읽음
|
||||
bad = []
|
||||
for b in d["산근"]:
|
||||
got = ev.stmate_block(b, [ln["풀이"] for ln in b["줄"]], d["단가"], RULE[san])["금액"]
|
||||
|
||||
@@ -71,11 +71,12 @@ def test_one_file_and_list_sheet(world) -> None:
|
||||
folder = world["personal"]
|
||||
with store.at(folder):
|
||||
got = fh.upload_native(folder, stc.name, stc.read_bytes(), t.WHO)
|
||||
assert got["원파일"] and got["호표수"] == 0 and "붙음" not in got
|
||||
assert got["원파일"] and got["호표수"] == 45 and "붙음" not in got # 828-1 구성표 일위대가
|
||||
grid = hs.grid(folder, got["키"])
|
||||
blocks = fn.numbers(t.native("STC", "봉화"))
|
||||
assert grid["보기"] == {"단가산출표": "s1"} and len(grid["단산"]) == len(blocks)
|
||||
head = grid["책"]["시트"][0]["칸"]
|
||||
assert grid["보기"] == {"단가산출표": "s1", "일위대가표": "s2"}
|
||||
assert len(grid["단산"]) == len(blocks) and len(grid["호표"]) == 45
|
||||
head = next(s for s in grid["책"]["시트"] if s["id"] == "s1")["칸"]
|
||||
assert [head[f"{c}1"]["값"] for c in "ABCDEFGH"] == list(fn._LIST_HEAD)
|
||||
first = next(iter(blocks.values()))
|
||||
assert head["A2"]["값"] == first["코드"] and head["H2"]["값"] == first["저장"]["T"]
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""828-1 지킴 — STmate 원파일 구성표(BDQTY) 읽기 · 실무 .STC 일곱 · 엑셀 짝 다섯(읽기만 · 임시 storage).
|
||||
|
||||
① 거울 — `M02_San_Bdqty.decode` = `tools/stmate-test/bdqty.py` 믿을 칸 1:1(7 건)
|
||||
② 스스로 검사 ① ~ ③ 실패 흉내(바이트 흩뜨림) → 일위 줄 없음 + 물러남 표지(오류 아님)
|
||||
③ 일위 구성 줄 = 엑셀 일위대가표(코드 차례 · 수량 · 136 호표 706 줄) · 다시 셈 = 저장 단가(B 216 + X 176)
|
||||
④ 중기 구성 = 엑셀 「중기사용료」(`M02_San_MyMachine._stm_sheet`) 118/118 · 내 기계 씨앗 꼴
|
||||
⑤ 낙찰률 줄(NAGCD > 0) — 일위 머리 · 구성 셈 = 2 칸 · 산근 · 자원 = 1 칸(엑셀 짝) · 원설계 그대로
|
||||
⑥ 일위대가 길 — 올림 → 호표 목록 · 「일위대가표」 시트 · 하나 만들기 금액 = 원본 · 재료 판에 구성 줄
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import warnings
|
||||
import zipfile
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for p in (ROOT, ROOT / "tools" / "stmate-test", ROOT / "resources" / "tester"):
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import helper_m02_hopyo_golden as golden # noqa: E402
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Bdqty as bq # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Ilwi as il # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_MatPick as mp # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_MyMachine as mm # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Stc as S # noqa: E402
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402
|
||||
from resources.tester import test_810_1_stc as stc # noqa: E402
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
|
||||
|
||||
#: 엑셀 짝 → 같은 공사 STC(코드 같음 · 울진대흥 = 코드 새로 매김 → 이름 짝)
|
||||
PAIR = {"봉화": "봉화", "동부청영월": "동부청영월", "울진대흥": "울진대흥", "영덕계류": "영덕계류", "울진소광": "울진소광"} # fmt: skip
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def parsed(tag: str) -> dict:
|
||||
return S.read(stc.raw(tag))
|
||||
|
||||
|
||||
def _members(data: bytes) -> dict[str, bytes]:
|
||||
z = zipfile.ZipFile(io.BytesIO(data))
|
||||
return {i.filename: z.read(i) for i in z.infolist()}
|
||||
|
||||
|
||||
def _bdqty_name(m: dict) -> str:
|
||||
return next(k for k in m if k.upper() == "BDQTY.DBF")
|
||||
|
||||
|
||||
# ── ① 거울 ───────────────────────────────────────────────────────────────
|
||||
@pytest.mark.parametrize("tag", list(stc.FILES))
|
||||
def test_mirror_bdqty(tag: str) -> None:
|
||||
import bdqty
|
||||
|
||||
m = _members(stc.raw(tag))
|
||||
raw = m[_bdqty_name(m)]
|
||||
theirs = [{k: r[k] for k in bq.TRUSTED if k in r} for r in bdqty.decode_bdqty(raw)]
|
||||
assert bq.decode(raw) == theirs and len(theirs) > 300
|
||||
|
||||
|
||||
# ── ② 스스로 검사 · 물러남 ───────────────────────────────────────────────
|
||||
def _field(raw: bytes, name: str) -> tuple[int, int, int, int]:
|
||||
"""(헤더 길이, 레코드 길이, 칸 자리, 칸 길이)."""
|
||||
_, hlen, rlen = struct.unpack_from("<IHH", raw, 4)
|
||||
o, off = 32, 1
|
||||
while raw[o] != 0x0D:
|
||||
if raw[o : o + 11].split(b"\0")[0].decode() == name:
|
||||
return hlen, rlen, off, raw[o + 16]
|
||||
off += raw[o + 16]
|
||||
o += 32
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def _scrambled(name: str) -> bytes:
|
||||
"""봉화 STC — BDQTY 첫 레코드 그 칸 바이트를 흩뜨린 사본."""
|
||||
data = stc.raw("봉화")
|
||||
m = _members(data)
|
||||
key = _bdqty_name(m)
|
||||
raw = bytearray(m[key])
|
||||
hlen, _, off, ln = _field(bytes(raw), name)
|
||||
for j in range(off, off + ln):
|
||||
raw[hlen + j] ^= 0x5A
|
||||
m[key] = bytes(raw)
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as z:
|
||||
for k, v in m.items():
|
||||
z.writestr(k, v)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["CODE", "BDCODE", "DEVD1"])
|
||||
def test_check_falls_back(name: str) -> None:
|
||||
d = S.read(_scrambled(name))
|
||||
why = [x for x in d["표지"] if x.startswith(S.FALLBACK)]
|
||||
assert len(why) == 1 and S.UNREAD[0] in d["표지"]
|
||||
assert all(not b["줄"] and S.UNREAD[0] in b["표지"] for b in d["일위"])
|
||||
assert not d.get("기계구성") and len(d["산근"]) == 65 # 산근 길 그대로
|
||||
assert {"CODE": "자식 코드", "BDCODE": "부모 코드", "DEVD1": "나누기"}[name] in why[0]
|
||||
|
||||
|
||||
def test_check_rules() -> None:
|
||||
ok = [{"BDCODE": "B00001", "CODE": c, "DEVD1": "F"} for c in ("M00001", "PR", "C1", "T2", "")]
|
||||
assert bq.check(ok, {"M00001"}) is None
|
||||
assert bq.check([{"BDCODE": "Q00001", "CODE": "", "DEVD1": ""}], set())
|
||||
assert bq.check([{"BDCODE": "B00001", "CODE": "M9", "DEVD1": ""}], set())
|
||||
assert bq.check([{"BDCODE": "B00001", "CODE": "", "DEVD1": "T"}], set())
|
||||
|
||||
|
||||
# ── ③ 일위 구성 줄 = 엑셀 · 다시 셈 ─────────────────────────────────────────
|
||||
def _meta(s: str) -> dict:
|
||||
return dict(t.split("|", 1) for t in re.split(r"_x000[0-9A-Fa-f]_`", s or "") if "|" in t)
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _xl(tag: str, sheet: str) -> list[list]:
|
||||
import openpyxl
|
||||
|
||||
path = golden.FILES[tag]
|
||||
if not path.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
|
||||
return [list(r) + [None] * 30 for r in wb[sheet].iter_rows(values_only=True)]
|
||||
|
||||
|
||||
def _xl_ilwi(tag: str) -> dict[str, dict]:
|
||||
"""엑셀 일위대가표 → {호표 코드: {머리 (이름, 규격), 줄 [(코드, 수량, 이름)]}}(숨은 N 열 메타)."""
|
||||
out: dict[str, dict] = {}
|
||||
cur = None
|
||||
for r in _xl(tag, "일위대가표"):
|
||||
n = r[13]
|
||||
if isinstance(n, str) and "BDCOD|" in n:
|
||||
cur = out.setdefault(_meta(n)["BDCOD"], {"머리": None, "줄": []})
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
if r[0] == "합 계":
|
||||
cur = None
|
||||
elif isinstance(n, str) and "COD|" in n:
|
||||
cur["줄"].append((_meta(n).get("COD", "").strip(), r[2], str(r[0] or "")))
|
||||
elif cur["머리"] is None:
|
||||
cur["머리"] = (str(r[0] or "").strip(), str(r[1] or "").strip())
|
||||
return out
|
||||
|
||||
|
||||
def _flat(v) -> str:
|
||||
return re.sub(r"\s+", "", str(v or ""))
|
||||
|
||||
|
||||
def _same_lines(xl: list, mine: list, res: dict) -> bool:
|
||||
if len(xl) != len(mine):
|
||||
return False
|
||||
for (code, qty, name), one in zip(xl, mine, strict=True):
|
||||
if code != one["코드"] and (
|
||||
code[:1] != one["코드"][:1] or _flat(name) != _flat(one["이름"])
|
||||
):
|
||||
return False
|
||||
if code not in ("PR", "RR") and qty is not None and abs(float(qty) - one["수량"]) > 1e-6:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def test_lines_equal_excel_ilwi() -> None:
|
||||
hops = lines = 0
|
||||
for xt, st in PAIR.items():
|
||||
d = parsed(st)
|
||||
mine = {b["코드"]: b for b in d["일위"]}
|
||||
by_name = {(_flat(b["이름"]), _flat(b["규격"])): b for b in d["일위"]}
|
||||
for code, x in _xl_ilwi(xt).items():
|
||||
b = mine.get(code)
|
||||
if b is None or (_flat(b["이름"]), _flat(b["규격"])) != tuple(map(_flat, x["머리"])):
|
||||
b = by_name[tuple(map(_flat, x["머리"]))]
|
||||
assert _same_lines(x["줄"], b["줄"], d["자원"]), (xt, code)
|
||||
hops += 1
|
||||
lines += len(x["줄"])
|
||||
assert (hops, lines) == (136, 706)
|
||||
|
||||
|
||||
def test_recompute_equals_saved() -> None:
|
||||
n_b = n_x = 0
|
||||
for tag in stc.FILES:
|
||||
d = parsed(tag)
|
||||
assert not any(b["표지"] for b in d["일위"]), tag # 일위 다시 셈 = 저장(B00038 꼴 포함)
|
||||
assert not [m for m in d["표지"] if il.MISMATCH in m], tag # 중기 다시 셈 = 저장
|
||||
n_b += len(d["일위"])
|
||||
n_x += sum(1 for c in d["자원"] if c[:1] == "X")
|
||||
assert (n_b, n_x) == (216, 176)
|
||||
b38 = next(b for b in parsed("소광변경1")["일위"] if b["코드"] == "B00038")
|
||||
assert [ln["코드"] for ln in b38["줄"]][-2:] == ["C1", "PR"] and not b38["표지"]
|
||||
|
||||
|
||||
def test_line_shape() -> None:
|
||||
b = next(b for b in parsed("봉화")["일위"] if b["코드"] == "B00013")
|
||||
rate = b["줄"][-1]
|
||||
assert rate["갈래"] == "율" and rate["율"] == {"밑": "L", "대상": "M", "곱": False}
|
||||
assert rate["이름"] == "노무비의 3%" and rate["금액"]["M"] == pytest.approx(409.3)
|
||||
assert all(ln["밑줄"] for ln in b["줄"][:2]) and b["줄합"]["T"] == b["저장"]["T"]
|
||||
kinds = {ln["갈래"] for t in stc.FILES for x in parsed(t)["일위"] for ln in x["줄"]}
|
||||
assert kinds <= {"M", "L", "S", "X", "B", "D", "W", "율", "닫기", "구분", "글"}
|
||||
apart = [ln for x in parsed("동부청영월")["일위"] for ln in x["줄"] if ln.get("별도계상")]
|
||||
assert len(apart) == 17 # 엑셀 비고 「별산…」 17 줄과 같음(분석 813 2 장)
|
||||
|
||||
|
||||
# ── ④ 중기 구성 ──────────────────────────────────────────────────────────
|
||||
def test_machines_equal_excel() -> None:
|
||||
n = m = 0
|
||||
for xt, st in PAIR.items():
|
||||
xl = mm._stm_sheet(_xl(xt, "중기사용료"))
|
||||
mine = parsed(st)["기계구성"]
|
||||
for a in {
|
||||
id(m): m for m in xl.values()
|
||||
}.values(): # 호표 하나 = 구성 하나(코드 · 이름 열쇠 둘)
|
||||
b = mine.get(mm.key_of(a["이름"], a["규격"]))
|
||||
assert a == b, (xt, a["이름"])
|
||||
n += 1
|
||||
rows = _xl(xt, "중기사용료")
|
||||
heads = [rows[i + 1] for i, r in enumerate(rows) if re.fullmatch(r"제\d+호표", _flat(r[0]))]
|
||||
assert all(mm.key_of(h[0], h[1]) in mine for h in heads), xt # 엑셀 읽기기가 못 읽는 머리도
|
||||
m += len(heads)
|
||||
assert (n, m) == (100, 118) # 엑셀 읽기기 100(울진대흥 머리 코드 없음 9 만) · 엑셀 호표 118
|
||||
|
||||
|
||||
def test_my_machine_seed_and_peers() -> None:
|
||||
d = parsed("봉화")
|
||||
make = d["기계구성"]["X00018"]
|
||||
assert make["손료"] == [[2799.0, 875000.0, False]] and make["주연료"] == 1.3
|
||||
assert make["잡재료율"] == 20.0
|
||||
line = {"자원": {"이름": make["이름"], "규격": make["규격"], "갈래": "기계"}}
|
||||
body = mm.body(make, 0, line, "봉화.STC")
|
||||
assert body and body["이름"] == make["이름"]
|
||||
saved = fn.read("봉화.STC", stc.raw("봉화"))
|
||||
assert saved["기계구성"]["X00018"] == make # 둘 꼴(올림 저장)에 감
|
||||
assert any(p.get("같은구성") for p in saved["단가"].values())
|
||||
|
||||
|
||||
# ── ⑤ 낙찰률 줄 2 칸 · 원설계 그대로 ─────────────────────────────────────────
|
||||
def test_bid_rate_rows_slot_two() -> None:
|
||||
d = parsed("영덕계류")
|
||||
assert d["자원"]["D01416"]["저장"]["T"] == 2239 # 산근 · 자원 = 1 칸(엑셀 단가산출근거와 같음)
|
||||
assert d["자원"]["D01416"]["원단가"]["T"] == 2547 # 2 칸 = 원단가(2239 = 2547 × 88 %)
|
||||
b = next(b for b in d["일위"] if b["코드"] == "B01092")
|
||||
assert b["저장"]["T"] == 62804 == b["줄합"]["T"] # 일위 머리 = 2 칸(엑셀 일위대가표 62,804)
|
||||
for tag in ("봉화", "동부청영월", "울진대흥", "소광22"):
|
||||
z, names = S._zip(stc.raw(tag))
|
||||
rows = S.dbf(z.read(names["COSTN.DBF"]))
|
||||
assert all(S._f(r.get("NAGCD")) == 0 for r in rows)
|
||||
res = parsed(tag)["자원"]
|
||||
assert all(res[r["CODE"]]["저장"]["T"] == S._f(r["TAMT1"]) for r in rows if r["CODE"])
|
||||
assert not any("원단가" in v for v in res.values())
|
||||
|
||||
|
||||
# ── ⑥ 일위대가 길 ─────────────────────────────────────────────────────────
|
||||
def test_hopyo_route(world) -> None:
|
||||
folder = world["personal"]
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
tag = "동부청영월"
|
||||
with store.at(folder):
|
||||
row = fh.upload_native(folder, stc.FILES[tag].name, stc.raw(tag), {"이름": "sub3임시"})
|
||||
assert row["호표수"] == 23
|
||||
grid = fn.grid(fn.read(stc.FILES[tag].name, stc.raw(tag)))
|
||||
assert grid["보기"]["일위대가표"] == "s2" and grid["책"]["시트"][0]["이름"] == "일위대가표"
|
||||
from M02_MasterTemplete import M02_Hopyo_View as hv
|
||||
|
||||
got = hv.listing(folder, row["키"], folder)
|
||||
assert len(got["호표"]) == 23 and all(h["맞춤"][1] for h in got["호표"])
|
||||
no = next(h["번"] for h in got["호표"] if h["번"] == 1984) # 측구보호용잡석채움 — 단산 둘
|
||||
made = hb.build_all(folder, folder, row["키"], None, True, no)
|
||||
assert [x["번"] for x in made["만듦"]] == [no] and made["같음"] == 1, made
|
||||
assert len(made["단가산출"]) == 2
|
||||
kinds = mp.census(folder, row["키"])
|
||||
assert any(g.get("호표") for g in kinds) # 구성 줄 재료가 판에 옴
|
||||
Reference in New Issue
Block a user