"""경쟁사 원파일(.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 = 코드 숫자). 올리기 = 파일 하나(814-1 · 요구 742-1 — 같은 공사 엑셀 붙이기 걷음) · 원파일 = 단가산출표 판(`grid` · 산출근거 머리) + 산출근거 · 일위대가 안 만듦(요구 743-2 · `hopyo` = 단산만). """ 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} if ln["자원"].get("같은구성"): # 816-2 같은 파일에서 구성이 같은 단독 기계 one["자원"]["같은구성"] = ln["자원"]["같은구성"] 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}} out["원본"] |= ( {"규격": blk["규격"]} if blk.get("규격") else {} ) # 연결 찾기 근거 · 이름 꼬리(814-1) 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 # ── 단가산출표 판(814-1 · 요구 742-2 · 743-2) ─────────────────────────── LIST = "단가산출표" _LIST_HEAD = ("코드", "이름", "규격", "단위", "재료비", "노무비", "경비", "합계") _LIST_COST = ("M", "L", "S", "T") def hopyo(saved: dict) -> dict[str, Any]: """원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리)만 · 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀).""" out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}} out["단산"] = [ {"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))} for n, b in numbers(saved).items() ] return out def grid(saved: dict) -> dict[str, Any]: """원파일 → 단가산출표 한 시트 격자(`M02_Hopyo_Grid.read` 꼴) — 산출근거 머리(코드 · 이름 · 규격 · 단위 · 저장 금액) 한 줄씩 · 행 = 1 머리 + 블록 차례.""" cells: dict[str, dict] = {} cols = "ABCDEFGH" for c, head in zip(cols, _LIST_HEAD): cells[f"{c}1"] = {"값": head} rows = [] for at, (n, b) in enumerate(numbers(saved).items(), 2): cost = b.get("저장") or {} vals = [b.get("코드"), b.get("이름"), b.get("규격"), b.get("단위")] vals += [_dec(cost.get(k)) for k in _LIST_COST] for c, v in zip(cols, vals): if v not in (None, ""): cells[f"{c}{at}"] = {"값": v if isinstance(v, (int, float)) else str(v)} rows.append({"번": n, "이름": b.get("이름"), "행": [at, at]}) 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 { "책": book, "호표": [], "단산": rows, "보기": {LIST: "s1"}, "원본": {LIST: LIST}, "목록": {"호표": [], "단산": [{"번": r["번"], "행": r["행"][0]} for r in rows]}, "줄": [], } 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()}