- 읽기 view(Master.value) = 인력 · 잡품 목록 → 식이 읽는 조종원 수 · 직종 · 잡재료율(옛 꼴 = 그대로) - 목록이 식 한 줄로 안 풀리면 row_entry 바탕 덮음 · 단가산출 기계덮음은 merge 로 부분별 위에(짝 TS) - M02_Machine_Hourly.hourly · brief · M02_MyMachine.hourly = 문서 엔진(doc_sums) · 식 문서 없는 식만 옛 로직 엔진 - pick · _operated · why · logic_key · 호표 기계 고름 · 불러오기 기계 수 = 인력 줄로 - 내 기계 clean = 인력 · 잡품 목록 받음(옛 칸 = from_old) · M01 fix · check_master = 목록 꼴 검사 - 사본 자동 따라옴 = 옛 꼴 사본 줄 대 새 꼴 M01 줄은 새 꼴로 바꿔 대조(덮음 안 지움) - 시험 836_1 몸 검사: 조종원 빈 키 = 식 입력 직종으로 받음 → 틀림 꼴을 「키 없음」으로 고침 - 전후: 655 대 로직 엔진 = 문서 엔진 611 같음 · 다름 0 · 코드만 바꾼 뒤 655 대 hourly · brief · 문서 셈 전후 같음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
12 KiB
Python
267 lines
12 KiB
Python
"""M02 단가산출(SK) M01 자동 따라옴(PLAN 836-2 · 요구 854) — 서버 단독.
|
||
|
||
프로젝트 층 밖 SK(마스터 · 회사 · 개인) = 셈 · 열기 · 목록 · 일위대가 펼침이 모두
|
||
사본 줄 = 지금 M01(+ 층 내 기계) 줄(`rows` · 지운 키 = 사본 마지막 줄) ·
|
||
부름 문서 = 지금 M01 수식 문서(`form`)로 봄 — 묻지 않음 · [단가 갱신] · [재생성] 없음.
|
||
파일(작업본)은 [저장] · 알림 [확인](= 저장) 때만 새 것으로 씀 — 읽기 길은 안 씀 ·
|
||
저장 파일의 사본 = 마지막 저장 때 받은 M01(= 고침 판정의 「복사 당시 원본」).
|
||
고침 판정 = 내용 대조(표지 아님 · 바꿨다 되돌림 = 안 고침) — 셈에 닿는 칸(`LOGIC`)만 견줌:
|
||
끊음[키] 대 부름[키](복사 당시) · 따로 문서 대 `처음`(따로가 된 때의 문서 · 없으면 고친 것).
|
||
M01 에서 바뀐 항목에 고침이 얹혀 있으면 그 고침을 지우고 다시 복사 + 알림 `갱신` 한 줄
|
||
{키, 이름, 갈래, M01 [전 → 후], 내고침 [지워진 값]} · 고침 없는 항목 = 조용히 따라옴 ·
|
||
안 바뀐 항목의 고침 = 그대로. 따로 문서를 지우면 파생(옛 칸 + 새 M01)으로 돌아감.
|
||
가져온 식 SK = 값 줄만 따라옴(가져온 줄 문서 · 식 안 건드림 · 단가기준 「엑셀」 은 엑셀 값 그대로).
|
||
프로젝트 층 · B09 사본 = 그대로(사용자가 고르는 [단가 갱신] · [재생성]).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy as cp
|
||
import json
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||
|
||
import master_doc_cover as mcv # noqa: E402 — 기계 덮음(836-1 · scripts 길 = dc)
|
||
|
||
COVER = mcv.COVER # 문서 칸 기계덮음(836-1) — 따로 문서를 다시 복사할 때 `수식` 머리에 맡겨 둠
|
||
FIRST = "처음" # `수식` 칸 — 따로가 된 때의 문서(고침 판정 원본)
|
||
LOGIC = (
|
||
"변수",
|
||
"식",
|
||
"금액",
|
||
"끝수",
|
||
"결과단위",
|
||
) # 셈에 닿는 칸 — 쌓기(차림) · 이름 · 판은 안 견줌
|
||
MAX_LINES = 8
|
||
_MEMO: dict[tuple, dict] = {}
|
||
_LIVE: list[Any] = [None]
|
||
|
||
|
||
def on() -> bool:
|
||
"""자동 따라옴 — 프로젝트 층 밖."""
|
||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||
|
||
return refs.here()[0] != "project"
|
||
|
||
|
||
def is_sk(doc: Any) -> bool:
|
||
return isinstance(doc, dict) and doc.get("종류") == sd.TITLE
|
||
|
||
|
||
# ── 값 줄 ──────────────────────────────────────────────────────────────
|
||
def _fresh(row: dict, live: Any) -> dict:
|
||
key = str(row.get("키"))
|
||
got = live.index.get(key[:2], {}).get(key)
|
||
if got is None:
|
||
return row # M01 에서 지운 키 = 마지막 값
|
||
new = prices.to_json(got)
|
||
return row if skc._fp(new) == skc._fp(row) else new
|
||
|
||
|
||
def rows(copy: Any) -> Any:
|
||
"""값 사본(`skc` 꼴) → 줄마다 지금 M01(+ 층 내 기계) · 바뀐 몫만 판 다시 · 프로젝트 층 = 그대로."""
|
||
if not (isinstance(copy, dict) and isinstance(copy.get("로직"), list)) or not on():
|
||
return copy
|
||
live = skc.whole_()
|
||
if live is not _LIVE[0]:
|
||
_MEMO.clear()
|
||
_LIVE[0] = live
|
||
tag = (copy.get("판"), copy.get("단가판"), len(copy["로직"]), len(copy.get("단가") or []))
|
||
hit = _MEMO.get(tag)
|
||
if hit is not None:
|
||
return {**copy, **hit}
|
||
got: dict = {}
|
||
for part, mark in (("로직", "판"), ("단가", "단가판")):
|
||
old = copy.get(part) or []
|
||
new = [_fresh(r, live) if isinstance(r, dict) else r for r in old]
|
||
if any(a is not b for a, b in zip(old, new)):
|
||
got |= {part: new, mark: skc._fp(new)}
|
||
if len(_MEMO) > 64: # ponytail: 통째 비움 · SK 가 많아 느려지면 LRU 로
|
||
_MEMO.clear()
|
||
_MEMO[tag] = got
|
||
return {**copy, **got}
|
||
|
||
|
||
# ── 수식 사본(부름 · 끊음 · 따로 문서) ──────────────────────────────────
|
||
def _logic(doc: Any) -> str:
|
||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||
|
||
doc = doc if isinstance(doc, dict) else {}
|
||
return dc.fp(dsk._strip({k: doc.get(k) for k in LOGIC}))
|
||
|
||
|
||
def _say(v: dict) -> str:
|
||
link = v.get("연결") or {}
|
||
for slot in ("값", "식", "키"):
|
||
if link.get(slot) not in (None, ""):
|
||
return str(link[slot])
|
||
return json.dumps(link, ensure_ascii=False, default=str)[:60]
|
||
|
||
|
||
def _name(v: dict) -> str:
|
||
return str(v.get("이름") or f"{v.get('기호') or ''}{v.get('아래') or ''}")
|
||
|
||
|
||
def diff(a: Any, b: Any) -> list[str]:
|
||
"""두 문서의 셈 칸 다른 곳 — 「이름 전 → 후」(변수 · 금액 줄) · 많으면 앞 몇 줄."""
|
||
a, b = (x if isinstance(x, dict) else {} for x in (a, b))
|
||
out: list[str] = []
|
||
va = {dc.md.var_id(v): v for v in a.get("변수") or []}
|
||
vb = {dc.md.var_id(v): v for v in b.get("변수") or []}
|
||
for vid in [*va, *(k for k in vb if k not in va)]:
|
||
x, y = va.get(vid), vb.get(vid)
|
||
if x and y and _say(x) == _say(y):
|
||
continue
|
||
name = _name(y or x)
|
||
out.append(f"{name} {_say(x) if x else '—'} → {_say(y) if y else '—'}")
|
||
la, lb = a.get("금액") or [], b.get("금액") or []
|
||
for n in range(max(len(la), len(lb))):
|
||
x, y = (la[n] if n < len(la) else None), (lb[n] if n < len(lb) else None)
|
||
if dc.fp(x) == dc.fp(y):
|
||
continue
|
||
name = str((y or x or {}).get("이름") or f"금액 {n + 1}")
|
||
show = lambda z: " × ".join(str(z[k]) for k in ("수량", "단가") if z and z.get(k)) or "—" # noqa: E731
|
||
out.append(f"{name} {show(x)} → {show(y)}")
|
||
for slot in ("식", "끝수", "결과단위"):
|
||
if dc.fp(a.get(slot)) != dc.fp(b.get(slot)):
|
||
out.append(slot)
|
||
return out[:MAX_LINES] + ([f"… {len(out) - MAX_LINES}"] if len(out) > MAX_LINES else [])
|
||
|
||
|
||
def form(sk: Any, got: Any, notes: list | None = None) -> Any:
|
||
"""수식 사본 → 지금 M01 으로 본 것(값 줄 · 부름) · M01 에서 바뀐 항목의 고침(끊음 · 따로 문서)은
|
||
지우고 `notes` 에 한 줄 · 안 바뀐 항목 고침 · 프로젝트 층 = 그대로."""
|
||
if not isinstance(got, dict) or not on() or not is_sk(sk):
|
||
return got
|
||
out = rows(got) if "로직" in got else dict(got)
|
||
out = _covers(got, out, notes)
|
||
if imp.is_imported(sk):
|
||
return out # 가져온 식 = 값만(식 · 가져온 줄 그대로)
|
||
live = dc.folder()
|
||
calls = dict(out.get("부름") or {})
|
||
changed = {k for k, d in calls.items() if k in live and dc.fp(d) != dc.fp(live[k])}
|
||
if not changed and not out.get("따로"):
|
||
return out
|
||
for k in changed:
|
||
calls[k] = cp.deepcopy(live[k])
|
||
cut = dict(out.get("끊음") or {})
|
||
for k in sorted(changed & set(cut)):
|
||
mine, was = cut.pop(k), (got.get("부름") or {}).get(k)
|
||
if _logic(mine) != _logic(was) and notes is not None:
|
||
notes.append(_note(k, was, live[k], diff(was, mine), "부름"))
|
||
out |= {"부름": calls, "끊음": cut}
|
||
base = out.get("원본") or {}
|
||
key = str(base.get("키") or "")
|
||
if out.get("따로") and key in live and dc.fp(live[key]) != base.get("판"):
|
||
first = out.get(FIRST)
|
||
mine = out.get("문서") or {}
|
||
if first is None or _logic(mine) != _logic(first):
|
||
mark = (
|
||
diff(first, mine)
|
||
if first is not None
|
||
else ["수식 문서 고침(옛 저장본 — 원본 없음)"]
|
||
)
|
||
if notes is not None:
|
||
notes.append(_note(key, first, live[key], mark, "문서", mine.get("이름")))
|
||
keep = {COVER: mine[COVER]} if mine.get(COVER) else {} # 기계 덮음 = 다른 항목(그대로)
|
||
out = {k: v for k, v in out.items() if k not in ("따로", "문서", FIRST, COVER)} | keep
|
||
if changed and "문서" in out: # 새 문서가 부르는 키 = 닫힌 묶음
|
||
keys = [*dc.calls(out["문서"]), *(c for d in cut.values() for c in dc.calls(d))]
|
||
out["부름"] = dc.closure(dc.over(out), keys, calls)
|
||
return out
|
||
|
||
|
||
# ── 기계 덮음(836-1) — 그 기계 줄이 M01 에서 바뀌면 덮음 지움 ─────────────
|
||
def _eq_rows(copy: dict) -> dict[str, dict]:
|
||
"""사본의 기계 줄 {원문번호 · 키: 줄}."""
|
||
out: dict[str, dict] = {}
|
||
for r in [*(copy.get("로직") or []), *(copy.get("단가") or [])]:
|
||
if isinstance(r, dict) and str(r.get("키"))[:2] == "EQ":
|
||
out[str(r.get("키"))] = r
|
||
if r.get("원문번호"):
|
||
out[str(r["원문번호"])] = r
|
||
return out
|
||
|
||
|
||
def _mine(entry: dict) -> list[str]:
|
||
vals = [f"{k} {v}" for k, v in (entry.get("값") or {}).items()]
|
||
crew = [f"조종원 {x.get('키')} {x.get('인')}인" for x in entry.get("조종원") or []]
|
||
misc = [
|
||
f"잡품 {x.get('이름') or x.get('키') or x.get('줄')} {x.get('율')}%"
|
||
for x in entry.get("잡품") or []
|
||
]
|
||
return [*vals, *crew, *misc] or ["덮음"]
|
||
|
||
|
||
def _row_diff(a: dict, b: dict) -> list[str]:
|
||
va, vb = a.get("값") or {}, b.get("값") or {}
|
||
if not isinstance(va, dict) or not isinstance(vb, dict):
|
||
return [f"{a.get('값')} → {b.get('값')}"]
|
||
keys = [*va, *(k for k in vb if k not in va)]
|
||
return [f"{k} {va.get(k, '—')} → {vb.get(k, '—')}" for k in keys if va.get(k) != vb.get(k)]
|
||
|
||
|
||
def _same_new(a: dict, b: dict) -> bool:
|
||
"""옛 꼴 사본 줄 대 새 꼴 M01 줄(842-3 인력 · 잡품 목록 옮김) — 새 꼴로 바꿔 같으면 안 바뀜."""
|
||
import master_machine_crew as mcrew
|
||
|
||
return skc._fp(mcrew.from_old_row(a)) == skc._fp(mcrew.from_old_row(b))
|
||
|
||
|
||
def _covers(got: dict, out: dict, notes: list | None) -> dict:
|
||
"""문서(따로) · 맡긴 기계 덮음 가운데 그 기계 줄이 M01 에서 바뀐 것 = 지움 + 알림."""
|
||
doc = out.get("문서") if isinstance(out.get("문서"), dict) else None
|
||
cover = dict(mcv.cover_of(doc) if doc is not None else out.get(COVER) or {})
|
||
if not cover:
|
||
return out
|
||
was, now = _eq_rows(got), _eq_rows(out)
|
||
for text in list(cover):
|
||
a, b = was.get(text), now.get(text)
|
||
if a is None or b is None or skc._fp(a) == skc._fp(b) or _same_new(a, b):
|
||
continue
|
||
entry = cover.pop(text)
|
||
if notes is not None:
|
||
name = f"{b.get('이름') or ''} {b.get('규격') or ''}".strip() or text
|
||
notes.append(
|
||
{
|
||
"키": str(b.get("키")),
|
||
"이름": name,
|
||
"갈래": "기계",
|
||
"M01": _row_diff(a, b) or ["바뀜"],
|
||
"내고침": _mine(entry),
|
||
} # fmt: skip
|
||
)
|
||
if doc is not None:
|
||
rest = {k: v for k, v in doc.items() if k != COVER}
|
||
return {**out, "문서": {**rest, COVER: cover} if cover else rest}
|
||
return {k: v for k, v in out.items() if k != COVER} | ({COVER: cover} if cover else {})
|
||
|
||
|
||
def carry(got: dict, root: dict) -> dict:
|
||
"""파생 문서에 맡긴 기계 덮음을 다시 얹음(따로 문서를 다시 복사한 뒤)."""
|
||
return {**root, COVER: got[COVER]} if isinstance(got.get(COVER), dict) and got[COVER] else root
|
||
|
||
|
||
def _note(key: str, was: Any, now: dict, mine: list[str], kind: str, name: Any = None) -> dict:
|
||
return {
|
||
"키": key,
|
||
"이름": str(name or now.get("이름") or key),
|
||
"갈래": kind,
|
||
"M01": diff(was, now) or ["바뀜"],
|
||
"내고침": mine,
|
||
}
|
||
|
||
|
||
def notes(sk: Any) -> list[dict]:
|
||
"""열 때 알림 — 이 SK 저장본에서 지울 고침(없으면 []) · 가져온 식 · 프로젝트 층 = []."""
|
||
if not is_sk(sk) or not dc.has(sk) or imp.is_imported(sk):
|
||
return []
|
||
out: list[dict] = []
|
||
form(sk, sk[dc.KEY], out)
|
||
return json.loads(json.dumps(dc.ml._norm(out), ensure_ascii=False, default=str))
|