- 문서 칸 `수식`(옛 `사본` 은 802-5 까지 옆에) = 값 사본(로직 · 자원 단가 줄) + 부름 닫힌 묶음 + SK 문서 · 끊음 · 따로 — `M02_Doc_Copy`(새)
· 부름 문서 = 값 줄이 지금 M01 줄과 같으면 M01 수식 문서 · 다르면 만든 때 줄 변환
- SK 금액 = 문서 셈(`M02_Doc_Sk.on_run`) — 옛 칸(덮개 · 더한줄 …)은 파생(엔진에 넣던 고친 줄 → 문서 · 덮개 없는 바탕 = M01 문서 사본) · 수식 길 고침 = 따로(옛 칸 고침 저장 409 · 재생성 = 파생으로)
- 일위대가 · SC 로직 줄 = 문서 셈(`M02_Labor_Copy.line`) · 줄 옵션 = 문서 규칙(켜쌓기율 변수 r · 고른 자재 = 요소 키 · 별도계상 = 합밖 + 뒤 「재료비의 %」 밑엔 그 줄 금액 더함)
- [단가 갱신] = 자원 단가 줄만(문서 그대로) · [재생성] = 지금 M01 문서로(고친 문서 · 끊음 버림 알림) · 빈 SK = 빈 문서 「—」
- 수식 길 GET/POST …/sangeun/{키}/doc · /preview · /save(층 쓰기 · 서버 정본 셈) — `M02_Doc_Sk_Path` · `M02_Doc_Router`(새 · main 등록)
- B09 구조물 · 서고 사본 = `수식.json` 함께 굳힘(`M02_Doc_B09` · 원본판 안 듦) · B09 셈 그대로(문서 셈 전환 = 802-6)
- 옮기기 `M02_Doc_Migrate`(보기만 · --apply --backup · --restore) · 목록 `docs/raw/요구_1104/_목록_802-4a_M02대조.xlsx` — 층 SK 15 · 일위대가 21 · SC 4 · B09 서고 사본 3 같음 · 빈 SK 2 = 옛 0 → 「—」 · 가져온 식 1 = 802-4b
- 시험 test_802_4a(12) · test_204_2 서고 사본 파일 목록에 수식.json · test_795_1 `_fake` 가 M01 수식 문서도 같이 바꿈(M01 정본 = 수식 문서 — 옛 줄만 바꾼 흉내는 문서 사본에 안 닿음) · 옛 SK 흉내 = 사본 · 수식 둘 다 뗌
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
9.0 KiB
Python
188 lines
9.0 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(엑셀 · 바탕 null) = 옛 길 그대로(802-4b) · 바탕 참조 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_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 빼고."""
|
|
if not isinstance(doc, dict) or doc.get("가져온"):
|
|
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:
|
|
"""옛 칸 지문 — 따로 문서 위 옛 칸 고침 막음."""
|
|
return dc.fp(_strip({k: doc.get(k) for k in OLD_FIELDS if doc.get(k) not in (None, {}, [])}))
|
|
|
|
|
|
# ── 파생 — 엔진에 넣는 고친 줄 → 문서 ─────────────────────────────────
|
|
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 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
|
|
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 = dc.run(copy["문서"], copy)
|
|
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}
|
|
|
|
|
|
# ── 저장 첫머리(옛 길 `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
|
|
if not skc.has(out): # 빈 SK(바탕 · 줄 없음) = 빈 문서 — 금액 「—」(요구서_664 0 장)
|
|
empty = seed(out)
|
|
return {**out, KEY: norm(empty)} if empty else 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 else have
|
|
return {**out, KEY: norm(copy)}
|
|
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]), "부름": 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 != "따로"})}
|