- M02_San_Xlsx(공통 · 글자 칸 배치 · 금액 칸 · 자원 표) · M02_San_Xlsx_Est(EST 글 → 표지: 변수 = 가장 늦은 대입 값 하나 · 칸 나눔 꼴은 닫는 표지 2 칸 · 합계 줄 · 세 줄 머리 · 노임 · 자재 단가 짝 · 소계 61 · 율 58 · 62 · 구간 글자) · M02_San_Xlsx_Stmate(숨은 G 열 원문 = stc_line · 끝전 파일 투표) - M02_Doc_FromNative: 엑셀 둘 꼴(번 · 시트 행 · 원본 엑셀 참 · 원파일 거짓) · M02_Doc_Import: 엑셀 길도 소계 비목 칸 비움 · 추정 칸(출처 = 엑셀 꼬리표) - M02_Sangeun_FromHopyo: 산근 블록 = 새 읽기기(옛 San_Est · Emit.est 부름 걷음 · 옛 저장은 한 번 지어 둠) - test_812_1: 실제 짝(거창 · STC 다섯) 같음 · 합성 배치 = 거창 실제 · 합성 EST 1,849 · STC 371 · 금액 실무 11(엇갈림 고정 목록) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
11 KiB
Python
247 lines
11 KiB
Python
"""경쟁사 원파일(.estx · .stc) → 가져온 식(PLAN 810-2 · 요구 705 · 706 · 서버 단독 · 원파일 우선).
|
|
|
|
읽기 = 810-1 파서(`M02_San_Estx` · `M02_San_Stc`) → 둘 꼴(`pack`)을 실무자료 산근 자리
|
|
(`M02_Sangeun_FromHopyo` · `실무자료/산근/<HP>.json` 칸 `원파일`)에 둠(원본 파일은 안 둠) →
|
|
블록 하나 = `{로직, 원본}`(`emit` · 793-2a 꼴 → 가져온 식 SK → `M02_Doc_Import.derive` 수식 문서):
|
|
EST = `M02_Doc_NativeEst`(구조 줄 표지로 정함) ·
|
|
STmate = 원문 그대로 `M02_San_Emit.stmate`(원파일 원문 = 엑셀 숨은 G 열 · 단가 = COSTN ·
|
|
끝전 = 옵션 SAN_TYP 1 비목버림 / 0 합버림_큰나머지 · 810-1 저장값 대조로 확인).
|
|
블록 번 = 엑셀 길과 같음(EST = 산근 표 차례 1부터 = 「단산N」 · STmate = 코드 숫자).
|
|
같은 공사 = 엑셀 산근 블록이 모두 원파일 블록과 금액 · 이름이 같음(`same`) — 둘 다 있으면 원파일.
|
|
일위대가(EST HeadMainDat) → 실무자료 호표 꼴(`hopyo`) · STmate 일위대가 구성은 못 읽음(BDQTY 난독화) →
|
|
호표 없음(엑셀을 같이 올리면 엑셀 호표 + 원파일 산근).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_Doc_NativeEst as ne
|
|
from M02_MasterTemplete import M02_San_Emit as em
|
|
|
|
EXT = (".estx", ".stc")
|
|
RULE = {"0": "합버림_큰나머지"} # STmate SAN_TYP — 그 밖 = 비목버림
|
|
_EST_KEYS = ("행", "글", "보임", "품명", "단", "색인", "연결", "비목", "자원", "연결단가", "변수")
|
|
_EST_KEYS += ("줄참조", "결과", "합계", "저장")
|
|
_RES_KEYS = ("코드", "종류", "이름", "규격", "단위")
|
|
|
|
|
|
class NativeError(ValueError):
|
|
"""못 읽는 원파일."""
|
|
|
|
|
|
def is_native(name: str) -> bool:
|
|
return str(name or "").lower().endswith(EXT)
|
|
|
|
|
|
def read(name: str, data: bytes) -> dict[str, Any]:
|
|
"""원파일 → 둘 꼴(`pack`) · 못 읽음 = NativeError."""
|
|
from M02_MasterTemplete import M02_San_Estx as estx
|
|
from M02_MasterTemplete import M02_San_Stc as stc
|
|
|
|
try:
|
|
got = estx.read(data) if name.lower().endswith(".estx") else stc.read(data)
|
|
except Exception as e: # noqa: BLE001 — 파서 오류 = 사유 글
|
|
raise NativeError(f"원파일을 못 읽음 — {type(e).__name__}: {str(e)[:80]}") from e
|
|
return pack(got, name)
|
|
|
|
|
|
def pack(got: dict, name: str) -> dict[str, Any]:
|
|
"""파서 답 → 둘 꼴(셈 · 변환에 쓰는 칸만 · JSON)."""
|
|
out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []}
|
|
out |= {"엑셀": True} if got.get("엑셀") else {} # 812-1 엑셀 읽기기(`M02_San_Xlsx`) — 같은 꼴
|
|
head = ("코드", "번", "이름", "규격", "단위", "저장")
|
|
if got.get("엑셀"): # 엑셀 읽기기 — 줄 행 = 시트 행(원문 카드 · 옛 엑셀 길과 같은 자리)
|
|
got = {**got, "산근": [_rows(b) for b in got["산근"]]}
|
|
if got["꼴"] == "EST":
|
|
out["산근"] = [
|
|
{**{k: b.get(k) for k in head if k in b}, "줄": [_est_line(ln) for ln in b["줄"]]}
|
|
for b in got["산근"]
|
|
]
|
|
out["일위"] = [
|
|
{**{k: b.get(k) for k in head if k in b}, "줄": b["줄"]} for b in got["일위"]
|
|
]
|
|
return out
|
|
out["산근"] = [
|
|
{
|
|
**{k: b.get(k) for k in head if k in b},
|
|
"줄": [{"행": ln["행"], "글": ln["글"]} for ln in b["줄"]],
|
|
}
|
|
for b in got["산근"]
|
|
]
|
|
out["일위"] = [{k: b.get(k) for k in head 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 ("이름", "규격", "단위")}}
|
|
for code, p in (got.get("단가") or {}).items()
|
|
}
|
|
out["끝전"] = RULE.get(str((got.get("옵션") or {}).get("SAN_TYP")), "비목버림")
|
|
return out
|
|
|
|
|
|
def _rows(b: dict) -> dict:
|
|
return {**b, "줄": [{**ln, "행": ln.get("엑셀행") or ln["행"]} for ln in b["줄"]]}
|
|
|
|
|
|
def _est_line(ln: dict) -> dict:
|
|
one = {k: ln.get(k) for k in _EST_KEYS if ln.get(k) not in (None, [], False, "")}
|
|
if one.get("자원"):
|
|
one["자원"] = {k: ln["자원"].get(k) for k in _RES_KEYS}
|
|
return one
|
|
|
|
|
|
# ── 블록 ──────────────────────────────────────────────────────────────
|
|
def numbers(saved: dict) -> dict[int, dict]:
|
|
"""블록 번 → 산근 블록(엑셀 길 번과 같음 · 엑셀 읽기기 EST = 시트 「N.」 번)."""
|
|
if saved["꼴"] == "EST":
|
|
return {int(b.get("번") or i): b for i, b in enumerate(saved["산근"], 1)}
|
|
return {int(str(b["코드"])[1:]): b for b in saved["산근"] if str(b["코드"])[1:].isdigit()}
|
|
|
|
|
|
def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]:
|
|
"""원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError."""
|
|
blk = numbers(saved)[int(no)]
|
|
if saved["꼴"] == "EST":
|
|
names = {b["코드"]: n for n, b in numbers(saved).items()}
|
|
out = ne.block(blk, int(no), keys, saved["파일"], names)
|
|
out["원본"] |= {"원코드": blk["코드"]} | ({"규격": blk["규격"]} if blk.get("규격") else {})
|
|
return _excel(saved, out)
|
|
from M02_MasterTemplete import M02_San_Stmate as stm
|
|
|
|
nodes = [{"행": ln["행"], "글": ln["글"], **stm.parse_line(ln["글"])} for ln in blk["줄"]]
|
|
saved_amt = blk.get("저장") or {}
|
|
head = {"C": saved_amt.get("T"), "D": saved_amt.get("L"), "E": saved_amt.get("M")}
|
|
head["F"] = saved_amt.get("S")
|
|
block = {"코드": blk["코드"], "이름": blk.get("이름"), "몸": [], "꼬리": [], "머리": head}
|
|
out = em.stmate(block, nodes, saved["단가"], saved["끝전"], keys, saved["파일"])
|
|
marks = sum(1 for one in out["원본"]["글줄"] if one.get("표지"))
|
|
sure = len(out["로직"]["호표"]) + len(out["로직"].get("덧줄") or []) + len(out["로직"]["중간"])
|
|
out["원본"] |= {"원파일": True, "갈래": "STmate", "확정": {"확정": sure, "표지": marks}}
|
|
return _excel(saved, out)
|
|
|
|
|
|
def _excel(saved: dict, out: dict) -> dict[str, Any]:
|
|
"""엑셀 읽기기 꼴(812-1) — 원본 `원파일` 거짓 · `엑셀` 참(화면 꼬리표 「엑셀」 · 원코드 = 단산 번)."""
|
|
if saved.get("엑셀"):
|
|
out["원본"] |= {"원파일": False, "엑셀": True}
|
|
out["원본"] |= {"시트": "단가산출"} if saved["꼴"] == "EST" else {}
|
|
out["원본"].pop("원코드", None)
|
|
return out
|
|
|
|
|
|
def inner(saved: dict, logic: dict) -> list[tuple[dict, int | None, str | None]]:
|
|
"""안쪽 산근 줄 `[(호표 줄, 같은 원파일 블록 번, 못 잇는 까닭)]`(`FromHopyo._inner` 짝)."""
|
|
have = numbers(saved)
|
|
by_code = {str(b.get("코드")): n for n, b in have.items()}
|
|
out = []
|
|
for line in logic["호표"]:
|
|
res = line.get("자원") or {}
|
|
if res.get("갈래") != "단가산출":
|
|
continue
|
|
n = by_code.get(str(res.get("코드")))
|
|
if n is None:
|
|
out.append((line, None, "같은 원파일에 안쪽 블록 없음"))
|
|
elif res.get("옮김"):
|
|
out.append((line, n, "비목이 바뀐 부름"))
|
|
else:
|
|
out.append((line, n, None))
|
|
return out
|
|
|
|
|
|
# ── 같은 공사 · 일위대가 ───────────────────────────────────────────────
|
|
def same(saved: dict, sheets: dict) -> bool:
|
|
"""엑셀 산근 블록이 모두 원파일 같은 번 블록과 계 금액(1 원 안)이 같음 = 같은 공사."""
|
|
from M02_MasterTemplete import M02_San_Book as book
|
|
|
|
have = numbers(saved)
|
|
kind = book.kind_of(sheets)
|
|
if not kind or (kind == "STmate") != (saved["꼴"] == "STmate"):
|
|
return False
|
|
if kind == "STmate":
|
|
excel = {int(b["코드"][1:]): b["머리"].get("C") for b in book.stmate_blocks(sheets)}
|
|
else:
|
|
excel = {int(n): t.get("T") for n, t in book.est_prices(sheets)[1].items()}
|
|
if not excel:
|
|
return False
|
|
for n, total in excel.items():
|
|
blk = have.get(int(n))
|
|
if blk is None or abs(float(total or 0) - float((blk.get("저장") or {}).get("T") or 0)) > 1:
|
|
return False
|
|
return True
|
|
|
|
|
|
GALLAE = {6: "노임", 5: "자재", 105: "기계", 104: "단가산출", 103: "호표", 21: "기타", 101: "기타"}
|
|
|
|
|
|
def hopyo(saved: dict) -> dict[str, Any]:
|
|
"""EST 일위대가(HeadMainDat) → 실무자료 읽기 꼴(`M02_Hopyo_Excel.read_book` 짝 · 호표 · 단산) ·
|
|
STmate = 호표 없음(구성 못 읽음)."""
|
|
blocks = numbers(saved)
|
|
names = {str(b.get("코드")): n for n, b in blocks.items()}
|
|
heads = {str(h.get("코드")): n for n, h in enumerate(saved.get("일위") or [], 1)}
|
|
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
|
|
out["단산"] = [
|
|
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
|
|
for n, b in blocks.items()
|
|
]
|
|
if saved["꼴"] != "EST":
|
|
return out
|
|
for n, h in enumerate(saved.get("일위") or [], 1):
|
|
lines = []
|
|
for k, ln in enumerate(h.get("줄") or []):
|
|
link = ln.get("연결")
|
|
code = str((ln.get("자원") or {}).get("코드") or "")
|
|
ref = None
|
|
if link == 104 and code in names:
|
|
ref = {"종류": "단산", "번": names[code]}
|
|
elif link == 103 and code in heads:
|
|
ref = {"종류": "일위", "번": heads[code]}
|
|
kind = "%" if ln.get("연결") in (52, 58, 62) else GALLAE.get(link or 0, "기타")
|
|
if kind == "자재" and str(ln.get("단위") or "") == "인":
|
|
kind = "노임"
|
|
if not any((ln.get("단가") or {}).values()) and kind not in ("%",):
|
|
kind = "별도계상" if not ref else kind
|
|
lines.append(
|
|
{
|
|
"차례": k,
|
|
"이름": ln.get("품명") or "",
|
|
"규격": ln.get("규격") or "",
|
|
"수량": _dec(ln.get("수량")),
|
|
"단위": ln.get("단위") or "",
|
|
"단가": _cost(ln.get("단가")),
|
|
"금액": _cost(ln.get("저장")),
|
|
"비고": "",
|
|
"참조": ref,
|
|
"갈래": kind,
|
|
"행": ln.get("행"),
|
|
"코드": code or None,
|
|
}
|
|
)
|
|
out["호표"].append(
|
|
{
|
|
"번": n,
|
|
"이름": h.get("이름"),
|
|
"규격": h.get("규격") or "",
|
|
"단위": h.get("단위") or "",
|
|
"금액": _cost(h.get("저장")),
|
|
"행": [n, n],
|
|
"줄": lines,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def _dec(v: Any) -> int | float | None:
|
|
"""수 → 엑셀 읽기와 같은 꼴(정수 = int · 아니면 float · 없음 = None)."""
|
|
if v is None:
|
|
return None
|
|
d = Decimal(str(v))
|
|
return int(d) if d == d.to_integral_value() else float(d)
|
|
|
|
|
|
def _cost(d: dict | None) -> dict[str, Any]:
|
|
d = d or {}
|
|
out = {"계": _dec(d.get("T"))}
|
|
return out | {name: _dec(d.get(c)) for c, name in em.COMP.items()}
|