feat(836-2): M02 단가산출 M01 자동 따라옴 — 서버(사본 줄 · 부름 문서 · 고친 항목 다시 복사 · 알림)
- 새 M02_Sk_AutoRenew — 프로젝트 층 밖 SK 사본 줄 = 지금 M01(+ 내 기계) · 부름 문서 = 지금 M01 수식 문서 - 고침 판정 = 내용 대조(끊음 대 부름 · 따로 문서 대 처음) · 바꿨다 되돌림 = 안 고침 - M01 에서 바뀐 항목의 고침 = 지움 + 다시 복사 + 열기 답 사본.갱신 알림 · 사본.자동 표지 - 파일은 [저장] · 알림 확인(저장) 때만 씀 · 읽기 길 안 씀 · 프로젝트 층 그대로 - 옛 시험 8 개 = 손 갱신 길(프로젝트 층 꼴 · 자동 끔) 고정 — 개인 층 「금액 그대로」 단언이 낡음 - 새 시험 test_836_2_sk_auto_renew(㉠ ~ ㉦) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ from M02_MasterTemplete import M02_Doc_Import as dim
|
|||||||
from M02_MasterTemplete import M02_MyMachine_Draft as draft
|
from M02_MasterTemplete import M02_MyMachine_Draft as draft
|
||||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
KEY = dc.KEY
|
KEY = dc.KEY
|
||||||
OLD_FIELDS = (
|
OLD_FIELDS = (
|
||||||
@@ -119,6 +120,7 @@ def copy_of(sk: dict, ctx: tuple | None) -> dict | None:
|
|||||||
got = sk.get(KEY) if dc.has(sk) else seed(sk)
|
got = sk.get(KEY) if dc.has(sk) else seed(sk)
|
||||||
if got is None:
|
if got is None:
|
||||||
return None
|
return None
|
||||||
|
got = renew.form(sk, got) # M01 자동 따라옴(836-2)
|
||||||
if got.get("따로") or ctx is None:
|
if got.get("따로") or ctx is None:
|
||||||
return got
|
return got
|
||||||
if dim.is_imported(sk): # 가져온 식 — 엑셀 단가 기준 줄에서 지음(우리 = 한 번 더 돎)
|
if dim.is_imported(sk): # 가져온 식 — 엑셀 단가 기준 줄에서 지음(우리 = 한 번 더 돎)
|
||||||
@@ -133,6 +135,7 @@ def copy_of(sk: dict, ctx: tuple | None) -> dict | None:
|
|||||||
except dc._ERR as e:
|
except dc._ERR as e:
|
||||||
root = {**dc.md.blank(str(sk.get("키"))), "_틀림": str(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) # 서버가 정한 값
|
values = dc.values_of(sk[skc.KEY]) if skc.has(sk) else dc.values_of(got) # 서버가 정한 값
|
||||||
|
values = renew.rows(values) if renew.is_sk(sk) else values # 836-2
|
||||||
whole = dc.over(values)
|
whole = dc.over(values)
|
||||||
calls = dc.closure(whole, dc.calls(root), got.get("부름"))
|
calls = dc.closure(whole, dc.calls(root), got.get("부름"))
|
||||||
base = str((sk.get("바탕") or {}).get("로직") or "")
|
base = str((sk.get("바탕") or {}).get("로직") or "")
|
||||||
@@ -191,6 +194,7 @@ def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict:
|
|||||||
return {**out, KEY: norm(mine["사본"])}
|
return {**out, KEY: norm(mine["사본"])}
|
||||||
file = stored(out.get("키")) or {}
|
file = stored(out.get("키")) or {}
|
||||||
have = file.get(KEY) if dc.has(file) else (raw.get(KEY) if dc.has(raw) else None)
|
have = file.get(KEY) if dc.has(file) else (raw.get(KEY) if dc.has(raw) else None)
|
||||||
|
have = renew.form(out, have) if have else have # 836-2 — 바뀐 항목 고침 지움 · 저장 때 굳음
|
||||||
if have and have.get("따로") and job != skc.REGEN:
|
if have and have.get("따로") and job != skc.REGEN:
|
||||||
if file and old_fp(file) != old_fp(out):
|
if file and old_fp(file) != old_fp(out):
|
||||||
raise sd.DocError(
|
raise sd.DocError(
|
||||||
@@ -210,6 +214,8 @@ def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict:
|
|||||||
calc.run(out, whole, catch=box)
|
calc.run(out, whole, catch=box)
|
||||||
calls = {} if job == skc.REGEN else (have or {}).get("부름") or {}
|
calls = {} if job == skc.REGEN else (have or {}).get("부름") or {}
|
||||||
first = {**(dc.values_of(out[skc.KEY]) if skc.has(out) else {}), "부름": calls}
|
first = {**(dc.values_of(out[skc.KEY]) if skc.has(out) else {}), "부름": calls}
|
||||||
|
if job != skc.REGEN and (have or {}).get("끊음"): # 안 바뀐 항목 고침 = 그대로(836-2)
|
||||||
|
first["끊음"] = have["끊음"]
|
||||||
copy = copy_of({**out, KEY: first}, box[-1] if box else None)
|
copy = copy_of({**out, KEY: first}, box[-1] if box else None)
|
||||||
if (
|
if (
|
||||||
copy and skc.has(out) and skc.READS.get() is None and (keys := gap.gaps(copy))
|
copy and skc.has(out) and skc.READS.get() is None and (keys := gap.gaps(copy))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|||||||
from M02_MasterTemplete import M02_MyMachine_Draft as draft
|
from M02_MasterTemplete import M02_MyMachine_Draft as draft
|
||||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
MAX = 2_000_000 # 문서 글 크기(바이트) 상한
|
MAX = 2_000_000 # 문서 글 크기(바이트) 상한
|
||||||
JOBS = (skc.PRICE, skc.REGEN)
|
JOBS = (skc.PRICE, skc.REGEN)
|
||||||
@@ -77,7 +78,7 @@ def answer(copy: dict, sk: dict) -> dict[str, Any]:
|
|||||||
"""{문서, 부름, 끊음, 셈, 사본 표지, 연결 기록} — 글 · 수는 JSON 꼴 · 가져온 식 표지 = 단가기준."""
|
"""{문서, 부름, 끊음, 셈, 사본 표지, 연결 기록} — 글 · 수는 JSON 꼴 · 가져온 식 표지 = 단가기준."""
|
||||||
got = dsk.run(sk, copy)
|
got = dsk.run(sk, copy)
|
||||||
head = {k: copy.get(k) for k in ("시점", "단가시점")}
|
head = {k: copy.get(k) for k in ("시점", "단가시점")}
|
||||||
mark = {**head, "따로": bool(copy.get("따로"))}
|
mark = {**head, "따로": bool(copy.get("따로")), "자동": renew.on()} # 836-2 M01 자동 따라옴
|
||||||
mark |= (
|
mark |= (
|
||||||
{"단가기준": dim.basis_of(sk)}
|
{"단가기준": dim.basis_of(sk)}
|
||||||
if dim.is_imported(sk)
|
if dim.is_imported(sk)
|
||||||
@@ -97,7 +98,9 @@ def answer(copy: dict, sk: dict) -> dict[str, Any]:
|
|||||||
def read(key: str) -> dict[str, Any]:
|
def read(key: str) -> dict[str, Any]:
|
||||||
"""읽기 — {문서, 부름, 끊음, 셈, 사본, 판}."""
|
"""읽기 — {문서, 부름, 끊음, 셈, 사본, 판}."""
|
||||||
got = _file(key)
|
got = _file(key)
|
||||||
return {**answer(current(got["문서"]), got["문서"]), "판": got["판"]}
|
out = {**answer(current(got["문서"]), got["문서"]), "판": got["판"]}
|
||||||
|
out["사본"]["갱신"] = renew.notes(got["문서"]) # 836-2 열 때 알림(지울 고침)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# ── 몸 검사 ───────────────────────────────────────────────────────────
|
# ── 몸 검사 ───────────────────────────────────────────────────────────
|
||||||
@@ -137,6 +140,8 @@ def edited(copy: dict, body: dict, key: str, sk: dict | None = None) -> dict:
|
|||||||
out["부름"] = dc.closure(dc.over(out), calls, copy.get("부름"))
|
out["부름"] = dc.closure(dc.over(out), calls, copy.get("부름"))
|
||||||
out = gap.fill(out) # 셈이 읽은 줄 가운데 사본 없는 것 = 지금 값으로 박음(822-1)
|
out = gap.fill(out) # 셈이 읽은 줄 가운데 사본 없는 것 = 지금 값으로 박음(822-1)
|
||||||
out = draft.renew(out) # 기계 창 초안으로 만든 · 고친 내 기계 = 새 값(827-1)
|
out = draft.renew(out) # 기계 창 초안으로 만든 · 고친 내 기계 = 새 값(827-1)
|
||||||
|
if not copy.get("따로"): # 고침 판정 원본(836-2) = 따로가 되기 전 문서
|
||||||
|
out[renew.FIRST] = copy["문서"]
|
||||||
out["따로"] = True
|
out["따로"] = True
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -125,11 +125,18 @@ def _forget(whole: Any) -> None:
|
|||||||
_LIVE[0] = whole
|
_LIVE[0] = whole
|
||||||
|
|
||||||
|
|
||||||
|
def _auto(doc: Any, copy: dict) -> dict:
|
||||||
|
"""SK 사본 = 프로젝트 층 밖이면 지금 M01 줄(836-2 · `M02_Sk_AutoRenew`)."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
return renew.rows(copy) if renew.is_sk(doc) else copy
|
||||||
|
|
||||||
|
|
||||||
def over(doc: Any, whole: Any) -> Any:
|
def over(doc: Any, whole: Any) -> Any:
|
||||||
"""사본 있는 문서 = 사본 색인 · 없으면 `whole` 그대로(옛 길)."""
|
"""사본 있는 문서 = 사본 색인 · 없으면 `whole` 그대로(옛 길)."""
|
||||||
if not has(doc):
|
if not has(doc):
|
||||||
return whole
|
return whole
|
||||||
copy = doc[KEY]
|
copy = _auto(doc, doc[KEY])
|
||||||
tag = (copy.get("판"), copy.get("단가판"))
|
tag = (copy.get("판"), copy.get("단가판"))
|
||||||
if isinstance(whole, Copy) and whole._tag == tag:
|
if isinstance(whole, Copy) and whole._tag == tag:
|
||||||
return whole
|
return whole
|
||||||
@@ -290,6 +297,8 @@ def settle(doc: Any, save: bool = False) -> Any:
|
|||||||
copy = refresh(copy, whole_())[0]
|
copy = refresh(copy, whole_())[0]
|
||||||
if copy and any(isinstance(x, dict) and x.get("다시잇기") for x in out.get("더한줄") or []):
|
if copy and any(isinstance(x, dict) and x.get("다시잇기") for x in out.get("더한줄") or []):
|
||||||
copy = rejoin(copy, out["더한줄"])
|
copy = rejoin(copy, out["더한줄"])
|
||||||
|
if copy:
|
||||||
|
copy = _auto(doc, copy) # M01 자동 따라옴(836-2) — 저장 때 굳음
|
||||||
if save and copy and READS.get() is None: # 읽은 줄만(826-3) = 저장 관문이 더함
|
if save and copy and READS.get() is None: # 읽은 줄만(826-3) = 저장 관문이 더함
|
||||||
copy = grow(copy, stamp(out))
|
copy = grow(copy, stamp(out))
|
||||||
out = _formula_job(out, job)
|
out = _formula_job(out, job)
|
||||||
@@ -305,7 +314,8 @@ def _formula_job(out: dict, job: Any) -> dict:
|
|||||||
if got is None or job not in (PRICE, REGEN):
|
if got is None or job not in (PRICE, REGEN):
|
||||||
return out
|
return out
|
||||||
if job == REGEN:
|
if job == REGEN:
|
||||||
got = {k: v for k, v in got.items() if k not in ("문서", "끊음", "따로")} | {"부름": {}}
|
drop = ("문서", "끊음", "따로", "처음")
|
||||||
|
got = {k: v for k, v in got.items() if k not in drop} | {"부름": {}}
|
||||||
elif got.get("따로"):
|
elif got.get("따로"):
|
||||||
got = dc.refresh(got)[0]
|
got = dc.refresh(got)[0]
|
||||||
return {**out, dc.KEY: got}
|
return {**out, dc.KEY: got}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
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: "—" if not z else f"{z.get('수량') or ''} × {z.get('단가') 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)
|
||||||
|
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("이름")))
|
||||||
|
out = {k: v for k, v in out.items() if k not in ("따로", "문서", FIRST)}
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
@@ -23,6 +23,15 @@ from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
|||||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
PCT = "GC001109" # 원/㎡ · 인력 둘 + 「노무비 * 3 / 100」
|
PCT = "GC001109" # 원/㎡ · 인력 둘 + 「노무비 * 3 / 100」
|
||||||
MAN = "LB000002" # 보통인부
|
MAN = "LB000002" # 보통인부
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ from resources.tester.test_795_1_sk_copy import (
|
|||||||
)
|
)
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
KEY = "GC000211" # 합판거푸집 — 중간 시공량 · 형틀목공 · 보통인부 · 호표 0 = 형틀목공
|
KEY = "GC000211" # 합판거푸집 — 중간 시공량 · 형틀목공 · 보통인부 · 호표 0 = 형틀목공
|
||||||
DOUBLE = [{"할일": "식", "자리": "호표:0", "식": "형틀목공 × 2", "까닭": "sub2임시 시험"}]
|
DOUBLE = [{"할일": "식", "자리": "호표:0", "식": "형틀목공 × 2", "까닭": "sub2임시 시험"}]
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ from resources.tester.test_795_2_sc_copy import LB, PCT, _double, _labor, _sc
|
|||||||
from resources.tester.test_795_2_sc_copy import _calc as _sc_calc
|
from resources.tester.test_795_2_sc_copy import _calc as _sc_calc
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
FORM = "GC000211" # 합판거푸집(옵션 많음)
|
FORM = "GC000211" # 합판거푸집(옵션 많음)
|
||||||
BASIC = "GF000089" # 토사깍기(기계) — 굴착기 시간당 × 1 / 작업량(813-2 GC001148 지움 뒤)
|
BASIC = "GF000089" # 토사깍기(기계) — 굴착기 시간당 × 1 / 작업량(813-2 GC001148 지움 뒤)
|
||||||
ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub2임시 굴착기"}
|
ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub2임시 굴착기"}
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
|||||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ — 연료 · 조종원 있음
|
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ — 연료 · 조종원 있음
|
||||||
PCT = "GC001109" # M01 만 쓰는 SK 바탕
|
PCT = "GC001109" # M01 만 쓰는 SK 바탕
|
||||||
NAME = "서브셋임시기계" # 찾기 기종 글(수 = 규격으로 읽음 · 창 표시는 글로)
|
NAME = "서브셋임시기계" # 찾기 기종 글(수 = 규격으로 읽음 · 창 표시는 글로)
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|||||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
DIG, DIG_NO, DIG_LOGIC = "EQ000022", "0201-0100", "GC000267" # 굴착기(무한궤도) 1.0㎥
|
DIG, DIG_NO, DIG_LOGIC = "EQ000022", "0201-0100", "GC000267" # 굴착기(무한궤도) 1.0㎥
|
||||||
DIG2_NO = "0201-0070" # 바꿔 끼울 굴착기
|
DIG2_NO = "0201-0070" # 바꿔 끼울 굴착기
|
||||||
MAN = "LB000002" # 보통인부
|
MAN = "LB000002" # 보통인부
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import json
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||||||
@@ -24,6 +26,15 @@ from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
|||||||
from resources.tester.test_822_1_copy_gap import _move
|
from resources.tester.test_822_1_copy_gap import _move
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
HERE = os.path.dirname(__file__)
|
HERE = os.path.dirname(__file__)
|
||||||
DIG = "GC000267" # 굴착기 시간당 경비
|
DIG = "GC000267" # 굴착기 시간당 경비
|
||||||
NOW, NEW, NEW_EQ = "'0201-0070'", "'0201-0100'", "EQ000022" # 0.7㎥ → 1.0㎥
|
NOW, NEW, NEW_EQ = "'0201-0070'", "'0201-0100'", "EQ000022" # 0.7㎥ → 1.0㎥
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import copy
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from M01_MasterData import M01_MasterData_Store as m01
|
from M01_MasterData import M01_MasterData_Store as m01
|
||||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||||
@@ -36,6 +38,15 @@ from resources.tester.test_822_1_copy_gap import (
|
|||||||
)
|
)
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
DIG_BASE = "GC000088" # 터파기(기계) 보통토사 — 「기계」 고르기 후보를 닫힌 묶음이 다 담음
|
DIG_BASE = "GC000088" # 터파기(기계) 보통토사 — 「기계」 고르기 후보를 닫힌 묶음이 다 담음
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ from M02_MasterTemplete import M02_MyMachine_Draft as draft
|
|||||||
from M02_MasterTemplete import M02_MyMachine_Router as route
|
from M02_MasterTemplete import M02_MyMachine_Router as route
|
||||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _manual(monkeypatch):
|
||||||
|
"""836-2 — 이 시험 = 손 갱신 길(프로젝트 층 꼴 · 자동 따라옴 끔) · 자동 = test_836_2."""
|
||||||
|
from M02_MasterTemplete import M02_Sk_AutoRenew as renew
|
||||||
|
|
||||||
|
monkeypatch.setattr(renew, "on", lambda: False)
|
||||||
|
|
||||||
|
|
||||||
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ — 연료 · 조종원 있음
|
DIG = "EQ000022" # 굴착기(무한궤도) 1.0㎥ — 연료 · 조종원 있음
|
||||||
NAME = "sub1임시 기계"
|
NAME = "sub1임시 기계"
|
||||||
USER = {"role": "USER", "company_id": 7, "user_id": 42}
|
USER = {"role": "USER", "company_id": 7, "user_id": 42}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||||
|
"""836-2 M01 → M02 단가산출 자동 따라옴(요구 854) — 임시 storage + M01 메모리 사본(정본 파일 안 고침).
|
||||||
|
|
||||||
|
㉠ 노임 · 재료 · 기계 값 바뀜 = 누르지 않고 새 금액(수식 길 열기 · 옛 셈 길 목록 · 일위대가 펼침 같은 답)
|
||||||
|
㉡ 부른 M01 수식 문서 바뀜 = 따라옴 ㉢ 고친 문서 + 그 문서 M01 바뀜 = 다시 복사 + 알림(전 · 후 · 내 고침)
|
||||||
|
㉣ 고침 + 다른 항목만 바뀜 = 고침 그대로 ㉤ 바꿨다 되돌림 = 안 고침(알림 없음)
|
||||||
|
㉥ 프로젝트 층 = 그대로 ㉦ 가져온 식(우리) = 값만 · 식 그대로 · 열기 = 파일 안 고침 · 알림 [확인] = 저장.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||||
|
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||||
|
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||||
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||||
|
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||||
|
from resources.tester.test_822_1_copy_gap import (
|
||||||
|
DIG,
|
||||||
|
MACHINE,
|
||||||
|
MAN,
|
||||||
|
PCT,
|
||||||
|
_at,
|
||||||
|
_live,
|
||||||
|
_move,
|
||||||
|
_sk,
|
||||||
|
_stack,
|
||||||
|
_sum,
|
||||||
|
)
|
||||||
|
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||||
|
|
||||||
|
TOOL = 2 # PCT 「공구손료 및 경장비」 금액 줄 차례
|
||||||
|
|
||||||
|
|
||||||
|
def _doc_move(monkeypatch, key: str, fn) -> None:
|
||||||
|
"""M01 수식 문서 하나 고침(메모리 · 폴더 그대로)."""
|
||||||
|
live = dc.folder()
|
||||||
|
new = copy.deepcopy(live[key])
|
||||||
|
fn(new)
|
||||||
|
fake = {**live, key: new}
|
||||||
|
monkeypatch.setattr(dc, "folder", lambda: fake)
|
||||||
|
|
||||||
|
|
||||||
|
def _rate(text: str):
|
||||||
|
def fn(d: dict) -> None:
|
||||||
|
d["금액"][TOOL]["단가"] = text
|
||||||
|
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def _edit(me, key: str, text: str) -> None:
|
||||||
|
got = _at(me, path.read, key)
|
||||||
|
doc = copy.deepcopy(got["문서"])
|
||||||
|
doc["금액"][TOOL]["단가"] = text
|
||||||
|
_at(me, path.save, key, {"문서": doc, "판": got["판"]})
|
||||||
|
|
||||||
|
|
||||||
|
def _base(me) -> str:
|
||||||
|
return _sk(me, 바탕={"로직": PCT}, 단위="㎡")
|
||||||
|
|
||||||
|
|
||||||
|
def _labor_unit(me, key: str):
|
||||||
|
"""일위대가 줄이 SK 참조를 펼치는 길(`Expand` → `calc.unit`) 1 단위 계."""
|
||||||
|
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||||
|
|
||||||
|
with store.at(me):
|
||||||
|
ref = {"층": "personal", "주인": "7/42", "키": key}
|
||||||
|
whole = _live()
|
||||||
|
got = calc.unit(refs.read_ref(ref), whole, Expand(whole), ())
|
||||||
|
return got["기준"]["계"] if got["기준"] else None
|
||||||
|
|
||||||
|
|
||||||
|
def _list_unit(me, key: str):
|
||||||
|
"""목록 · 옛 셈 길(`calc_doc`) 1 단위 계."""
|
||||||
|
with store.at(me):
|
||||||
|
got = calc.calc_doc(store.read("sangeun", key)["문서"], None, False)
|
||||||
|
return got["1단위"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("what", ["노임", "재료", "기계"])
|
||||||
|
def test_values_follow(world, monkeypatch, what):
|
||||||
|
"""㉠ 값 바뀜 = 묻지 않고 새 금액 · 세 길 같은 답 · 열기 = 파일 안 고침 · 알림 없음."""
|
||||||
|
me = world["personal"]
|
||||||
|
key = _sk(me)
|
||||||
|
res = {"노임": MAN, "재료": next(iter(_live().index["MT"])), "기계": DIG}[what]
|
||||||
|
link = MACHINE if what == "기계" else {"갈래": "요소", "키": res}
|
||||||
|
part = {"노임": "노무비", "재료": "재료비", "기계": "묶음"}[what]
|
||||||
|
_stack(me, key, [("자원", link, part)])
|
||||||
|
was = _sum(me, key)
|
||||||
|
file = me / "sangeun" / f"{key}.json"
|
||||||
|
before = file.read_bytes()
|
||||||
|
_move(monkeypatch, res)
|
||||||
|
got = _at(me, path.read, key)
|
||||||
|
assert got["셈"]["계"] > was
|
||||||
|
assert got["사본"]["자동"] is True and got["사본"]["갱신"] == []
|
||||||
|
assert not got["사본"]["단가새값"] and not got["사본"]["M01새판"]
|
||||||
|
unit = _list_unit(me, key)
|
||||||
|
assert unit["계"] == pytest.approx(got["셈"]["계"])
|
||||||
|
assert _labor_unit(me, key) == pytest.approx(got["셈"]["계"])
|
||||||
|
assert file.read_bytes() == before # 읽기 = 작업본 안 씀
|
||||||
|
|
||||||
|
|
||||||
|
def test_logic_follows(world, monkeypatch):
|
||||||
|
"""㉡ 부른 M01 수식 문서(기계 시간당) 바뀜 = 따라옴 · 알림 없음."""
|
||||||
|
me = world["personal"]
|
||||||
|
key = _sk(me)
|
||||||
|
_stack(me, key, [("기계", MACHINE, "묶음")])
|
||||||
|
was = _sum(me, key)
|
||||||
|
|
||||||
|
def twice(d: dict) -> None:
|
||||||
|
for line in d["금액"]:
|
||||||
|
line["수량"] = f"({line.get('수량') or 1}) × 2"
|
||||||
|
|
||||||
|
_doc_move(monkeypatch, MACHINE["키"], twice)
|
||||||
|
got = _at(me, path.read, key)
|
||||||
|
assert got["셈"]["계"] > was * 1.5 # 율 줄은 누계를 받아 두 배 넘음
|
||||||
|
assert got["사본"]["갱신"] == [] and not got["사본"]["문서새판"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edited_item_recopied(world, monkeypatch):
|
||||||
|
"""㉢ 고친 문서 + 그 M01 문서 바뀜 = 고침 지우고 다시 복사 + 알림(전 · 후 · 내 고침) ·
|
||||||
|
[확인](저장) 뒤 알림 없음 · 따로 아님."""
|
||||||
|
me = world["personal"]
|
||||||
|
key = _base(me)
|
||||||
|
plain = _sum(me, key)
|
||||||
|
_edit(me, key, "노무비 × 5%")
|
||||||
|
assert _sum(me, key) > plain
|
||||||
|
_doc_move(monkeypatch, PCT, _rate("노무비 × 4%"))
|
||||||
|
got = _at(me, path.read, key)
|
||||||
|
labor = got["셈"]["노무비"]
|
||||||
|
assert got["셈"]["경비"] == pytest.approx(int(labor * 0.04), abs=1)
|
||||||
|
assert got["문서"]["금액"][TOOL]["단가"] == "노무비 × 4%"
|
||||||
|
[note] = got["사본"]["갱신"]
|
||||||
|
assert note["키"] == PCT and note["갈래"] == "문서"
|
||||||
|
assert any("3%" in x and "4%" in x for x in note["M01"])
|
||||||
|
assert any("3%" in x and "5%" in x for x in note["내고침"])
|
||||||
|
_at(me, path.save, key, {"할일": "갱신확인", "판": got["판"]})
|
||||||
|
after = _at(me, path.read, key)
|
||||||
|
assert after["사본"]["갱신"] == [] and after["사본"]["따로"] is False
|
||||||
|
assert after["셈"]["계"] == got["셈"]["계"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_kept_when_other_item_moves(world, monkeypatch):
|
||||||
|
"""㉣ 고침 + 다른 항목(노임 값)만 바뀜 = 고침 그대로 · 값은 따라옴."""
|
||||||
|
me = world["personal"]
|
||||||
|
key = _base(me)
|
||||||
|
_edit(me, key, "노무비 × 5%")
|
||||||
|
was = _at(me, path.read, key)["셈"]
|
||||||
|
_move(monkeypatch, MAN)
|
||||||
|
got = _at(me, path.read, key)
|
||||||
|
assert got["문서"]["금액"][TOOL]["단가"] == "노무비 × 5%"
|
||||||
|
assert got["사본"]["따로"] is True and got["사본"]["갱신"] == []
|
||||||
|
assert got["셈"]["노무비"] > was["노무비"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reverted_is_not_edit(world, monkeypatch):
|
||||||
|
"""㉤ 바꿨다 되돌려 저장 = 안 고침 — M01 바뀌면 조용히 따라옴(알림 없음)."""
|
||||||
|
me = world["personal"]
|
||||||
|
key = _base(me)
|
||||||
|
_edit(me, key, "노무비 × 5%")
|
||||||
|
_edit(me, key, "노무비 × 3%")
|
||||||
|
_doc_move(monkeypatch, PCT, _rate("노무비 × 4%"))
|
||||||
|
got = _at(me, path.read, key)
|
||||||
|
assert got["사본"]["갱신"] == []
|
||||||
|
assert got["문서"]["금액"][TOOL]["단가"] == "노무비 × 4%"
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_layer_keeps(world, monkeypatch, tmp_path):
|
||||||
|
"""㉥ 프로젝트 층 사본 = 그대로(M01 바뀌어도 금액 같음 · 고침 그대로)."""
|
||||||
|
me = world["personal"]
|
||||||
|
key = _base(me)
|
||||||
|
_edit(me, key, "노무비 × 5%")
|
||||||
|
proj = me.parent / "p836" / "templates"
|
||||||
|
(proj / "sangeun").mkdir(parents=True)
|
||||||
|
shutil.copy(me / "sangeun" / f"{key}.json", proj / "sangeun" / f"{key}.json")
|
||||||
|
with store.at(proj):
|
||||||
|
assert refs.here()[0] == "project"
|
||||||
|
was = _at(proj, path.read, key)
|
||||||
|
_move(monkeypatch, MAN)
|
||||||
|
_doc_move(monkeypatch, PCT, _rate("노무비 × 4%"))
|
||||||
|
got = _at(proj, path.read, key)
|
||||||
|
assert got["셈"]["계"] == was["셈"]["계"] and got["사본"]["자동"] is False
|
||||||
|
assert got["문서"]["금액"][TOOL]["단가"] == "노무비 × 5%" and got["사본"]["갱신"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_imported_values_only(world, monkeypatch):
|
||||||
|
"""㉦ 가져온 식 단가기준 우리 = 이음 노임만 따라옴 · 가져온 줄 식 그대로 · 엑셀 기준 = 엑셀 값."""
|
||||||
|
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||||
|
|
||||||
|
me = world["personal"]
|
||||||
|
man = {"종류": "인력", "이름": "보통인부", "단위": "인", "수량": "0.5", "비목": "노무비"}
|
||||||
|
row = {
|
||||||
|
"이름": "sub2임시 가져온",
|
||||||
|
"결과단위": "원/㎡",
|
||||||
|
"호표": [{**man, "단가": 165545, "이음": MAN}],
|
||||||
|
}
|
||||||
|
key = _at(me, lambda: imp.create(row, None, "sub2임시 가져온")["이름"])
|
||||||
|
excel = _sum(me, key)
|
||||||
|
doc = _at(me, lambda: store.read("sangeun", key)["문서"])
|
||||||
|
ours = _at(me, lambda: imp.to_price(doc, "우리")["문서"])
|
||||||
|
_at(me, lambda: store.write("sangeun", key, store.read("sangeun", key)["판"], ours))
|
||||||
|
was = _at(me, path.read, key)
|
||||||
|
_move(monkeypatch, MAN)
|
||||||
|
got = _at(me, path.read, key)
|
||||||
|
assert got["셈"]["계"] == pytest.approx(was["셈"]["계"] * 2, abs=2)
|
||||||
|
assert got["문서"]["금액"] == was["문서"]["금액"] and got["사본"]["갱신"] == []
|
||||||
|
back = _at(me, lambda: imp.to_price(store.read("sangeun", key)["문서"], "엑셀")["문서"])
|
||||||
|
_at(me, lambda: store.write("sangeun", key, store.read("sangeun", key)["판"], back))
|
||||||
|
assert _sum(me, key) == excel
|
||||||
Reference in New Issue
Block a user