Files
Aislo/M02_MasterTemplete/M02_Doc_Sk.py
T
eomsangdonandClaude Opus 5.5 9c803024e0 feat(807-1): 엑셀 불러오기 = 수식 문서 · 단가산출 · 일위대가 하나씩 개인 층으로 · 원문 패널 기초 찾기 + 「어디서 왔나」 카드
- 임도품셈 판 대응표 CSV 새 자리(STmate/old/30_원자료) — 불러오기 시험 26 실패 → 0
- 가져온 식 SK = 수식 문서(멈춘 wip/802-4b-1 M02_Doc_Import 를 메인 판에 다시 얹음 · 빈 산근 = 엑셀 0 원 줄) —
  줄 쌓기 「수식」 탭 길 열림 · 셈 = 단가기준(엑셀 값 기억 | 연결 값) · 화면 캐시 기록 · 수식 길 저장 `단가기준`
- 1,021 블록 전수(실무 여덟 + 표본 밖 셋) 엑셀 검산 · 엑셀 전후 · 우리 전후 모두 같음 · 셈됨 줄지 않음
- 옮기기 도구 「가져온식」 상태 걷음(가져온 줄 문서로 대조)
- [내 일위대가로] = 호표 하나 + 하위 → 개인 층 SC · 부르는 단산 = 가져온 식 SK(같은 내용 = 있는 것 · 못 이은 줄 = 고정)
- [내 단가산출로] = 단산 블록 판 → 개인 층 가져온 식 SK(개인 층이면 [가져온 식으로 열기] 숨김)
- 원문 패널(M01 로직 · M02 단가산출) 맨 위 「단가산출 기초 찾기」 · 가져온 식 SK 첫 화면 = 「어디서 왔나」 카드
  (프로그램 · 파일 · 블록 · 행 · 가져온 때 · 표지 · 엑셀 글의 품셈 절 글만)
- 수식 탭 [엑셀 단가 그대로 | 우리 단가로](단가산출 화면 · 저장 뒤 페이지 문서 단가기준 맞춤)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:11:09 +09:00

211 lines
10 KiB
Python

"""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 != "따로"})}