"""경쟁사 원파일(.estx · .stc) → 가져온 식(PLAN 810-2 · 요구 705 · 706 · 서버 단독 · 원파일 우선). 읽기 = 810-1 파서(`M02_San_Estx` · `M02_San_Stc`) → 둘 꼴(`pack`)을 실무자료 산근 자리 (`M02_Sangeun_FromHopyo` · `실무자료/산근/.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( "엑셀" ): # 엑셀 읽기기 — 줄 행 = 시트 행(원문 카드 · 옛 엑셀 길과 같은 자리) · 블록 표지 head += ("표지", "몸", "꼬리") 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, blk) 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} block |= {"몸": blk.get("몸") or [], "꼬리": blk.get("꼬리") or []} # 엑셀 = 줄 금액 · 계 꼬리 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, blk) def _excel(saved: dict, out: dict, blk: dict) -> dict[str, Any]: """엑셀 읽기기 꼴(812-1) — 원본 `원파일` 거짓 · `엑셀` 참(화면 꼬리표 「엑셀」 · 원코드 = 단산 번).""" if saved.get("엑셀"): out["원본"] |= {"원파일": False, "엑셀": True} out["원본"] |= {"시트": "단가산출"} if saved["꼴"] == "EST" else {} out["원본"].pop("원코드", None) out["원본"]["표지"] += [m for m in blk.get("표지") or [] if m.startswith("참고 · ")] 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()}