"""M02 가져온 식 단가산출(SK 바탕 갈래 · PLAN 793-2 · 요구 649 · 652 갈래 C) — 서버 단독. 경쟁사 엑셀 산근 블록 = 엔진 로직 줄 하나(`_틀.md` 7 장 꼴 · sub4 793-2a 내보내기)를 SK 가 품음 · 셈 = 그 줄을 바탕 줄로(`M02_Sangeun_Doc.empty_row` → `row_of`) → `_Over` 로 얹어 같은 엔진 한 벌 · 790-2 · 792-1 고침(덮개 · 상수 · 표값 · 식 덮개 · 변수 줄 · 글 줄 · 까닭 · 더한줄)이 그대로 먹음. 가져온 로직 줄(만들기 몸 `로직`) = {이름, 결과단위 「원/단위」, 입력?, 중간? [{이름, 식, 표시?}], 호표 [줄], 덧줄? [{이름, 식, 비목, 번지?, 합밖?, 자리?}], 단계?, 끝전?, 조정?, 끝수?} 호표 줄 = {종류 인력|재료|기계|로직, 이름, 규격?, 단위, 수량(엔진 식 글), 비목(로직 줄은 없으면 노 · 재 · 경 나눠 얹음), 단가?(엑셀 단가 · 인력 · 재료 · 기계) | 값묶음? {노무비, 재료비, 경비}(엑셀 · 로직 줄), 이음?(M01 요소 키 LB · MT · EQ … | 로직 줄 = 「로직(GC…, 이름='값')」 글 | null), 번지?, 합밖?, 자리?} · `요소` 칸은 서버가 채움(이음 | {이름}) · 줄 정체 cid 머리 = 이음 키 | 이름. 원본(만들기 몸 `원본`) = {파일, 시트, 행 [처음, 끝], 갈래 STmate|EST, 엑셀금액 {노무비, 재료비, 경비, 계}(1 단위), 글줄 [{행, 글, 자리?(셈 줄 cid | mid:이름), 값?(그 줄 엑셀 금액)}]}. SK 문서 덧칸 `가져온` = {로직(엑셀 단가 그대로 · 고치지 않음), 단가기준 엑셀|우리, 원본, 검산 {같음, 까닭, 셈 {노..계}, 엑셀 {노..계}, 줄 [{자리, 행, 셈, 엑셀, 같음, 까닭?}]}, 근거후보 [{기초, 옵션, 품검산, 단 확정|근접|사람}](793-3 이 채움), 바꿈?(우리 기초로 바꾼 뒤 = 그때 로직 · 단가기준 · 전 · 후 · 차이)} · 바탕 = null · 가져온 식 SK = `가져온.로직` 있음 · 검산 = 저장 때 서버가 다시. 단가기준 엑셀 = 줄 단가 · 값묶음 그대로 / 우리 = 이음 있는 줄만 단가 · 값묶음 떼고 M01(B09 = 사본 단가) 지금 값 · 이음 없는 줄 = 엑셀 단가 그대로 + 표지(`이음없음`). 길(`M02_Sangeun_Router` · 저장 없음 = 화면 캐시 · 만들기만 층 쓰기): POST …/sangeun/imported 몸 {로직, 원본, 이름?, 단위?, 구분?, 상세구분?, 새로?, 씀?} → {이름(SK 키), 검산, 단가기준}(서버가 다시 셈해 엑셀과 맞댐 · 같은 내용 409 = 기존 길) POST …/sangeun/imported/price 몸 {문서, 기준} → {문서, 전, 후, 이음없음 [{자리, 이름}]} POST …/sangeun/imported/link 몸 {문서, 자리, 이음} → {문서}(이음 고름 · 덮개 열쇠 옮김) POST …/sangeun/imported/to-base 몸 {문서, 로직(M01 GC), 입력값, 환산?} → {문서, 전, 후, 차이, 필요?}(기초 결과단위 성격이 SK 단위와 달라 환산 계수가 없으면 후 = None · 필요 ["환산"] · 793-3b) 셈 답(`…/sangeun/calc`) = 옛 꼴 그대로 · `원래` = 엑셀 금액(「M01 원래」 자리) · 덧칸 `가져온` {단가기준, 파일, 시트, 행, 갈래, 엑셀금액, 글줄 [{행, 글, 자리, 엑셀, 지금, 같음}], 이음없음}. """ from __future__ import annotations import json import re from decimal import Decimal from typing import Any from M02_MasterTemplete import M02_Sangeun_Doc as sd KEY = "가져온" BASIS = ("엑셀", "우리") COSTS = ("노무비", "재료비", "경비") KINDS = ("인력", "재료", "기계", "로직") ROW = ("이름", "결과단위", "입력", "중간", "호표", "덧줄", "단계", "끝전", "조정", "끝수") LINE = ("종류", "이름", "규격", "단위", "수량", "비목", "단가", "값묶음", "이음", "번지", "합밖") LINE += ("자리", "출처", "비고", "자원") # 자원 = 경쟁사 코드 · 엑셀 단가 원본(793-2a · 셈 안 씀) LINE += ("참조", "참조비목") # 산근 속 산근 = 같은 엑셀의 형제 가져온 식 SK(797-1 · 로직 줄만) EXTRA = ("이름", "식", "비목", "번지", "합밖", "자리", "출처", "비고") META = ("원본", "검산", "근거후보", "바꿈", "연결") # 검산 = 저장 때 서버가 다시 적음(`stamp`) MANY = 300 # 호표 · 덧줄 · 중간 줄 수 BIG = 400_000 # 메타 글 크기(바이트) _KEYED = ("덮개", "고름", "자재표시", "상수덮개", "표값덮개", "식덮개", "까닭", "M01판") def _mf() -> Any: from M02_MasterTemplete import M02_Labor_Doc as labor return labor.m01.cm.mf def _whole() -> Any: from M01_MasterData import M01_MasterData_Store as m01 return m01.loaded()[1] def is_imported(doc: Any) -> bool: return isinstance(doc, dict) and isinstance((doc.get(KEY) or {}).get("로직"), dict) # ── 바탕 줄 ─────────────────────────────────────────────────────────── _HOURLY: dict[str, Any] = {"whole": None, "rows": {}} def hourly_of(key: str) -> dict: """이음 기계(EQ) 시간당 노 · 재 · 경 — M01 판이 바뀌면 다시(캐시 = M01 묶음 하나에 매임).""" from M02_MasterTemplete import M02_Machine_Hourly as machine whole = _whole() if _HOURLY["whole"] is not whole: _HOURLY.update(whole=whole, rows={}) if key not in _HOURLY["rows"]: _HOURLY["rows"][key] = machine.hourly(key) return _HOURLY["rows"][key] def machine_price(link: str, part: Any, excel: Any) -> Any: """[우리 단가로] 기계 이음 줄 값 — 시간당 경비의 그 비목 · 시간당 자료 없으면 엑셀 단가 그대로.""" got = hourly_of(link) if got.get("ok") and part in COSTS: return sd.plain(Decimal(str(got[part]))) return excel def row_of(doc: Any) -> dict | None: """가져온 식 바탕 줄(엔진 로직 줄) — 단가기준 우리 = 이음 있는 줄의 단가 · 값묶음 뗌 · 요소 = 이음 | {이름}(로직 줄 = 이음 | None) · 가져온 식 아님 = None.""" if not is_imported(doc): return None meta = doc[KEY] ours = meta.get("단가기준") == "우리" items = [] for one in meta["로직"].get("호표") or []: link = one.get("이음") item = {k: v for k, v in one.items() if k != "이음"} item["요소"] = link or (None if one.get("종류") == "로직" else {"이름": one.get("이름")}) if one.get( "참조" ): # 안쪽 산근 = 형제 SK 1 단위 값(셈이 채움 · 엑셀 값묶음은 얼린 사본으로 둠) item |= {"요소": None, "값묶음": None, "기준": "우리" if ours else "엑셀"} if ours and link: item.pop("단가", None) item.pop("값묶음", None) if str(link).startswith( "EQ" ): # EQ = 기계 제원 묶음 — 줄 값은 시간당 기계경비의 그 비목 item["단가"] = machine_price(link, one.get("비목"), one.get("단가")) items.append(item) row = {"입력": [], "중간": [], "덧줄": [], **meta["로직"]} return {**row, "키": doc.get("키"), "호표": items} def base_row(doc: dict) -> dict[str, Any]: """바탕 없는 SK 바탕 줄(`sd.empty_row`) — 가져온 식 = 그 줄 · 아니면 입력 · 줄 없음.""" return row_of(doc) or { "키": doc.get("키"), "이름": doc.get("이름"), "결과단위": f"원/{doc.get('단위') or ''}", **{k: [] for k in ("입력", "중간", "호표", "덧줄")}, } # ── 꼴 검사 ─────────────────────────────────────────────────────────── def _formula(raw: Any, where: str) -> str: from M02_MasterTemplete import M02_Sangeun_Formula as fx text = str(raw if raw is not None else "").strip() if not text: raise sd.DocError(f"{where} · 식이 빔") if bad := fx.limits(text): raise sd.DocError(f"{where} · {bad[0]}") try: _mf().parse(text) except Exception as e: # noqa: BLE001 — 엔진 FormulaError raise sd.DocError(f"{where} · {e}") from e return text def _link(raw: Any, kind: str, where: str, whole: Any) -> str | None: """이음 — 로직 줄 = 「로직(GC…)」 글(부르는 로직 있음) · 그 밖 = M01 요소 키(있음).""" if raw in (None, ""): return None text = str(raw).strip() try: if kind == "로직": node = _mf().parse(text) if node[0] != "logic": raise ValueError("로직(…) 꼴 아님") whole.logic(node[1]) else: whole.get(text) except Exception as e: # noqa: BLE001 raise sd.DocError(f"{where} · 없는 이음 「{text}」 ({e})") from e return text def _line(raw: Any, where: str, whole: Any) -> dict: if not isinstance(raw, dict): raise sd.DocError(f"{where} · 묶음 아님") kind = raw.get("종류") if kind not in KINDS: raise sd.DocError(f"{where} · 종류는 {' · '.join(KINDS)}") out = {k: raw[k] for k in LINE if raw.get(k) not in (None, "")} out["이름"] = str(raw.get("이름") or "").strip() or where out["단위"] = str(raw.get("단위") or "") out["수량"] = _formula(raw.get("수량"), f"{where} 수량") out["이음"] = _link(raw.get("이음"), kind, where, whole) if "참조" in out: ref = out["참조"] if kind != "로직" or not isinstance(ref, dict): raise sd.DocError(f"{where} · 참조는 로직 줄의 {{층, 주인, 키}}") if ref.get("층") not in ("system", "company", "personal") or not re.fullmatch( r"SK-\d{6}", str(ref.get("키") or "") ): raise sd.DocError(f"{where} · 참조는 단가산출(SK)만") out["참조"] = {k: ref.get(k) for k in ("층", "주인", "키")} if out.get("참조비목") not in (*COSTS, None): raise sd.DocError(f"{where} · 참조비목은 노무비 · 재료비 · 경비") if kind != "로직" and out.get("비목") not in sd.PARTS.values(): raise sd.DocError(f"{where} · 비목은 노무비 · 재료비 · 경비") if "단가" in out: out["단가"] = sd.plain(sd._num(out["단가"], f"{where} 단가")) if "값묶음" in out: pack = out["값묶음"] if isinstance(out["값묶음"], dict) else {} out["값묶음"] = {k: sd.plain(sd._num(pack.get(k, 0), f"{where} {k}")) for k in COSTS} if kind == "로직" and "단가" in out: raise sd.DocError(f"{where} · 로직 줄 엑셀 값은 값묶음") if not any(k in out for k in ("단가", "값묶음", "참조")) and not out["이음"]: raise sd.DocError(f"{where} · 엑셀 단가 · 이음 둘 다 없음") return out def clean_row(raw: Any, whole: Any = None) -> dict[str, Any]: """가져온 로직 줄 꼴 — 줄 수 · 식(엔진 파서 · 상한) · 이음 키 · 엑셀 값 · 틀리면 `DocError`.""" whole = whole or _whole() if not isinstance(raw, dict): raise sd.DocError("가져온 로직 · 묶음 아님") out = {k: raw[k] for k in ROW if raw.get(k) not in (None, "", [], {})} out["이름"] = str(raw.get("이름") or "").strip() or "가져온 식" if not re.match(r"^원/\S+$", str(raw.get("결과단위") or "").strip()): raise sd.DocError("가져온 로직 · 결과단위는 「원/단위」") items, extras, mids = raw.get("호표") or [], raw.get("덧줄") or [], raw.get("중간") or [] if not all(isinstance(x, list) for x in (items, extras, mids)): raise sd.DocError("가져온 로직 · 호표 · 덧줄 · 중간은 목록") # 빈 산근(0 원) = 받음 if max(len(items), len(extras), len(mids)) > MANY: raise sd.DocError(f"가져온 로직 · 줄은 {MANY} 까지") out["호표"] = [_line(x, f"호표 {n}째", whole) for n, x in enumerate(items, 1)] if extras: out["덧줄"] = [_extra(x, f"덧줄 {n}째") for n, x in enumerate(extras, 1)] if mids: out["중간"] = [ {**x, "식": _formula(x.get("식"), f"중간 「{x.get('이름')}」")} for x in mids if isinstance(x, dict) and str(x.get("이름") or "").strip() ] return out def _extra(raw: Any, where: str) -> dict: if not isinstance(raw, dict) or raw.get("비목") not in sd.PARTS.values(): raise sd.DocError(f"{where} · 묶음 · 비목(노무비 · 재료비 · 경비)") out = {k: raw[k] for k in EXTRA if raw.get(k) not in (None, "")} return {**out, "이름": str(raw.get("이름") or where), "식": _formula(raw.get("식"), where)} def _meta(raw: Any, name: str) -> Any: try: size = len(json.dumps(raw, ensure_ascii=False, default=str).encode("utf-8")) except (TypeError, ValueError) as e: raise sd.DocError(f"가져온 {name} · 글로 못 담음") from e if size > BIG: raise sd.DocError(f"가져온 {name} · {BIG} 바이트를 넘음") want = list if name == "근거후보" else dict if not isinstance(raw, want): raise sd.DocError(f"가져온 {name} · {'목록' if want is list else '묶음'} 아님") return raw def clean(raw: Any, out: dict) -> dict: """`sd.clean` 끝(`const.clean_more`) — `가져온` 칸 · 바탕과 같이 못 둠.""" meta = raw.get(KEY) if isinstance(raw, dict) else None if meta in (None, {}): return out if not isinstance(meta, dict): raise sd.DocError("가져온 · 묶음 아님") got = {k: _meta(meta[k], k) for k in META if meta.get(k) not in (None, {}, [])} if meta.get("로직") is not None: if out.get("바탕"): raise sd.DocError("가져온 식 단가산출은 바탕 없음") basis = meta.get("단가기준") or "엑셀" if basis not in BASIS: raise sd.DocError("단가기준은 엑셀 · 우리") got |= {"로직": clean_row(meta["로직"]), "단가기준": basis} if got.get("원본"): got["원본"] = _named(got["원본"], got["로직"]) if got.get("연결"): # 소요량 · 계수 이음 기록(797-2) — 자리 · 수 맞는 것만 from M02_MasterTemplete import M02_Sangeun_QtyLinks as qty got["연결"] = qty.tidy(got["연결"], got["로직"]) got = {k: v for k, v in got.items() if v or k != "연결"} return out | ({KEY: got} if got else {}) def _named(src: dict, row: dict) -> dict: """원본 글줄 `자리` · `자리들` = 셈 줄 이름(793-2a) → 줄 정체 cid(이미 cid · `mid:` · `단계:` = 그대로 · 블록 안 줄 이름은 겹치지 않음).""" ids = sd.line_ids(row_of({KEY: {"로직": row}}) or {}) by: dict[str, str] = {} for one, cid in zip([*row["호표"], *(row.get("덧줄") or [])], ids): by.setdefault(str(one.get("이름")), cid) lines = [] for one in src.get("글줄") or []: if isinstance(one, dict): one = {**one, **({"자리": by.get(one["자리"], one["자리"])} if one.get("자리") else {})} if isinstance(one.get("자리들"), list): one["자리들"] = [by.get(x, x) for x in one["자리들"]] lines.append(one) return {**src, "글줄": lines} if "글줄" in src else src def same_part(doc: Any) -> dict | None: """같은 내용 지문 몫(`M02_Library_Same`) — 로직 · 단가기준(원본 · 검산 · 근거후보 뺌).""" if not is_imported(doc): return None return {k: doc[KEY].get(k) for k in ("로직", "단가기준")} def unlinked(doc: dict) -> list[dict]: """이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름}]`.""" row = row_of(doc) or {} ids = sd.line_ids(row) return [ {"자리": ids[n], "이름": one.get("이름") or ""} for n, one in enumerate(doc[KEY]["로직"].get("호표") or []) if not one.get("이음") and not one.get("참조") ] # ── 셈 · 검산 ───────────────────────────────────────────────────────── def _json(value: Any) -> Any: from M02_MasterTemplete import M02_Labor_Doc as labor return labor._json(value) def _run(doc: dict, whole: Any) -> dict: from M02_MasterTemplete import M02_Sangeun_Calc as calc return calc.run(sd.clean(doc, None, whole), whole) def _money(got: dict) -> dict | None: unit = got.get("1단위") if not unit: return None return {**{k: unit[k] for k in COSTS}, "계": unit["계"]} def _same(mine: Any, excel: Any) -> bool: """셈 ≈ 엑셀 — 엑셀 값 끝자리 한 칸 안(엑셀 보이는 값 = 끊은 값).""" if mine is None or excel is None: return False want = round(Decimal(str(excel)), 6) # 엑셀 float 꼬리(8109.000000000001) 뗌 step = Decimal(1).scaleb(min(want.normalize().as_tuple().exponent, 0)) return abs(Decimal(str(mine)) - want) < step def check(doc: dict, whole: Any = None) -> dict[str, Any]: """검산 — 가져온 줄(엑셀 단가 · 고침 없음)을 엔진으로 다시 셈 → 엑셀 금액 · 줄마다 맞댐 · 못 셈 = {같음 False, 까닭}.""" whole = whole or _whole() meta = doc[KEY] bare = {k: doc.get(k) for k in ("키", "이름", "단위", "입력값", "환산")} bare |= {"바탕": None, KEY: {"로직": meta["로직"], "단가기준": "엑셀"}} src = meta.get("원본") or {} excel = src.get("엑셀금액") or {} try: got = _run(bare, whole) except sd.DocError as e: return {"같음": False, "까닭": str(e), "셈": None, "엑셀": excel, "줄": []} mine = _money(got) now = {x.get("cid"): x.get("금액") for x in got.get("줄") or []} | _inner(bare, whole) lines = [x for x in (_compare(one, now) for one in src.get("글줄") or []) if x] ok = bool(mine) and all(_same(mine.get(k), excel.get(k)) for k in excel) and excel != {} why = None if ok else (got.get("까닭") or ("엑셀 금액 없음" if not excel else "금액 다름")) ok = ok and all(x["같음"] for x in lines) return _json({"같음": ok, "까닭": why, "셈": mine, "엑셀": excel, "줄": lines}) def _inner(doc: dict, whole: Any) -> dict: """엔진 날 셈의 중간 `mid:이름` · 단계 `단계:이름` 계(검산 맞댈 값) · 못 셈 = 빈.""" from M02_MasterTemplete import M02_Sangeun_Calc as calc m01, key = calc.labor.m01, str(doc.get("키") or "SK-000000") row = {**row_of(doc), "키": key} if any(x.get("참조") for x in row["호표"]): # 안쪽 산근 값 채움(797-1 · 셈 길과 같음) from M02_MasterTemplete.M02_Labor_Expand import Expand row["호표"] = [dict(x) for x in row["호표"]] calc._bundles(row, whole, Expand(whole), ()) over = calc._Over(whole, key, row) try: auto = {k: v for k, v in m01.mau.values(over, over.logic(key)).items() if v is not None} given = {**auto, **m01._dec(doc.get("입력값") or {})} got = calc.labor.mcut.engine(over, key, m01.cm.mf.run(over, key, given)) except Exception: # noqa: BLE001 — 엔진 FormulaError · 셈 틀림 = 맞댈 값 없음 return {} out = {f"mid:{k}": v for k, v in (got.get("중간") or {}).items()} return out | {f"단계:{x.get('이름')}": x.get("계") for x in got.get("단계") or []} def _compare(one: Any, now: dict, loose: bool = False) -> dict | None: """글줄 하나 맞댐 — 자리(들) 셈 합 ≈ 엑셀 `값` · 맞댈 것 없음 = None · `loose` = 셈에 없는 자리면 None(화면 보기 · 단계 값 없음).""" if not isinstance(one, dict) or one.get("값") is None: return None spots = one.get("자리들") or ([one["자리"]] if one.get("자리") else []) if not spots or (loose and any(s not in now for s in spots)): return None want = one["값"] vals = [now.get(s) for s in spots] got = None if None in vals else sum(Decimal(str(v)) for v in vals) same = _same(got, want) why = None if same else ("짝 줄 없음" if got is None else f"셈 {got} · 엑셀 {want}") out = {"자리": spots[0], "행": one.get("행"), "셈": got, "엑셀": want, "같음": same} return out | ({"자리들": spots} if len(spots) > 1 else {}) | ({"까닭": why} if why else {}) def more(out: dict, clean_: dict) -> dict: """셈 답 덧칸(`const.answer`) — 가져온 식: `원래` = 엑셀 금액 · `가져온` 원본 글 보기.""" if not is_imported(clean_): return out meta = clean_[KEY] src = meta.get("원본") or {} spots = (out.get("시트") or {}).get("자리") or {} now = {k: v.get("지금") for k, v in spots.items()} texts = [] for one in src.get("글줄") or []: hit = _compare(one, now, loose=True) or {} text = {k: one.get(k) for k in ("행", "글", "자리")} text |= {"자리들": one["자리들"]} if one.get("자리들") else {} text |= {"엑셀": one["값"]} if one.get("값") is not None else {} texts.append(text | ({"지금": hit["셈"], "같음": hit["같음"]} if hit else {})) head = {k: src.get(k) for k in ("파일", "시트", "행", "갈래", "엑셀금액")} view = {"단가기준": meta.get("단가기준"), **head, "글줄": texts, "이음없음": unlinked(clean_)} view |= {k: meta[k] for k in ("검산", "근거후보", "바꿈") if meta.get(k)} excel = src.get("엑셀금액") return out | {KEY: view} | ({"원래": excel} if excel else {}) # ── 고치기(화면 캐시 · 저장 없음) ─────────────────────────────────────── def _need(doc: Any) -> dict: if not is_imported(doc): raise sd.DocError("가져온 식 단가산출 아님") return doc def _with(doc: dict, **meta: Any) -> dict: return {**doc, KEY: {**doc[KEY], **meta}} def to_price(doc: Any, basis: str) -> dict[str, Any]: """[우리 단가로] · [엑셀 단가로] — 단가기준만 바꾼 문서 · 바꾸기 전후 1 단위 금액 · 이음없음.""" doc = _need(doc) if basis not in BASIS: raise sd.DocError("단가기준은 엑셀 · 우리") whole = _whole() after = _with(doc, 단가기준=basis) before = _money(_run(doc, whole)) return { "문서": after, "전": before, "후": _money(_run(after, whole)), "이음없음": unlinked(doc), } def link(doc: Any, spot: str, key: Any) -> dict[str, Any]: """자원 줄 이음 고름 — 그 줄 `이음` · 줄 정체가 바뀐 만큼 덮개 · 뺀줄 · 글줄 · 까닭 열쇠 옮김.""" doc = _need(doc) whole = _whole() old = sd.line_ids(row_of(doc)) if spot not in old[: len(doc[KEY]["로직"]["호표"])]: raise sd.DocError(f"없는 자원 줄 「{spot}」") items = [dict(x) for x in doc[KEY]["로직"]["호표"]] at = old.index(spot) items[at]["이음"] = _link(key, items[at]["종류"], f"「{spot}」", whole) after = _with(doc, 로직={**doc[KEY]["로직"], "호표": items}) move = dict(zip(old, sd.line_ids(row_of(after)))) return {"문서": _rekey(after, {a: b for a, b in move.items() if a != b})} def _rekey(doc: dict, move: dict) -> dict: if not move: return doc out = dict(doc) for name in _KEYED: if isinstance(doc.get(name), dict): out[name] = {move.get(c, c): v for c, v in doc[name].items()} if isinstance(doc.get("뺀줄"), list): out["뺀줄"] = [move.get(c, c) for c in doc["뺀줄"]] if isinstance(doc.get("글줄"), list): out["글줄"] = [{**t, "자리": move.get(t.get("자리"), t.get("자리"))} for t in doc["글줄"]] out["더한줄"] = [_rate_rekey(x, move) for x in doc.get("더한줄") or []] src = doc[KEY].get("원본") or {} if src.get("글줄"): lines = [ {**t, "자리": move.get(t.get("자리"), t.get("자리"))} | ({"자리들": [move.get(x, x) for x in t["자리들"]]} if t.get("자리들") else {}) for t in src["글줄"] ] out[KEY] = {**out[KEY], "원본": {**src, "글줄": lines}} return out def _rate_rekey(one: Any, move: dict) -> Any: """율 더한줄 기준 줄 목록(778-4 · `열쇠@비목`) 열쇠 옮김.""" rate = ((one.get("고정") or {}).get("비율") or {}) if isinstance(one, dict) else {} if not isinstance(rate.get("기준"), list): return one keys = [] for k in rate["기준"]: head, _, part = str(k).partition("@") keys.append(move.get(head, head) + (f"@{part}" if part else "")) return {**one, "고정": {**one["고정"], "비율": {**rate, "기준": keys}}} def to_base(doc: Any, code: str, given: dict | None = None, swap: Any = None) -> dict[str, Any]: """[우리 기초로 바꾸기] — 가져온 식 SK → M01 기초(바탕 로직) + 옵션 SK · 고침 걷힘 · 원본 · 검산 · 근거후보 보존 + `바꿈` {로직, 단가기준, 전, 후, 차이}(1 단위) · `swap` = SK 단위 환산 {계수} · 없는데 단위 성격이 다르면 후 = None · `필요` ["환산"](793-3b).""" doc = _need(doc) whole = _whole() before = _money(_run(doc, whole)) head = {k: doc[k] for k in ("키", "이름", "단위", "구분", "상세구분") if k in doc} new = {**head, "바탕": {"로직": str(code or "").strip()}, "입력값": dict(given or {})} new |= {"열린입력": [], "고름": {}, "자재표시": {}, "덮개": {}, "뺀줄": [], "더한줄": []} new |= {"환산": swap} if swap else {} need: list[str] = [] try: after = _money(_run(new, whole)) except sd.DocError as e: if swap or not str(e).startswith("단가산출 단위"): raise after, need = None, ["환산"] gap = {k: after[k] - before[k] for k in after} if before and after else None keep = {k: doc[KEY][k] for k in ("원본", "검산", "근거후보") if doc[KEY].get(k)} was = {k: doc[KEY].get(k) for k in ("로직", "단가기준")} new[KEY] = {**keep, "바꿈": {**was, "전": before, "후": after, "차이": gap}} return {"문서": new, "전": before, "후": after, "차이": gap} | ({"필요": need} if need else {}) # ── 만들기(개인 층) ─────────────────────────────────────────────────── def create( logic: Any, source: Any, name: str = "", unit: str = "", head: dict | None = None, new: bool = False, use: str | None = None, basis: list | None = None, ) -> dict[str, Any]: # fmt: skip """엔진 로직 줄 + 원본 → 지금 층 새 가져온 식 SK — 서버가 다시 셈해 엑셀과 맞댐(검산 · 다르면 `같음` False 로 표지 · 막지 않음) · 같은 층 같은 내용 = 409(`ask` · 733-2).""" from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Library_Ask as ask whole = _whole() row = clean_row(logic, whole) unit = str(unit or "").strip() or row["결과단위"].split("/", 1)[1] doc = {"키": "SK-000000", "이름": str(name or "").strip() or row["이름"], "단위": unit} doc |= {k: str((head or {}).get(k) or "") for k in ("구분", "상세구분")} doc |= {"바탕": None, "입력값": {}, "덮개": {}, "뺀줄": [], "더한줄": []} meta = {"로직": row, "단가기준": "엑셀"} if source not in (None, {}): meta["원본"] = _meta(source, "원본") if basis: # 근거 제안(793-3a `제안.근거후보` 그대로 · 793-3b) meta["근거후보"] = _meta(basis, "근거후보") doc[KEY] = meta got = check(sd.clean(doc, None, whole), whole) key = ask.ask(sd.KIND, doc, new, use) or refs.create(sd.KIND, doc)["이름"] return {"이름": key, "검산": got, "단가기준": "엑셀"}