"""M02 가져온 식(엑셀 불러오기) 단가산출 = 수식 문서(PLAN 802-4b-1 · 요구 664 · 665) — 서버 단독. 경쟁사 산근 블록도 같은 꼴(식 + 값)로 들어옴 — 엑셀 단가 그대로 엔진 줄(`가져온.로직` + SK 고침)을 `master_doc_convert` 로 문서로 옮기고(손 값 = 숫자 연결 · 중간 = 식 줄 · 덧줄 = 금액 줄 식 · 끝수 · 단계 · 조정 · 합밖 · 자리 그대로) 이은 것을 연결로 얹음 · 연결마다 엑셀 값을 기억(`엑셀` 칸): 인력 · 재료 · 유가 · 전력 · 환율 이음 = 요소 {키} · 기계 이음(EQ) = 기계경비 단가산출 {키 GC, 입력 기계 …, 칸 = 그 줄 비목}(시간당 자료 없음 = 숫자 그대로 · 설명) · 로직 이음 「로직(GC…)」 = 단가산출 {키, 입력} · 산근 속 산근(797-1 참조) = 단가산출 {키 형제 SK, 참조 {층, 주인}, 칸?} · 소요량 · 계수 고른 것(797-2 `가져온.연결.고름`) = 그 수 자리에 표 연결 기호(pct · kgt = ÷ 배). 문서 머리 `가져온` = {파일, 시트, 행, 갈래, 코드?, 표지(797-1a · 못 읽음 까닭 그대로), 알림?}. 셈(`run`) — 단가기준 엑셀(기본) = 엑셀 값 기억 연결을 숫자로 보고 셈 · 우리 = 연결 값으로 셈 · 형제 SK 는 늘 연결(부르는 쪽 단가기준으로 · 안쪽이 바뀌면 따라감). """ from __future__ import annotations import copy as cp from decimal import Decimal from typing import Any from M02_MasterTemplete import M02_Doc_Copy as dc from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Sangeun_Doc as sd from M02_MasterTemplete import M02_Sangeun_Imported as imp XL = "엑셀" # 연결 칸 — 엑셀 값 기억 HEAD = ("파일", "시트", "행", "갈래", "코드") PICK = "소요량" # 고른 표 칸 기호 이름 머리 _SCALE = {"eq": (1,), "pct": (100, 0.01), "kgt": (1000, 0.001)} def is_imported(sk: Any) -> bool: return imp.is_imported(sk) def basis_of(sk: dict) -> str: return "우리" if (sk.get(imp.KEY) or {}).get("단가기준") == "우리" else "엑셀" # ── 엑셀 단가 그대로 엔진 줄(옛 셈 길 · SK 고침 얹은 것) ─────────────────── def _ctx(sk: dict, whole: Any) -> tuple: """엑셀 단가 기준 고친 줄 — 옛 셈 길(`OLD`)로 돌려 엔진에 넣은 (색인, 키, 입력, 답, 재료).""" from M02_MasterTemplete import M02_Doc_Sk as dsk from M02_MasterTemplete import M02_Sangeun_Calc as calc raw = {**sk, imp.KEY: {**sk[imp.KEY], "단가기준": "엑셀"}} box: list = [] token = dsk.OLD.set(True) try: calc.run(sd.clean(raw, None, whole), whole, catch=box) finally: dsk.OLD.reset(token) return box[-1] # ── 소요량 · 계수 고른 칸 → 수 자리에 기호 ────────────────────────────── def _swap_nth(node: Any, n: int, new: tuple, seen: list) -> Any: """`M02_Sangeun_QtyLinks._literals` 차례의 n 번째 수 → `new` 마디(같은 걸음 규칙).""" mf = dc.cv.mf if not isinstance(node, tuple): return node kind = node[0] if kind == "num": seen[0] += 1 return new if seen[0] - 1 == n else node if kind == "bin": cmp_ = node[1] in mf._OPS sides = [ side if cmp_ and isinstance(side, tuple) and side[0] == "num" else _swap_nth(side, n, new, seen) for side in node[2:] ] return (kind, node[1], *sides) if kind in ("neg", "field"): return (kind, _swap_nth(node[1], n, new, seen), *node[2:]) if kind == "call": return ("call", node[1], [_swap_nth(a, n, new, seen) for a in node[2]]) return node def _conds(table: dict, at: int) -> dict | None: """표 줄 차례 → 그 줄 하나만 맞는 조건 {조건: 값}(범위 = 위 끝 › 아래 끝) · 못 가림 = None.""" rows = table.get("줄") or [] if not (0 <= at < len(rows)): return None row, out = rows[at], {} for cond, how in (table.get("조건") or {}).items(): if cond not in row: continue have = row[cond] if how == "범위": low, high = have have = high if high is not None else low if have is None: return None out[cond] = have try: hit = dc.ml.find(table, out) except Exception: # noqa: BLE001 — 못 가림 = 수 그대로 return None return out if hit is row else None def _picks(row: dict, ids: list[str], base: list[str], meta: dict, master: Any) -> dict: """고른 칸(`가져온.연결.고름`) → 중간 줄(표 찾기 하나 · 연결 기호) · 수 자리에 그 기호 ÷ 배 · 돌려줌 = {중간 이름: 엑셀 칸 값}. 자리 줄이 SK 고침으로 바뀌었으면 건너뜀.""" mf, mee = dc.cv.mf, dc.cv.mee src = meta["로직"] picked = ((meta.get("연결") or {}).get("고름")) or [] out: dict[str, Any] = {} mids = row.setdefault("중간", []) taken = {str(x.get("이름")) for x in [*(row.get("입력") or []), *mids]} for one in picked: part, i, n = str(one.get("자리") or "::").split(":")[:3] field = "수량" if part == "호표" else "식" lines = src.get(part) or [] if not (i.isdigit() and n.isdigit() and int(i) < len(lines)): continue cid = base[int(i) + (len(src.get("호표") or []) if part == "덧줄" else 0)] if cid not in ids: continue at = ids.index(cid) mine = (row["호표"] if at < len(row["호표"]) else row.get("덧줄") or [])[ at if at < len(row["호표"]) else at - len(row["호표"]) ] if str(mine.get(field)) != str(lines[int(i)].get(field)): continue # SK 덮개가 그 식을 바꿈 — 수 자리 어긋남 try: table = master.get(str(one.get("표"))) conds = _conds(table, int(one.get("줄"))) node = mf.parse(str(mine[field])) except Exception: # noqa: BLE001 — 표 · 식 못 읽음 = 수 그대로 continue if conds is None: continue name = next(f"{PICK}{k}" for k in range(1, 999) if f"{PICK}{k}" not in taken) taken.add(name) scale = next( (m for m in _SCALE.get(str(one.get("꼴")), (1,)) if _near(one, m)), None ) # 엑셀 수 = 칸 값 ÷ 배 if scale is None: continue ref = ( ("name", name) if scale == 1 else ("bin", "/", ("name", name), ("num", Decimal(str(scale)))) ) mine[field] = mee.raw_text(_swap_nth(node, int(n), ref, [0])) args = ", ".join(f"{k}={_lit(v)}" for k, v in conds.items()) mids.append( {"이름": name, "식": f"찾기({one['표']}{', ' if args else ''}{args}).{_q(one['칸'])}"} ) out[name] = Decimal(str(one.get("값"))) return out def _near(one: dict, m: float) -> bool: cell, v = float(one.get("값") or 0), float(one.get("수") or 0) return abs(cell - v * m) <= 1e-9 * max(1.0, abs(cell)) def _lit(v: Any) -> str: if isinstance(v, (int, float, Decimal)) and not isinstance(v, bool): return dc.cv.mf._plain(Decimal(str(v))) return "'" + str(v).replace("'", "") + "'" def _q(name: Any) -> str: return dc.cv.mee._q(str(name)) # ── 이음 → 연결 ──────────────────────────────────────────────────────── def hourly_link(eq: str, part: str) -> dict | None: """기계(EQ) 시간당 그 비목 = 기계경비 단가산출 부름(`M02_Machine_Hourly` 와 같은 짝 고름) · 자료 없음 = None.""" from M02_MasterTemplete import M02_Machine_Hourly as machine if part not in imp.COSTS or not imp.hourly_of(eq).get("ok"): return None code = str(imp._whole().get(eq).get("원문번호") or "") fit = [r for r in machine._family() if code in [str(c) for c in machine._codes(r)]] if not fit: return None logic = min(fit, key=machine._rank) given = {"기계": _lit(code)} for spec in logic.get("입력") or []: if spec.get("이름") != "기계": given[spec["이름"]] = _lit((spec.get("고르기") or [""])[0]) return {"갈래": "단가산출", "키": str(logic["키"]), "입력": given, "칸": part} def _logic_link(text: str) -> dict | None: """「로직(GC…, 이름='값')」 → 단가산출 연결 · 인자가 수 · 글 아니면 None.""" try: node = dc.cv.mf.parse(text) except Exception: # noqa: BLE001 return None if node[0] != "logic" or any(v[0] not in ("num", "str") for _, v in node[2]): return None return {"갈래": "단가산출", "키": node[1], "입력": {k: _lit(v[1]) for k, v in node[2]}} def _link_of(item: dict) -> tuple[dict | None, str]: """가져온 줄 하나의 연결(엑셀 값 기억 없이) · 설명(못 이은 까닭).""" if item.get("참조"): ref = item["참조"] link = { "갈래": "단가산출", "키": ref["키"], "참조": {"층": ref["층"], "주인": ref.get("주인")}, } return link | ({"칸": item["참조비목"]} if item.get("참조비목") else {}), "" key = str(item.get("이음") or "") if not key: return None, "" if item.get("종류") == "로직": return _logic_link(key), "" if key.startswith("EQ"): got = hourly_link(key, str(item.get("비목") or "")) return got, "" if got else f"기계 {key} 시간당 경비 자료 없음 — 엑셀 단가 그대로" link = {"갈래": "요소", "키": key} if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음) link["단위"] = str(item.get("단위") or "") return link, "" def _excel(item: dict) -> Any: if item.get("값묶음"): return {k: Decimal(str(item["값묶음"].get(k) or 0)) for k in imp.COSTS} return Decimal(str(item["단가"])) if item.get("단가") is not None else None def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None: """금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로).""" by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []} for at, line in enumerate((doc.get("금액") or [])[: len(row["호표"])]): item = src.get(ids[at]) if at < len(ids) else None price = str(line.get("단가") or "") var = by_id.get(price.removesuffix(".계")) if item is None or var is None or (var.get("연결") or {}).get("갈래") != "숫자": continue excel = _excel(item) if not item.get("참조") and var["연결"].get("값") != excel: continue # SK 덮개 단가 = 사용자 값 link, note = _link_of(item) if note: var["설명"] = note if link is None: continue if item.get("참조비목") and isinstance(excel, dict): excel = excel[item["참조비목"]] line["단가"], line["비목"] = dc.md.var_id(var), item["참조비목"] var["연결"] = {**link, XL: excel} def _numbers(doc: dict, keep: dict) -> None: """수 하나뿐인 식 줄(EST 얼린 변수) → 숫자 연결 · 고른 칸 중간 → 엑셀 칸 값 기억.""" by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []} lines = [] for one in doc.get("식") or []: var = by_id.get(str(one.get("기호"))) try: node = dc.md.parse(one.get("식")) except dc.md.DocError: node = None if var is not None and node is not None and node[0] == "num": var["연결"] = {"갈래": "숫자", "값": node[1]} continue lines.append(one) doc["식"] = lines for var in doc.get("변수") or []: if var.get("열쇠") in keep and (var.get("연결") or {}).get("갈래") == "표": var["연결"][XL] = keep[var["열쇠"]] def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict: """가져온 식 SK → 수식 문서(키 = SK 키) — `ctx` = 엑셀 단가 기준 옛 셈 길이 엔진에 넣은 것.""" meta = sk[imp.KEY] if ctx is None: ctx = _ctx(sk, whole or imp._whole()) master, key, values, got, _ = ctx row = cp.deepcopy(master.logic(key)) ids = list(got.get("정체") or []) bare = imp.row_of({**sk, imp.KEY: {**meta, "단가기준": "엑셀"}}) or {} base = sd.line_ids(bare) src = dict(zip(base, meta["로직"].get("호표") or [])) keep = _picks(row, ids, base, meta, master) given = dc.given_of(master, key, values) doc, notes = dc.cv.convert(_Row(master, key, row), row, given) _attach(doc, row, ids, src) _numbers(doc, keep) head = {k: (meta.get("원본") or {}).get(k) for k in HEAD if (meta.get("원본") or {}).get(k)} marks = list((meta.get("원본") or {}).get("표지") or []) if not (doc.get("금액") or doc.get("결과") or row["호표"] or row.get("덧줄")): _empty(doc) # 빈 산근 = 엑셀 0 원(줄 없음 합) — 「—」 아님 marks.append(EMPTY) doc["가져온"] = head | {"표지": marks} | ({"알림": notes} if notes else {}) return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))} EMPTY = "참고 · 빈 산근 — 엑셀 줄 없음 · 금액 0 그대로" def _empty(doc: dict) -> None: """줄 없는 엑셀 산근 — 0 묶음 줄 하나(엑셀 금액 0 = 줄 없는 합 · 지은 값 아님).""" doc["변수"].append( {"열쇠": "P_빈", "기호": "P", "아래": "빈", "위": "", "이름": "빈 산근", "단위": "", "연결": {"갈래": "숫자", "값": dict.fromkeys(imp.COSTS, Decimal(0))}} ) # fmt: skip doc["금액"] = [{"이름": "빈 산근", "단위": "식", "비목": "묶음", "단가": "P_빈"}] class _Row: """변환 색인 — 이 SK 줄만 고친 줄(소요량 중간 더함)로 · 그 밖 그대로.""" def __init__(self, master: Any, key: str, row: dict) -> None: self._m, self._k, self._r = master, key, row def logic(self, ref: str) -> dict: return self._r if ref == self._k else self._m.logic(ref) def __getattr__(self, name: str) -> Any: return getattr(self._m, name) # ── 셈 ──────────────────────────────────────────────────────────────── def view(doc: dict, basis: str) -> dict: """단가기준 엑셀 = 엑셀 값 기억 연결(형제 SK 빼고)을 숫자로 · 우리 = 그대로 · TS 짝 = `common_util/common_util_formula_doc_import.ts` `importView`(거울 시험 test_802_4b1).""" if basis == "우리": return doc out = cp.deepcopy(doc) for var in out.get("변수") or []: link = var.get("연결") or {} if XL in link and not str(link.get("키") or "").startswith("SK-"): var["연결"] = {"갈래": "숫자", "값": link[XL]} return out class Links(dc.ml.Links): """M01 연결(지금 M01 · 수식 문서 폴더) + 형제 가져온 식 SK(부르는 쪽 단가기준).""" def __init__(self, master: Any, basis: str, trail: tuple = ()) -> None: super().__init__(master, dc.folder()) self.basis, self.trail, self.mine = basis, trail, {} def doc(self, key: str) -> dict | None: if not str(key).startswith("SK-"): return super().doc(key) return self.mine.get(key) def learn(self, doc: dict) -> None: """문서의 형제 SK 연결 → 그 SK 문서를 읽어 둠(깊이 · 순환 = 못 읽음 · 표지는 계산기).""" for var in doc.get("변수") or []: link = var.get("연결") or {} key, ref = str(link.get("키") or ""), link.get("참조") if not key.startswith("SK-") or key in self.mine or not isinstance(ref, dict): continue full = {**ref, "키": key} rid = refs.ref_id(full) if rid in self.trail or len(self.trail) >= refs.DEPTH: continue sib = refs.read_ref(full) if not is_imported(sib): continue self.mine[key] = None # 순환 막음 try: sub = view(of(sib), self.basis) except Exception: # noqa: BLE001 — 못 지음 = 없는 단가산출(연결 끊김 표지) continue self.mine[key] = sub self.trail = (*self.trail, rid) self.learn(sub) def of(sk: dict) -> dict: """SK 의 문서 — 수식 길로 고친 것(따로) = 저장본 · 아니면 지금 줄에서 지음.""" got = sk.get(dc.KEY) if dc.has(sk) else None if got and got.get("따로") and isinstance(got.get("문서"), dict): return got["문서"] return derive(sk) def record(doc: dict, sk: dict) -> dict[str, Any]: """수식 탭 화면 캐시(`M02_Doc_Screen.record` 짝) — 단가기준 보기 문서 셈이 쓴 연결 · 형제 SK 문서.""" import master_doc_record as mr # `M02_Doc_Copy` 가 scripts 를 길에 둔 뒤 basis = basis_of(sk) links = Links(imp._whole(), basis) mine = view(doc, basis) rec = mr.Rec(links) try: links.learn(mine) rec.prefetch(mine) dc.mc.calc(mine, rec) except Exception: # noqa: BLE001 — 셈 틀림 = 셈 답 표지 · 기록은 모은 데까지 pass return rec.out() def run(doc: dict, sk: dict, master: Any = None) -> dict: """가져온 식 문서 셈(SK 단가기준) — 계산기 = M01 과 같은 `master_doc_calc`.""" basis = basis_of(sk) links = Links(master or imp._whole(), basis) mine = view(doc, basis) try: links.learn(mine) return dc.mc.calc(mine, links) except Exception as e: # noqa: BLE001 — 계산기 예외 없음이 원칙 · 남은 틈도 「—」 return { "셈됨": False, "표지": [{"갈래": "셈 틀림", "글": str(e)}], **dict.fromkeys(dc.COSTS), }