"""M02 가져온 식 수식 문서 다듬기(PLAN 809-1 · 요구 704) — `M02_Doc_Import.derive` 끝에 부름 · 금액 그대로. ① 소계 · 합계 성격 줄(덧줄 · 단위 「식」 · 수량 없음 · 단가 = 다른 줄 기호 합) 이름 = 엑셀 원 글 머리 (`가져온.원본.글줄` 자리 = 그 줄 이름 · 「:」 앞 글 · 글이 수식뿐이면 바로 위 설명 글 · 「합 계」 절 아래 비목 = 「노무비 합계」 꼴) — 「줄_2 · 노무비_2 · 계_2」 같은 지은 이름 걷음 ② EST 기계 세 줄(같은 자원 · 같은 기계 이음 · 같은 수량 · 노무비 · 재료비 · 경비 하나씩) = 한 기계 줄 (이름 = 자원 이름 · 비목 묶음 · 단가 = 기계 이음 칸 없음 · 엑셀 = 값묶음) · 세 줄 기호를 부르던 식 = 그 줄 비목 칸(`C_크레인.노무비`) — 셈 값 그대로(줄 끝수 = 비목마다 같은 규칙) 엑셀 원 글 · 엔진 줄은 안 고침(문서 겉꼴만) · 못 맞추면 그대로 둠. """ from __future__ import annotations import re from decimal import Decimal from typing import Any COSTS = ("노무비", "재료비", "경비") _MADE = re.compile(r"^(?:줄|노무비|재료비|경비|계|소계|합계)?_\d+$") _SECTION = re.compile(r"^\s*\d+\s*\.\s*(.+?)\s*$") def _flat(text: Any) -> str: return re.sub(r"\s+", "", str(text or "")) def _head(text: str) -> str: """「노무비 : 13,944.5 + …」 → 「노무비」 · 머리 없음 = 빈 글.""" head = text.split(":", 1)[0] if ":" in text else "" return _flat(head) def names(doc: dict, lines: list[dict]) -> None: """① 지은 이름(「줄_2 · 노무비_2 · 계_2」) → 엑셀 원 글 머리.""" section, note, by = "", "", {} for one in lines: text = str(one.get("글") or "") if hit := _SECTION.match(text): section, note = _flat(hit.group(1)), "" continue spot = one.get("자리") if not spot: note = text.strip() continue head = _head(text) or note if head and section in ("합계", "계") and head in COSTS: head = f"{head} 합계" by[str(spot)] = head note = "" taken = {str(x.get("이름")) for x in doc.get("금액") or []} for line in doc.get("금액") or []: name = str(line.get("이름") or "") new = by.get(name) if _MADE.match(name) and new and new not in taken: # 단계 · 다른 줄 이름과 안 겹칠 때만 line["이름"] = new taken.add(new) _rename_steps(doc, {name: new}) def _rename_steps(doc: dict, swap: dict[str, str]) -> None: """단계 줄 목록(이름으로 묶음) — 바뀐 이름 따라감 · 합친 줄 = 한 번.""" for st in doc.get("단계") or []: got = [] for n in st.get("줄") or []: n = swap.get(n, n) if n not in got: got.append(n) st["줄"] = got def _bundle_of(doc: dict, at: int) -> list[int] | None: """at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비) — 같은 기계 이음 · 같은 수량.""" money = doc.get("금액") or [] three = money[at : at + 3] if len(three) < 3 or [x.get("비목") for x in three] != list(COSTS): return None vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []} links = [(vars_.get(str(x.get("단가"))) or {}).get("연결") or {} for x in three] if any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links): return None same = {(x.get("키"), str(x.get("입력"))) for x in links} if len(same) != 1 or [x.get("칸") for x in links] != list(COSTS): return None for k in ("수량", "규격", "자리", "합밖"): if len({str(x.get(k) or "") for x in three}) != 1: return None names = {str(x.get("이름")) for x in three} for st in doc.get("단계") or []: # 단계가 셋 중 일부만 담으면 못 합침 if 0 < len(names & set(st.get("줄") or [])) < 3: return None return [at, at + 1, at + 2] def _place(doc: dict, name: str, used: set[str]) -> str: """기계 줄 번지 — 자원 이름 첫 낱말(괄호 · 규격 · 기호 뺌) · 겹치면 숫자.""" word = re.sub(r"[^\w]|_", "", re.split(r"[\s(]", name.strip())[0]) or "기계" if word[0].isdigit(): word = f"기계{word}" have = used | {str(x.get("번지")) for x in doc.get("금액") or [] if x.get("번지")} got, n = word, 1 while got in have: n += 1 got = f"{word}{n}" used.add(got) return got def _rewrite(text: Any, swap: dict[str, str]) -> Any: if not isinstance(text, str) or not swap: return text keys = "|".join(re.escape(k) for k in sorted(swap, key=len, reverse=True)) pat = re.compile(rf"(? None: """② EST 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름}.""" money = doc.get("금액") or [] vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []} out, swap, at, used = [], {}, 0, set() while at < len(money): idx = _bundle_of(doc, at) if idx is None: out.append(money[at]) at += 1 continue three = [money[i] for i in idx] first = vars_[str(three[0]["단가"])] link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")} excel = { c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0)) for c, x in zip(COSTS, three) } link["엑셀"] = excel name = title.get(str(three[0].get("이름"))) or str(three[0].get("이름")) first["연결"], first["이름"] = link, f"{name} 단가" if name in {str(x.get("이름")) for x in money} - {str(x.get("이름")) for x in three}: name = f"{name} ({at + 1})" # 같은 이름 줄이 있으면 겹치지 않게 line = {k: v for k, v in three[0].items() if k != "기호"} | {"이름": name, "비목": "묶음"} _rename_steps(doc, {str(x.get("이름")): name for x in three}) if any(x.get("기호") for x in three): # 부르던 식 = 번지 칸(엔진 `번지.X.노무비`) place = _place(doc, name, used) line["번지"] = place for c, x in zip(COSTS, three): if x.get("기호"): swap[str(x["기호"])] = f"번지.{place}.{c}" doc["변수"] = [ v for v in doc["변수"] if v is first or v.get("열쇠") not in {str(x["단가"]) for x in three[1:]} ] out.append(line) at += 3 doc["금액"] = out for line in out: for k in ("단가", "수량"): line[k] = _rewrite(line.get(k), swap) for one in doc.get("식") or []: one["식"] = _rewrite(one.get("식"), swap) def shape(doc: dict, meta: dict) -> dict: """derive 문서 다듬기 — 원본 글줄 · 엔진 줄 자원 이름.""" from M02_MasterTemplete import M02_Doc_NativeLayout as lay src = meta.get("원본") or {} lay.tag(doc, meta) # 812-3 원파일 차림 — 금액 줄 ↔ 로직 줄(끝에 `apply`) names(doc, list(src.get("글줄") or [])) title = {} for item in (meta.get("로직") or {}).get("호표") or []: res = item.get("자원") or {} if res.get("갈래") == "기계" and res.get("이름"): title[str(item.get("이름"))] = " ".join(x for x in (res["이름"], res.get("규격")) if x) machines(doc, title) lay.apply(doc, meta) return doc