"""M02 단가산출(SK) 수식 문서 사본(PLAN 802-4a · 요구 664 · 665) — 서버 단독. SK `수식`(꼴 = `M02_Doc_Copy`) — `문서` = 이 SK 문서(키 = SK 키 · 옵션 값 · 줄 규칙까지 든 정본) · `부름` = 부르는 단가산출 닫힌 묶음 · `끊음` = 이 사본에 굳힌 부름(고칠 수 있음) · 값 사본. 두 갈래: 파생(따로 없음) — 옛 칸(바탕 · 덮개 · 뺀줄 · 더한줄 · 수식고침 …)이 정함: 셈마다 옛 칸이 낸 줄 (`M02_Sangeun_Calc.run` 이 엔진에 넣는 고친 줄)을 문서로 옮겨(덮개 없는 바탕 = M01 문서 사본 · 고친 줄 = `master_doc_convert`) 문서 셈 · 지금 화면(옛 칸 고침)이 그대로 돎. 따로 — 수식 길(`M02_Doc_Router`)로 문서를 고침: 저장한 문서가 정본 · 옛 칸 고침 저장 = 409 · [재생성] = 지금 M01 로 다시 찍어 파생으로 돌아감(고친 문서 버림). SK 금액(기준 · 1 단위 = 기준 ÷ 배) = 문서 셈 — 일위대가 · 사전조합 · 구조물 조합이 이 값을 씀. 가져온 식 SK(807-1) = `M02_Doc_Import` 문서(파생 = 가져온 줄 + SK 고침에서 지음 · 따로 = 저장본) · 셈 = 단가기준(엑셀 = 엑셀 값 · 우리 = 연결 값) · 바탕 참조 SK = 옛 길(값 사본 없음). """ from __future__ import annotations import copy as cp import json from contextvars import ContextVar from decimal import Decimal from typing import Any from M02_MasterTemplete import M02_Doc_Copy as dc from M02_MasterTemplete import M02_Doc_Import as dim from M02_MasterTemplete import M02_Sangeun_Copy as skc from M02_MasterTemplete import M02_Sangeun_Doc as sd KEY = dc.KEY OLD_FIELDS = ( "바탕", "입력값", "열린입력", "덮개", "뺀줄", "더한줄", "뺀더한줄", "상수덮개", "표값덮개", "수식고침", "고름", "자재표시", "켜쌓기율", ) # fmt: skip _STAMPED = ("원값", "바탕판", "처음수량", "M01판", "끊음") # 서버가 적는 칸 — 옛 칸 대조에서 뺌 OLD: ContextVar[bool] = ContextVar("옛길", default=False) # 옛 셈 길(옮기기 전 금액 대조) SAVING: ContextVar[dict | None] = ContextVar("수식길저장", default=None) # {키, 사본} — 수식 길만 def own(doc: Any) -> bool: """수식 사본을 가질 SK — 바탕 참조 SK 빼고(가져온 식 = 가져온 줄 문서 · 807-1).""" if not isinstance(doc, dict) or (doc.get("가져온") and not dim.is_imported(doc)): return False return "참조" not in (doc.get("바탕") or {}) def _strip(v: Any) -> Any: if isinstance(v, dict): return {k: _strip(x) for k, x in v.items() if k not in _STAMPED} if isinstance(v, list): return [_strip(x) for x in v] return v def old_fp(doc: dict) -> str: """옛 칸 지문 — 따로 문서 위 옛 칸 고침 막음(가져온 식 = 가져온 줄 · 고른 칸 · 이음도).""" got = {k: doc.get(k) for k in OLD_FIELDS if doc.get(k) not in (None, {}, [])} if dim.is_imported(doc): got["가져온"] = {k: doc["가져온"].get(k) for k in ("로직", "연결")} return dc.fp(_strip(got)) # ── 파생 — 엔진에 넣는 고친 줄 → 문서 ───────────────────────────────── def _set_values(doc: dict, given: dict) -> None: for var in doc.get("변수") or []: link = var.get("연결") or {} if link.get("갈래") in ("선택", "입력") and var.get("열쇠") in given: link["값"] = given[var["열쇠"]] def derive(sk: dict, ctx: tuple) -> dict: """옛 칸이 낸 고친 줄(`ctx` = run 이 엔진에 넣은 (색인, 키, 입력, 바탕 답)) → 이 SK 문서. 덮개 없는 바탕(고친 줄 = 바탕 줄) = 그 M01 문서 사본 · 아니면 고친 줄 변환.""" whole, key, values, got, mats = ctx row = whole.logic(key) given = dc.given_of(whole, key, values) base_key = str((sk.get("바탕") or {}).get("로직") or "") plain = None if base_key: try: frozen = whole.logic(base_key) same = {**row, "키": base_key} == frozen except dc._ERR: same = False plain = dc.doc_of(whole, base_key) if same else None if plain is not None: doc = cp.deepcopy(plain) _set_values(doc, given) elif not (row.get("호표") or row.get("덧줄") or row.get("결과")): doc = dc.md.blank(str(sk.get("키") or ""), str(sk.get("이름") or "")) else: doc = dc.cv.convert(whole, row, given)[0] apart = dc.apart_of(got.get("자재표시"), mats) doc = dc.line_doc(doc, got.get("켜쌓기율"), got.get("고름") or None, apart) return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))} def seed(sk: dict) -> dict | None: """수식 사본 없는 SK(옮기기 전 · 새로)의 씨 — 로직 사본 값 + 빈 부름 · 빈 SK = 빈 문서 · 가져온 식 = 빈 부름(문서는 셈마다 지음).""" if dim.is_imported(sk): return {"부름": {}} if skc.has(sk): return {**dc.values_of(sk[skc.KEY]), "부름": {}} if sk.get("바탕") or sk.get("더한줄"): return None empty = {"판": skc._fp([]), "로직": [], "단가판": skc._fp([]), "단가": [], "부름": {}} return {**empty, "문서": dc.md.blank(str(sk.get("키") or ""), str(sk.get("이름") or ""))} def copy_of(sk: dict, ctx: tuple | None) -> dict | None: """셈에 쓸 수식 사본 — 따로 = 저장본 · 파생 = 지금 옛 칸으로 다시 지은 문서(부름은 있던 것 먼저) · `OLD` 켬(옮기기 대조 · 시험) = 옛 길.""" if OLD.get() or not own(sk): return None got = sk.get(KEY) if dc.has(sk) else seed(sk) if got is None: return None if got.get("따로") or ctx is None: return got if dim.is_imported(sk): # 가져온 식 — 엑셀 단가 기준 줄에서 지음(우리 = 한 번 더 돎) try: same = dim.basis_of(sk) == "엑셀" root = dim.derive(sk, ctx if same else None, dc.base(ctx[0])) except dc._ERR as e: root = {**dc.md.blank(str(sk.get("키"))), "_틀림": str(e)} return {**got, "문서": root, "부름": {}} try: root = derive(sk, ctx) except dc._ERR as e: root = {**dc.md.blank(str(sk.get("키"))), "_틀림": str(e)} values = dc.values_of(sk[skc.KEY]) if skc.has(sk) else dc.values_of(got) # 서버가 정한 값 whole = dc.over(values) calls = dc.closure(whole, dc.calls(root), got.get("부름")) base = str((sk.get("바탕") or {}).get("로직") or "") seen = dc.doc_of(whole, base) if base else None # 바탕 M01 문서 판(「M01 새 판」 표지) origin = {"원본": {"키": base, "판": dc.fp(seen)}} if seen else {} return {**got, **values, "문서": root, "부름": calls, **origin} def on_run(sk: dict, ans: dict, ctx: tuple | None) -> dict: """`M02_Sangeun_Calc.run` 끝 — 수식 사본 있는 SK 금액 = 문서 셈(기준 · 1 단위) + 덧칸 `수식셈`.""" copy = copy_of(sk, ctx) if copy is None: return ans got = run(sk, copy, ctx[0] if ctx else None) money = dc.money(got) base = {k: Decimal(str(v)) for k, v in money.items()} if money else None times = Decimal(str(ans.get("배") or 1)) unit = {k: v / times for k, v in base.items()} if base else None mark = {"셈됨": bool(base), "따로": bool(copy.get("따로"))} if not base: mark["까닭"] = copy["문서"].get("_틀림") or dc.why(got) or "돈 아닌 문서" return {**ans, "기준": base, "1단위": unit, "수식셈": mark} def run(sk: dict, copy: dict, master: Any = None) -> dict: """문서 셈 — 가져온 식 = 단가기준 · 형제 SK(`dim.run`) · 그 밖 = 값 사본(`dc.run`).""" if dim.is_imported(sk): return dim.run(copy["문서"], sk, dc.base(master) if master is not None else None) return dc.run(copy["문서"], copy) # ── 저장 첫머리(옛 길 `clean_doc` 끝) ────────────────────────────────── def stored(key: Any) -> dict | None: from M02_MasterTemplete import M02_MasterTemplete_Store as store if not sd.KEY.match(str(key or "")): return None try: return json.loads((store.root() / sd.KIND / f"{key}.json").read_bytes()) except (OSError, ValueError): return None def norm(copy: dict) -> dict: return json.loads(json.dumps(dc.ml._norm(copy), ensure_ascii=False, default=str)) def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict: """저장 꼴에 `수식` 을 정함 — 수식 길 저장(`NEW`) = 그 사본 · 따로 = 저장본(옛 칸 고침 409 · 단가갱신 = 값만 · 재생성 = 파생으로) · 그 밖 = 옛 칸으로 다시 지음(부름 = 있던 것 · 재생성 = 새로).""" from M02_MasterTemplete import M02_Sangeun_Calc as calc out = {k: v for k, v in out.items() if k != KEY} if not own(out): return out raw = raw if isinstance(raw, dict) else {} mine = SAVING.get() if mine is not None and mine.get("키") == out.get("키"): # 수식 길 저장 — 서버가 지은 사본 return {**out, KEY: norm(mine["사본"])} file = stored(out.get("키")) or {} have = file.get(KEY) if dc.has(file) else (raw.get(KEY) if dc.has(raw) else None) if have and have.get("따로") and job != skc.REGEN: if file and old_fp(file) != old_fp(out): raise sd.DocError( "수식 문서로 고친 단가산출 — 옛 칸(덮개 · 줄) 고침은 수식 탭에서 · 또는 [재생성]", 409, ) copy = dc.refresh(have)[0] if job == skc.PRICE and not dim.is_imported(out) else have return {**out, KEY: norm(copy)} # 빈 SK(바탕 · 줄 없음) = 빈 문서(요구서_664 0 장) · 수식 길로 고친 빈 SK = 위 따로(804-4) if not skc.has(out) and not dim.is_imported(out): empty = seed(out) return {**out, KEY: norm(empty)} if empty else out box: list = [] calc.run(out, whole, catch=box) calls = {} if job == skc.REGEN else (have or {}).get("부름") or {} first = {**(dc.values_of(out[skc.KEY]) if skc.has(out) else {}), "부름": calls} copy = copy_of({**out, KEY: first}, box[-1] if box else None) return {**out, KEY: norm({k: v for k, v in (copy or first).items() if k != "따로"})}