Merge branch 'main_laptop_1' of C:/Program_coding/임도설계 및 견적자동화 프로그램 개발 into sub_laptop_3
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""옛 SK · SC · 일위대가 → 로직 사본 찍어 넣기 — 한 번 돌리는 도구(PLAN 795-3 · 요구 655 · 656).
|
||||
|
||||
대상 = 사본 없는 마스터 · 회사 · 개인 층 문서(`sangeun` · `precombo` · `labor`) — 지금 M01 로 사본을
|
||||
찍고(`M02_Sangeun_Copy.stamp` · `M02_Labor_Copy.stamp`) 전후 금액 대조:
|
||||
SK = `calc_doc` 기준(노 · 재 · 경 · 계) · SC · 일위대가 = `Labor_Doc.calc` 기준합 · 반영합.
|
||||
같음 = 파일에 `사본` 칸만 더해 씀(그 밖 칸 그대로) · 다름 · 못 셈 = 안 씀(목록) ·
|
||||
닿을 것 없음(바탕 참조 SK · 참조 · 손 단가 줄만) = 그대로.
|
||||
프로젝트 층 일위대가 = 세기 · 대조만(B09 사본 규칙 795-4 와 함께 옮김).
|
||||
되돌릴 길 = 쓰기 전 파일 그대로 `--backup` 폴더에(층 자리 그대로) · `--restore` = 다시 덮음.
|
||||
|
||||
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Migrate # 보기만(표)
|
||||
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Migrate --apply --backup tmp/795_3
|
||||
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Migrate --restore tmp/795_3
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
KINDS = ("sangeun", "precombo", "labor")
|
||||
|
||||
|
||||
def _anchors() -> dict[str, Path]:
|
||||
return {"system": layers.system_dir(), "storage": layers.storage_root()}
|
||||
|
||||
|
||||
def _files() -> list[tuple[str, str, Path, bool]]:
|
||||
"""(어디, 종류, 파일, 프로젝트?) — 층 문서 + 프로젝트 층 일위대가."""
|
||||
out = [(w, k, p, False) for k in KINDS for w, p in layers.layer_files(k)]
|
||||
root = layers.storage_root()
|
||||
for p in sorted(root.glob("*/*/*/templates/labor/[!.]*.json")):
|
||||
out.append((f"프로젝트 {p.parts[-4]}", "labor", p, True))
|
||||
return out
|
||||
|
||||
|
||||
def _round(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {k: _round(v) for k, v in value.items()}
|
||||
return None if value is None else round(float(value), 6)
|
||||
|
||||
|
||||
def _money(kind: str, doc: dict, whole: Any) -> Any:
|
||||
"""그 문서 금액 — `whole` = 셈 색인(지금 M01 | 사본 덮개)."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
if kind == "sangeun":
|
||||
return _round(calc.run(doc, whole, None, line=False).get("기준"))
|
||||
got = labor.calc(doc, None if kind == "labor" else {}, whole=whole)
|
||||
return _round({k: got.get(k) for k in ("기준합", "반영합")})
|
||||
|
||||
|
||||
def _clean(kind: str, doc: dict, key: str) -> dict:
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
plain = {k: v for k, v in doc.items() if k not in (skc.KEY, skc.JOB)}
|
||||
if kind == "sangeun":
|
||||
return sd.clean(plain, None, skc.whole_())
|
||||
return labor.clean({**plain, "열": doc.get("열") or key})
|
||||
|
||||
|
||||
def check(kind: str, path: Path) -> dict[str, Any]:
|
||||
"""문서 하나 — {상태: 있음 | 필요없음 | 같음 | 다름 | 못셈, 전, 후, 사본?, 까닭?}."""
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
doc = json.loads(path.read_bytes())
|
||||
if skc.has(doc):
|
||||
return {"상태": "있음"}
|
||||
folder = path.parent.parent
|
||||
try:
|
||||
with store.at(folder):
|
||||
clean = _clean(kind, doc, path.stem)
|
||||
copy = skc.stamp(clean) if kind == "sangeun" else lc.stamp(clean)
|
||||
if copy is None:
|
||||
return {"상태": "필요없음"}
|
||||
live = skc.whole_()
|
||||
before = _money(kind, clean, live)
|
||||
after = _money(kind, clean, skc.over({skc.KEY: copy}, live))
|
||||
except Exception as e: # noqa: BLE001 — 못 셈 = 목록(통째로 멈추지 않음)
|
||||
return {"상태": "못셈", "까닭": str(e) or type(e).__name__}
|
||||
row = {"상태": "같음" if before == after else "다름", "전": before, "후": after}
|
||||
return {**row, "사본": copy} if before == after else {**row, "까닭": "전후 금액 다름 — 안 옮김"}
|
||||
|
||||
|
||||
def _backup_path(path: Path, backup: Path) -> Path:
|
||||
for name, anchor in _anchors().items():
|
||||
if path.resolve().is_relative_to(anchor.resolve()):
|
||||
return backup / name / path.resolve().relative_to(anchor.resolve())
|
||||
raise ValueError(f"층 밖 파일 「{path}」")
|
||||
|
||||
|
||||
def migrate(apply: bool = False, backup: Path | None = None) -> list[dict[str, Any]]:
|
||||
"""모든 층 — 문서마다 {어디, 종류, 키, 상태, 씀, …} · `apply` = 같음만 씀(프로젝트 층 빼고)."""
|
||||
if apply and backup is None:
|
||||
raise ValueError("--apply 는 --backup 폴더가 있어야 함")
|
||||
out = []
|
||||
for where_, kind, path, project in _files():
|
||||
got = check(kind, path)
|
||||
copy = got.pop("사본", None)
|
||||
row = {"어디": where_, "종류": kind, "키": path.stem, **got, "씀": False}
|
||||
if apply and copy and not project:
|
||||
keep = _backup_path(path, backup)
|
||||
keep.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, keep)
|
||||
atomic_write_json(path, {**json.loads(path.read_bytes()), skc.KEY: copy})
|
||||
row["씀"] = True
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
def restore(backup: Path) -> list[str]:
|
||||
"""`--backup` 폴더 파일을 제자리로 다시 덮음 — 덮은 파일 목록."""
|
||||
done = []
|
||||
for name, anchor in _anchors().items():
|
||||
for keep in sorted((backup / name).rglob("*.json")):
|
||||
target = anchor / keep.relative_to(backup / name)
|
||||
shutil.copy2(keep, target)
|
||||
done.append(str(target))
|
||||
return done
|
||||
|
||||
|
||||
def table(rows: list[dict[str, Any]]) -> dict[str, dict[str, int]]:
|
||||
"""층 갈래 × 종류 × 상태 개수."""
|
||||
count: Counter = Counter()
|
||||
for r in rows:
|
||||
count[(r["어디"].split(" ")[0], r["종류"], r["상태"])] += 1
|
||||
out: dict[str, dict[str, int]] = {}
|
||||
for (layer, kind, state), n in sorted(count.items()):
|
||||
out.setdefault(f"{layer} {kind}", {})[state] = n
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__": # 브레인 · 사용자 승인 뒤만 --apply
|
||||
args = sys.argv[1:]
|
||||
if "--restore" in args:
|
||||
print("\n".join(restore(Path(args[args.index("--restore") + 1]))))
|
||||
raise SystemExit
|
||||
at = Path(args[args.index("--backup") + 1]) if "--backup" in args else None
|
||||
rows = migrate("--apply" in args, at)
|
||||
for one in rows:
|
||||
if one["상태"] not in ("있음", "필요없음", "같음"):
|
||||
print(json.dumps(one, ensure_ascii=False, default=str))
|
||||
print(json.dumps(table(rows), ensure_ascii=False, indent=1))
|
||||
@@ -0,0 +1,146 @@
|
||||
"""M02 일위대가 · 사전조합(SC) 로직 사본(PLAN 795-2 · 요구 655 · 656 · 요구서_655) — 서버 단독.
|
||||
|
||||
SK 사본(`M02_Sangeun_Copy` · 795-1) 한 벌 — 문서 칸 `사본` 꼴 · 판 · 단가 · 할일 칸 `사본할일`
|
||||
(단가갱신 | 재생성) · [단가 갱신] = 자원 단가 줄만 · 자동 갱신 없음 · 지워진 M01 키 = 마지막 값.
|
||||
사본에 담는 것 = 담은로직 로직 줄 · 줄 덮개(`M02_Labor_Patch`) 더한줄(로직 · 요소 · 고정 이음) ·
|
||||
고정 줄 이음 · 사용자 자재(MT) 가 닿는 M01 닫힌 묶음(참조 줄 = 그 문서 제 사본).
|
||||
셈 = `M02_Labor_Doc.calc` 층 셈(whole 안 줌) 첫머리 `live` — M01 색인 위 사본 줄(`skc.Copy`) ·
|
||||
줄 덮개 = 사본 위 고친 칸(cid 열쇠 · [재생성] 뒤 다시 얹힘 · 못 얹은 칸 = 짝잃음) ·
|
||||
사전조합 1 단위 값(`Expand`) = 그 SC 사본(`combo`) ·
|
||||
프로젝트 층 · B09(단가 책 · 굳힌 참조사본) = 옛 길(795-4).
|
||||
사본 정함(`settle` · 셈 · 저장 첫머리) = 받은 사본 먼저(열 복사 · 층 복사 = 원본 사본 그대로 · 판은
|
||||
서버가 다시 셈) → 없으면 저장본(일위대가 = labor/<열> · SC = precombo/<키>) → [저장] 때 없으면 새로
|
||||
찍음 · 사본에 없는 새 줄만 지금 M01 에서 더함 · 사본 없는 옛 문서 = 셈은 옛 길.
|
||||
길(`M02_Labor_Router`) POST …/labor/{열 | SC 키}/copy/{price|regen} {doc, 수량?} →
|
||||
{doc(사본할일), 전, 후, 바뀐자원 | 못얹은}(저장 없음).
|
||||
짝(CLAUDE.md 5장 ①) `M02_MasterTemplete_Labor_Calc.ts` = 서버 기준 값을 받아 수량 · 반영 · 합만 —
|
||||
사본은 기준 값에만 닿음(짝 안 고침 · 거울 시험 `test_795_2_sc_copy`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
|
||||
KEY, JOB, PRICE, REGEN = skc.KEY, skc.JOB, skc.PRICE, skc.REGEN
|
||||
_SC = re.compile(r"^SC-\d{6}$")
|
||||
|
||||
|
||||
def _project() -> bool:
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
return refs.here()[0] == "project"
|
||||
|
||||
|
||||
def stamp(doc: dict) -> dict | None:
|
||||
"""지금 M01 로 새 사본 — 닿을 것 없음(참조 줄 · 손 단가 고정 줄만) = None."""
|
||||
lines = [x for x in doc.get("담은로직") or [] if isinstance(x, dict)]
|
||||
keys = [str(x["로직"]) for x in lines if x.get("로직")]
|
||||
links = [str(v) for x in lines for v in ((x.get("고정") or {}).get("이음") or {}).values() if v]
|
||||
for x in lines:
|
||||
more = skc.reach((x.get("패치") or {}).get("더한줄"))
|
||||
keys, links = keys + more[0], links + more[1]
|
||||
links += [
|
||||
str(m["코드"]) for m in doc.get("자재") or [] if isinstance(m, dict) and m.get("코드")
|
||||
]
|
||||
return skc.snap(list(dict.fromkeys(keys)), list(dict.fromkeys(links)))
|
||||
|
||||
|
||||
def _stored(doc: dict) -> dict | None:
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
key = str(doc.get("키") or doc.get("열") or "")
|
||||
kind = "precombo" if _SC.match(key) else "labor"
|
||||
try:
|
||||
got = json.loads(store._path(kind, key).read_bytes())
|
||||
except (OSError, ValueError, store.StoreError):
|
||||
return None
|
||||
return got[KEY] if skc.has(got) else None
|
||||
|
||||
|
||||
def settle(doc: Any, save: bool = False) -> Any:
|
||||
"""셈 · 저장 첫머리 — 서버가 정한 사본으로 갈음(할일 · 저장 때 찍음 · 새 줄 · 할일 칸 뗌)."""
|
||||
if not isinstance(doc, dict) or _project():
|
||||
return doc
|
||||
job = doc.get(JOB) # 모르는 할일 = 없음
|
||||
out = {k: v for k, v in doc.items() if k not in (KEY, JOB)}
|
||||
copy = skc.taken(doc.get(KEY)) or _stored(doc)
|
||||
if job == REGEN or (copy is None and save):
|
||||
copy = stamp(out)
|
||||
elif job == PRICE and copy:
|
||||
copy = skc.refresh(copy, skc.whole_())[0]
|
||||
if save and copy:
|
||||
copy = skc.grow(copy, stamp(out))
|
||||
return {**out, KEY: copy} if copy else out
|
||||
|
||||
|
||||
def keep(raw: Any, out: dict) -> dict:
|
||||
"""정리한 문서에 사본 · 할일 칸을 실어 보냄(꼴 검사는 `settle` 몫)."""
|
||||
if not isinstance(raw, dict):
|
||||
return out
|
||||
got = {k: raw[k] for k in (KEY, JOB) if raw.get(k) is not None}
|
||||
return {**out, **got}
|
||||
|
||||
|
||||
def live(doc: dict) -> Any:
|
||||
"""층 셈 색인 — 사본 있는 문서 = M01 위 사본 줄 · 없거나 프로젝트 층 = 지금 M01."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
whole = m01.loaded()[1]
|
||||
return whole if _project() else skc.over(settle(doc), whole)
|
||||
|
||||
|
||||
def combo(expand: Any, doc: dict, trail: tuple[str, ...]) -> dict[str, Any]:
|
||||
"""사전조합 1 단위 값 — 그 SC 사본 색인으로(층 셈만 · 프로젝트 = 굳힌 단가 그대로)."""
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
was = expand.whole
|
||||
if expand.read is refs.read_ref and skc.has(doc):
|
||||
expand.whole = skc.over(doc, was)
|
||||
try:
|
||||
return expand._combo(doc, trail)
|
||||
finally:
|
||||
expand.whole = was
|
||||
|
||||
|
||||
def mark(ans: dict, doc: dict) -> dict:
|
||||
"""셈 답 덧칸 `사본` {시점, 단가시점, M01새판, 지운키, 단가새값} — 사본 없음 = 칸 없음."""
|
||||
return skc.mark(ans, doc)
|
||||
|
||||
|
||||
# ── [단가 갱신] · [재생성] 미리 셈(저장 없음) ──────────────────────────
|
||||
def _money(ans: dict) -> dict:
|
||||
return {k: ans.get(k) for k in ("기준합", "반영합")}
|
||||
|
||||
|
||||
def _lost(ans: dict) -> dict[str, dict]:
|
||||
got = {}
|
||||
for n, line in enumerate(ans.get("줄") or []):
|
||||
for x in line.get("짝잃음") or []:
|
||||
if isinstance(x, dict):
|
||||
got[f"{n}|{x.get('cid')}"] = {**x, "줄": n}
|
||||
return got
|
||||
|
||||
|
||||
def preview(doc: dict, job: str, given: dict | None = None) -> dict[str, Any]:
|
||||
"""{doc(할일 얹음), 전, 후, 바뀐자원 · 못얹은} — `doc` = 정리한 문서 · 재생성 ⊃ 단가갱신."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
new = REGEN if REGEN in (doc.get(JOB), job) else job
|
||||
after_doc = {**doc, JOB: new}
|
||||
before, after = labor.calc(doc, given), labor.calc(after_doc, given)
|
||||
out: dict[str, Any] = {"doc": after_doc, "전": _money(before), "후": _money(after)}
|
||||
if job == PRICE:
|
||||
copy = skc.taken(doc.get(KEY)) or _stored(doc)
|
||||
out["바뀐자원"] = skc.refresh(copy, skc.whole_())[1] if copy else []
|
||||
else:
|
||||
was = _lost(before)
|
||||
out["못얹은"] = [
|
||||
{"줄": x["줄"], "cid": x.get("cid"), "까닭": x.get("까닭") or "새 사본에 그 줄 없음"}
|
||||
for c, x in _lost(after).items()
|
||||
if c not in was
|
||||
]
|
||||
return out
|
||||
@@ -42,7 +42,7 @@ from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
from M02_MasterTemplete import M02_Labor_Patch as patch
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc, M02_Labor_Patch as patch
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Labor_Spec as spec
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
@@ -102,7 +102,7 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
모르는 입력 이름 · 빈 입력값은 버림 · 자재 이름 · 규격 · 단위는 M01 것."""
|
||||
if not isinstance(doc, dict):
|
||||
raise _bad("일위대가 문서가 묶음 아님")
|
||||
whole = m01.loaded()[1]
|
||||
whole = lc.live(doc) # 로직 사본(795-2) — 지워진 M01 키도 사본 줄로
|
||||
lines, seen, here = [], set(), None
|
||||
for at, one in enumerate(doc.get("담은로직") or [], start=1):
|
||||
where = f"단가산출 기초 {at}째"
|
||||
@@ -203,7 +203,7 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
cols = spec.clean(doc.get("규격"), {spec.line_key(x) for x in [*lines, *mats]})
|
||||
except spec.SpecError as e:
|
||||
raise _bad(str(e)) from e
|
||||
return {**out, "규격": cols} if cols else out
|
||||
return lc.keep(doc, {**out, "규격": cols} if cols else out) # 사본 칸(795-2)
|
||||
|
||||
|
||||
def _alias(alias: Any, where: str) -> str | None:
|
||||
@@ -501,7 +501,7 @@ def calc(
|
||||
|
||||
프로젝트 사본 셈(142-1)도 이 한 벌 — `whole` = 사본 `단가.json`(`M02_Item_Prices.load_prices`) ·
|
||||
`read` = 참조 → 굳힌 `참조사본` · `pills` = 사본 `알약`(가져온 때 것). 없으면 지금 M01 · 층."""
|
||||
whole, have = m01.loaded()[1] if whole is None else whole, _area_of(doc.get("열")) if given is None else given # fmt: skip
|
||||
whole, have = lc.live(doc) if whole is None else whole, _area_of(doc.get("열")) if given is None else given # fmt: skip
|
||||
expand = refs.Expand(whole, read or refs.read_ref, have) # 사전조합 안 줄 수량 = 알약 값(723-1)
|
||||
if pills is None:
|
||||
pills = combo_store.combo_pills({"담은로직": doc.get("담은로직") or []})["물량"]
|
||||
@@ -665,7 +665,7 @@ def _json(value: Any) -> Any:
|
||||
def server(doc: Any) -> dict[str, Any]:
|
||||
"""[저장] · 복사 때 서버 정본 — 정리 + 지금 면적 값으로 `계산값`(브라우저 값 버림) ·
|
||||
사용자 자재가 로직 자재 품목과 같으면 400(103-1 · 같은 품목 두 번 셈)."""
|
||||
got = clean(doc)
|
||||
got = lc.settle(clean(doc), True) # 로직 사본(795-2) — 찍기 · 할일 · 새 줄
|
||||
out = calc(got)
|
||||
no_twice(got, out)
|
||||
return {**got, "계산값": {**out, **spec_sums(got, out)}}
|
||||
|
||||
@@ -13,6 +13,7 @@ from collections.abc import Callable
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Patch as patch
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
@@ -68,8 +69,8 @@ class Expand:
|
||||
self.memo[rid] = self._promote(doc)
|
||||
elif refs.kind_of(ref["키"]) == "sangeun": # 단가산출(715-2) = 고친 줄 셈 1 단위
|
||||
self.memo[rid] = calc.unit(doc, self.whole, self, (*trail, rid))
|
||||
else:
|
||||
self.memo[rid] = self._combo(doc, (*trail, rid))
|
||||
else: # 사전조합 = 그 SC 로직 사본(795-2)
|
||||
self.memo[rid] = lc.combo(self, doc, (*trail, rid))
|
||||
return self.memo[rid]
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -214,7 +214,9 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]:
|
||||
out["자재"] = _combo_mats(doc, out["담은로직"])
|
||||
out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")})
|
||||
out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None
|
||||
return out
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
|
||||
return lc.settle(lc.keep(doc, out), True) # 로직 사본(795-2)
|
||||
|
||||
|
||||
def is_rate(one: Any) -> bool:
|
||||
|
||||
@@ -18,6 +18,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
@@ -53,7 +54,34 @@ def _doc(column: str, sent: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
def _calc(column: str, body: CalcBody) -> dict[str, Any]:
|
||||
doc = _doc(column, body.doc)
|
||||
return {**labor.calc(doc, body.수량), "알약": labor.pills(doc)}
|
||||
got = lc.mark(labor.calc(doc, body.수량), lc.settle(doc)) # 사본 표지(795-2)
|
||||
return {**got, "알약": labor.pills(doc)}
|
||||
|
||||
|
||||
_JOBS = {"price": lc.PRICE, "regen": lc.REGEN}
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/labor/{column}/copy/{how}")
|
||||
def labor_copy(
|
||||
layer: Layer,
|
||||
column: str,
|
||||
how: str,
|
||||
body: CalcBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""로직 사본(795-2) — price [단가 갱신] · regen [재생성] · 일위대가 열 · 사전조합 SC 키 ·
|
||||
답 = 할일 얹은 doc(화면 캐시 · [저장] 때 서버가 적용) + 전 · 후 합 + 바뀐자원 | 못얹은."""
|
||||
if how not in _JOBS:
|
||||
raise HTTPException(status_code=404, detail="price · regen")
|
||||
return _call(_root(session, layer, owner), _copy_job, column, body, _JOBS[how])
|
||||
|
||||
|
||||
def _copy_job(column: str, body: CalcBody, job: str) -> dict[str, Any]:
|
||||
"""미리 셈은 정리한 문서로 · 답 doc = 보낸 화면 문서 + 할일(SC · 일위대가 꼴 그대로)."""
|
||||
got = lc.preview(_doc(column, body.doc), job, body.수량)
|
||||
sent = body.doc if body.doc is not None else store.read("labor", column)["문서"]
|
||||
return {**got, "doc": {**sent, lc.JOB: got["doc"][lc.JOB]}}
|
||||
|
||||
|
||||
def _pills(column: str) -> dict[str, Any]:
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import Any
|
||||
|
||||
from common_util.common_util_derived_store import derived_key
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
DROP = {"키", "이름", "구분", "상세구분", "본뜬", "메모", "별칭", "바탕판", "원값", "계산값", "열"}
|
||||
@@ -135,6 +136,7 @@ class Same:
|
||||
# 값 바꾸는 새 칸(790-2 · 792-1) — 없으면 빠짐 = 옛 지문 그대로
|
||||
**{k: norm(doc.get(k)) for k in ("상수덮개", "표값덮개", "식덮개", "변수줄")},
|
||||
"가져온": norm(imp.same_part(doc)), # 가져온 식(793-2) = 로직 · 단가기준
|
||||
"사본": skc.same_part(doc), # 로직 사본(795-1) — 지금 M01 과 다른 판만
|
||||
}
|
||||
return {k: v for k, v in body.items() if v not in EMPTY}
|
||||
|
||||
@@ -148,6 +150,7 @@ class Same:
|
||||
"담은로직": _sorted([self._line(x, trail, False) for x in doc.get("담은로직") or []]),
|
||||
"자재": _sorted(mats),
|
||||
"규격": norm(doc.get("규격")),
|
||||
"사본": skc.same_part(doc), # 로직 사본(795-2) — 지금 M01 과 다른 판만
|
||||
}
|
||||
return {k: v for k, v in body.items() if v not in EMPTY}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Const as const, M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Views as views
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc, M02_Sangeun_Views as views
|
||||
|
||||
COSTS = labor.COSTS
|
||||
Ref = dict[str, Any]
|
||||
@@ -124,7 +124,7 @@ def _base_of(doc: dict, whole: Any, read: Any, trail: tuple[str, ...]) -> dict[s
|
||||
return {**empty, "줄": sd.empty_row(doc)}
|
||||
if "로직" in base:
|
||||
try:
|
||||
return {**empty, "줄": whole.logic(base["로직"])}
|
||||
return {**empty, "줄": skc.over(doc, whole).logic(base["로직"])} # 사본(795-1)
|
||||
except Exception as e: # noqa: BLE001 — 엔진 FormulaError
|
||||
raise sd.DocError(f"바탕 로직 · {e}") from e
|
||||
ref = base["참조"]
|
||||
@@ -350,7 +350,7 @@ def run(
|
||||
`line` = 일위대가 덮개 줄."""
|
||||
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||
|
||||
got = resolve(doc, whole, read, trail, given, line)
|
||||
got = resolve(doc, whole := skc.over(doc, whole), read, trail, given, line)
|
||||
row = {**got["줄"], "호표": [dict(x) for x in got["줄"]["호표"]]}
|
||||
_bundles(row, whole, expand or Expand(whole, read), trail)
|
||||
head = {k: got[k] for k in ("정체", "짝잃음", "바탕판")} | {"배": got["배"]}
|
||||
@@ -529,7 +529,7 @@ def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]:
|
||||
"""저장 꼴(`Labor_Ref.clean_doc` 가 부름) — 검사 · 참조 · 순환 · 서버가 `원값` · `바탕판`."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
whole = m01.loaded()[1]
|
||||
whole = skc.over(doc := skc.settle(doc, True), m01.loaded()[1]) # 사본(795-1)
|
||||
try:
|
||||
rough = sd.clean(doc, None, whole)
|
||||
_check_refs(rough, here_)
|
||||
@@ -579,7 +579,7 @@ def calc_doc(doc: Any, given: dict | None = None, seen: bool = True) -> dict[str
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
with m01.cache.steady(): # 한 셈 동안 M01 파일 자국 한 번(792-2)
|
||||
whole = m01.loaded()[1]
|
||||
whole = skc.over(doc := skc.settle(doc), m01.loaded()[1]) # 사본(795-1)
|
||||
try:
|
||||
clean = sd.clean(doc, None, whole)
|
||||
_check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422
|
||||
@@ -588,7 +588,7 @@ def calc_doc(doc: Any, given: dict | None = None, seen: bool = True) -> dict[str
|
||||
got = run(clean, whole, given, trail=(me,), detail=True, line=line, seen=seen)
|
||||
except sd.DocError as e:
|
||||
raise _fail(e) from e
|
||||
return const.answer(got, clean, doc, given, (me,)) # 옛 답 + 원래 · 시트(790-2)
|
||||
return skc.mark(const.answer(got, clean, doc, given, (me,)), clean) # 790-2 · 795-1
|
||||
|
||||
|
||||
def from_line(
|
||||
|
||||
@@ -229,6 +229,8 @@ def clean_more(raw: Any, out: dict) -> dict:
|
||||
why = {_key(k, "까닭", _KEY): _words(v, f"까닭 「{k}」") for k, v in why.items()}
|
||||
if why := {k: v for k, v in why.items() if v}:
|
||||
out["까닭"] = why
|
||||
if isinstance(raw.get("사본"), dict): # 로직 사본(795-1) — 셈 · 저장 첫머리 settle 이 정함
|
||||
out["사본"] = raw["사본"]
|
||||
return imp.clean(raw, fx.clean(raw, out)) # 식 칸(792-1) · 가져온 식(793-2)
|
||||
|
||||
|
||||
@@ -371,7 +373,7 @@ def sheet(ans: dict, clean: dict, raw: Any, given: dict | None, trail: tuple) ->
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
whole = calc.labor.m01.loaded()[1]
|
||||
whole = calc.skc.over(clean, calc.labor.m01.loaded()[1]) # 사본(795-1) — 중간 · 표값도
|
||||
try:
|
||||
base = calc._base_of(clean, whole, refs.read_ref, trail)["줄"]
|
||||
bare = calc.per_unit(calc.run(_bare(clean), whole, given, trail=trail), clean)
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
"""M02 단가산출(SK) 로직 사본(PLAN 795-1 · 요구 655 · 656 · 요구서_655 0 · 1 장) — 서버 단독.
|
||||
|
||||
사본 = 저장 때 서버가 찍는 M01 닫힌 묶음(`M02_Item_Prices.collect` 다시 씀) — 문서 칸 `사본`:
|
||||
{판, 시점, 로직 [M01 줄], 단가판, 단가시점, 단가 [M01 줄], 빠진?}
|
||||
로직 = 바탕 로직 · 더한줄 로직 · 가져온 식 이음 · 하위 로직 · 찾기 표 · 그 밖 요소(요율 등)
|
||||
단가 = 자원 단가 줄(`RES` 노임 · 자재 · 기계 · 유가전력 · 환율) = 단가 기준 시점 묶음.
|
||||
셈 = M01 색인 위에 사본 줄을 얹은 `Copy`(키 · 원문번호 찾기만 사본 먼저 · M01 색인 안 고침) —
|
||||
`M02_Sangeun_Calc._base_of`(바탕 줄) · `run`(엔진 · 하위 로직 · 표 · 단가)이 `over` 로 부름 ·
|
||||
M01 에서 지워진 키 = 사본의 마지막 줄로 셈(표지 `지운키`) ·
|
||||
사본 없는 SK(옛 SK · 바탕 참조) = 옛 길.
|
||||
사본은 셈 · 저장 첫머리 `settle` 이 정함 — 저장본에 사본이 있으면 그것(화면이 보낸 것 버림) ·
|
||||
없으면 받은 사본(층 복사 · 새로 · 판 다시 셈) · 그것도 없으면 저장 때 새로 찍음.
|
||||
할일 칸 `사본할일`(화면 캐시 · [저장] 때 적용):
|
||||
단가갱신 = 단가 줄만 지금 M01 값(지워진 키 = 그대로) ·
|
||||
재생성 = 지금 M01 로 새로 찍음 — 덮개 · 식덮개 · 변수줄 · 상수 · 표값 · 까닭은 cid 열쇠라
|
||||
새 사본에 그대로 얹힘 · 못 얹은 칸 = 짝잃음.
|
||||
저장 때 = 사본에 없는 새 줄(더한줄 로직 더함 · 기초 바꿈)만 지금 M01 에서 더함.
|
||||
길(`M02_Sangeun_Router`): POST …/sangeun/copy/{price|regen} 몸 {문서, 입력값?} →
|
||||
{문서(사본할일), 전, 후, 바뀐자원 [{키, 이름, 전, 후, 지워짐?}] · 못얹은 [{cid, 까닭}]}
|
||||
(저장 없음).
|
||||
셈 답 덧칸 `사본` = {시점, 단가시점, M01새판, 지운키, 단가새값} · 같은 내용 지문 몫 = `same_part`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
KEY = "사본"
|
||||
JOB = "사본할일"
|
||||
PRICE, REGEN = "단가갱신", "재생성"
|
||||
RES = ("LB", "MT", "EQ", "MO", "FX") # 자원 단가 = 단가 기준 시점 묶음
|
||||
_MASTER = prices.mf.Master
|
||||
|
||||
|
||||
def whole_() -> Any:
|
||||
return prices.m01.loaded()[1]
|
||||
|
||||
|
||||
def _fp(rows: Any) -> str:
|
||||
raw = json.dumps(prices.to_json(rows), ensure_ascii=False, sort_keys=True)
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def has(doc: Any) -> bool:
|
||||
copy = doc.get(KEY) if isinstance(doc, dict) else None
|
||||
return isinstance(copy, dict) and isinstance(copy.get("로직"), list)
|
||||
|
||||
|
||||
# ── 셈 색인 ──────────────────────────────────────────────────────────
|
||||
class Copy:
|
||||
"""M01 색인 위 사본 줄 — `get` · `logic` · `value` · `label` = 엔진 `Master` 한 벌
|
||||
(사본 줄 먼저) · 그 밖 칸(자재 색인 · 찾기 답)은 속 M01 것을 읽음."""
|
||||
|
||||
get, label, value, logic = _MASTER.get, _MASTER.label, _MASTER.value, _MASTER.logic
|
||||
|
||||
def __init__(self, whole: Any, rows: list[dict], tag: Any = None) -> None:
|
||||
self._whole, self._tag, self.twice = whole, tag, whole.twice
|
||||
self.index, self.numbers = dict(whole.index), dict(whole.numbers)
|
||||
for row in rows:
|
||||
key = str(row.get("키"))
|
||||
tid = key[:2]
|
||||
self._own(self.index, whole.index, tid)[key] = row
|
||||
if row.get("원문번호"):
|
||||
self._own(self.numbers, whole.numbers, tid)[str(row["원문번호"])] = row
|
||||
|
||||
@staticmethod
|
||||
def _own(mine: dict, theirs: dict, tid: str) -> dict:
|
||||
if mine.get(tid) is theirs.get(tid):
|
||||
mine[tid] = dict(theirs.get(tid) or {})
|
||||
return mine[tid]
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._whole, name)
|
||||
|
||||
|
||||
def _rows(copy: dict) -> list[dict]:
|
||||
raw = json.dumps(prices.to_json([*copy["로직"], *(copy.get("단가") or [])]), ensure_ascii=False)
|
||||
return json.loads(raw, parse_float=Decimal, parse_int=Decimal)
|
||||
|
||||
|
||||
_CACHE: dict[tuple, Copy] = {}
|
||||
_SEEN: dict[tuple, dict] = {}
|
||||
_LIVE: list[Any] = [None] # 지금 M01 색인 — 바뀌면(저장 · 다시 읽음) 메모 비움(옛 색인 안 붙잡음)
|
||||
|
||||
|
||||
def _forget(whole: Any) -> None:
|
||||
while isinstance(whole, Copy):
|
||||
whole = whole._whole
|
||||
if whole is not _LIVE[0]:
|
||||
_CACHE.clear()
|
||||
_SEEN.clear()
|
||||
_LIVE[0] = whole
|
||||
|
||||
|
||||
def over(doc: Any, whole: Any) -> Any:
|
||||
"""사본 있는 문서 = 사본 색인 · 없으면 `whole` 그대로(옛 길)."""
|
||||
if not has(doc):
|
||||
return whole
|
||||
copy = doc[KEY]
|
||||
tag = (copy.get("판"), copy.get("단가판"))
|
||||
if isinstance(whole, Copy) and whole._tag == tag:
|
||||
return whole
|
||||
_forget(whole)
|
||||
got = _CACHE.get((id(whole), *tag))
|
||||
if got is None or got._whole is not whole:
|
||||
if len(_CACHE) > 32: # ponytail: 통째 비움 · 쓰는 SK 가 많아 느려지면 LRU 로
|
||||
_CACHE.clear()
|
||||
got = _CACHE[(id(whole), *tag)] = Copy(whole, _rows(copy), tag)
|
||||
return got
|
||||
|
||||
|
||||
# ── 찍기 · 단가 갱신 ──────────────────────────────────────────────────
|
||||
def _split(rows: list[dict]) -> dict[str, Any]:
|
||||
logic = [r for r in rows if str(r.get("키"))[:2] not in RES]
|
||||
cost = [r for r in rows if str(r.get("키"))[:2] in RES]
|
||||
when = _now()
|
||||
return {
|
||||
"판": _fp(logic),
|
||||
"시점": when,
|
||||
"로직": logic,
|
||||
"단가판": _fp(cost),
|
||||
"단가시점": when,
|
||||
"단가": cost,
|
||||
}
|
||||
|
||||
|
||||
def stamp(doc: dict) -> dict | None:
|
||||
"""지금 M01 로 새 사본 — 바탕 로직 · 더한줄 로직 · 고정 줄 이음 · 가져온 식 줄이 닿는
|
||||
닫힌 묶음 · 바탕 참조 · 닿을 것 없음 = None."""
|
||||
base = doc.get("바탕") or {}
|
||||
if "참조" in base:
|
||||
return None
|
||||
keys, links = reach(doc.get("더한줄"))
|
||||
keys = [str(base["로직"]), *keys] if base.get("로직") else keys
|
||||
extra = [row] if (row := imp.row_of(doc)) else []
|
||||
return snap(keys, links, extra)
|
||||
|
||||
|
||||
def reach(adds: Any) -> tuple[list[str], list[str]]:
|
||||
"""더한줄이 닿는 (로직 키, 요소 키) — 로직 줄 · 인력 · 재료 · 기계 요소 · 고정 줄 이음."""
|
||||
adds = [x for x in adds or [] if isinstance(x, dict)]
|
||||
keys = [str(x["로직"]) for x in adds if x.get("종류") == "로직" and x.get("로직")]
|
||||
links = [str(x["요소"]) for x in adds if x.get("종류") in sd.ADD_PART and x.get("요소")]
|
||||
links += [str(v) for x in adds for v in ((x.get("고정") or {}).get("이음") or {}).values() if v]
|
||||
return keys, links
|
||||
|
||||
|
||||
def snap(keys: list[str], links: list[str], extra: list[dict] | None = None) -> dict | None:
|
||||
"""키 · 요소 · 마스터 밖 줄이 닿는 M01 닫힌 묶음 → 사본(닿을 것 없음 = None)."""
|
||||
if not (keys or links or extra):
|
||||
return None
|
||||
got = prices.collect(keys, links, extra)
|
||||
lost = got.pop("_빠진", None)
|
||||
keys = [str(r.get("키")) for data in got.values() for r in data[prices.m01.items_key(data)]]
|
||||
whole = whole_() # 줄 = 셈 색인 것(파일 묶음과 같은 판)
|
||||
rows = [whole.index[k[:2]][k] for k in keys if k in whole.index.get(k[:2], {})]
|
||||
out = _split(prices.to_json(rows))
|
||||
return out | ({"빠진": lost} if lost else {})
|
||||
|
||||
|
||||
def refresh(copy: dict, whole: Any) -> tuple[dict, list[dict]]:
|
||||
"""[단가 갱신] — 단가 줄만 지금 M01 값 · (새 사본, 바뀐자원) · 지워진 키 = 마지막 값 그대로."""
|
||||
rows, changed = [], []
|
||||
for row in copy.get("단가") or []:
|
||||
key = str(row.get("키"))
|
||||
live = whole.index.get(key[:2], {}).get(key)
|
||||
if live is None:
|
||||
rows.append(row)
|
||||
changed.append({"키": key, "이름": row.get("이름"), "지워짐": True})
|
||||
continue
|
||||
new = prices.to_json(live)
|
||||
if _fp(new) != _fp(row):
|
||||
changed.append(
|
||||
{"키": key, "이름": new.get("이름"), "전": row.get("값"), "후": new.get("값")}
|
||||
)
|
||||
rows.append(new)
|
||||
return {**copy, "단가": rows, "단가판": _fp(rows), "단가시점": _now()}, changed
|
||||
|
||||
|
||||
def grow(copy: dict, fresh: dict | None) -> dict:
|
||||
"""저장 때 — 사본에 없는 키 줄만 새 사본(`fresh` = 지금 M01)에서 더함(있는 줄 = 그대로)."""
|
||||
have = {str(r.get("키")) for r in [*copy["로직"], *(copy.get("단가") or [])]}
|
||||
new = [
|
||||
r
|
||||
for r in [*(fresh or {}).get("로직", []), *(fresh or {}).get("단가", [])]
|
||||
if str(r.get("키")) not in have
|
||||
]
|
||||
if not new:
|
||||
return copy
|
||||
logic = [*copy["로직"], *(r for r in new if str(r.get("키"))[:2] not in RES)]
|
||||
cost = [*(copy.get("단가") or []), *(r for r in new if str(r.get("키"))[:2] in RES)]
|
||||
return {**copy, "로직": logic, "판": _fp(logic), "단가": cost, "단가판": _fp(cost)}
|
||||
|
||||
|
||||
def taken(raw: Any) -> dict | None:
|
||||
"""받은 사본(층 복사 · 새로) — 줄 꼴만 보고 판은 서버가 다시 셈."""
|
||||
if not has({KEY: raw}):
|
||||
return None
|
||||
logic = [r for r in raw["로직"] if isinstance(r, dict) and str(r.get("키"))[:2] not in RES]
|
||||
cost = [r for r in raw.get("단가") or [] if isinstance(r, dict) and str(r.get("키"))[:2] in RES]
|
||||
keep = {k: str(raw.get(k) or "") for k in ("시점", "단가시점")}
|
||||
return {**keep, "로직": logic, "판": _fp(logic), "단가": cost, "단가판": _fp(cost)}
|
||||
|
||||
|
||||
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:
|
||||
got = json.loads((store.root() / sd.KIND / f"{key}.json").read_bytes())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return got[KEY] if has(got) else None
|
||||
|
||||
|
||||
def settle(doc: Any, save: bool = False) -> Any:
|
||||
"""셈 · 저장 첫머리 — 서버가 정한 사본으로 갈음(할일 적용 · 저장 때 새 줄 더함 · 할일 칸 뗌)."""
|
||||
if not isinstance(doc, dict):
|
||||
return doc
|
||||
job = doc.get(JOB) # 모르는 할일 = 없음
|
||||
out = {k: v for k, v in doc.items() if k not in (KEY, JOB)}
|
||||
if "참조" in (doc.get("바탕") or {}):
|
||||
return out
|
||||
copy = _stored(doc.get("키")) or taken(doc.get(KEY))
|
||||
if job == REGEN or (copy is None and save):
|
||||
copy = stamp(out)
|
||||
elif job == PRICE and copy:
|
||||
copy = refresh(copy, whole_())[0]
|
||||
if save and copy:
|
||||
copy = grow(copy, stamp(out))
|
||||
return {**out, KEY: copy} if copy else out
|
||||
|
||||
|
||||
# ── 표지 ──────────────────────────────────────────────────────────────
|
||||
def state(copy: dict, whole: Any) -> dict[str, Any]:
|
||||
"""{M01새판, 지운키, 단가새값} — 사본 줄과 지금 M01 줄 대조(같은 M01 · 같은 판 = 메모)."""
|
||||
_forget(whole)
|
||||
tag = (id(whole), copy.get("판"), copy.get("단가판"))
|
||||
if tag in _SEEN and _SEEN[tag]["whole"] is whole:
|
||||
return _SEEN[tag]["답"]
|
||||
|
||||
def live(row: dict) -> Any:
|
||||
key = str(row.get("키"))
|
||||
return whole.index.get(key[:2], {}).get(key)
|
||||
|
||||
gone = [str(r.get("키")) for r in [*copy["로직"], *(copy.get("단가") or [])] if live(r) is None]
|
||||
logic = any(live(r) is None or _fp(live(r)) != _fp(r) for r in copy["로직"])
|
||||
cost = any(live(r) is not None and _fp(live(r)) != _fp(r) for r in copy.get("단가") or [])
|
||||
got = {"M01새판": logic, "지운키": gone, "단가새값": cost}
|
||||
_SEEN[tag] = {"whole": whole, "답": got}
|
||||
return got
|
||||
|
||||
|
||||
def mark(ans: dict, doc: dict) -> dict:
|
||||
"""셈 답 덧칸 `사본` — 사본 없는 문서 = 칸 없음."""
|
||||
if not has(doc):
|
||||
return ans
|
||||
copy = doc[KEY]
|
||||
head = {"시점": copy.get("시점"), "단가시점": copy.get("단가시점")}
|
||||
return {**ans, KEY: {**head, **state(copy, whole_())}}
|
||||
|
||||
|
||||
def same_part(doc: Any) -> str | None:
|
||||
"""같은 내용 지문 몫 — 지금 M01 과 같은 사본 = None(새로 찍을 문서와 같음) · 다르면 판."""
|
||||
if not has(doc):
|
||||
return None
|
||||
copy = doc[KEY]
|
||||
seen = state(copy, whole_())
|
||||
fresh = not (seen["M01새판"] or seen["지운키"] or seen["단가새값"])
|
||||
return None if fresh else f"{copy.get('판')}:{copy.get('단가판')}"
|
||||
|
||||
|
||||
# ── [단가 갱신] · [재생성] 미리 셈(저장 없음) ──────────────────────────
|
||||
def _money(ans: dict) -> dict | None:
|
||||
base = ans.get("기준")
|
||||
return {k: base.get(k) for k in ("노무비", "재료비", "경비", "계")} if base else None
|
||||
|
||||
|
||||
def _lost(ans: dict) -> dict[str, dict]:
|
||||
rows = [*(ans.get("짝잃음") or []), *((ans.get("시트") or {}).get("짝잃음") or [])]
|
||||
return {str(x.get("cid")): x for x in rows if isinstance(x, dict)}
|
||||
|
||||
|
||||
def preview(doc: Any, job: str, given: dict | None = None) -> dict[str, Any]:
|
||||
"""{문서(할일 얹음), 전, 후, 바뀐자원 · 못얹은} — 재생성은 단가갱신을 품음."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
if not isinstance(doc, dict):
|
||||
raise sd.DocError("단가산출 문서가 묶음 아님")
|
||||
old = doc.get(JOB)
|
||||
new = REGEN if REGEN in (old, job) else job
|
||||
after_doc = {**doc, JOB: new}
|
||||
before = calc.calc_doc(doc, given, False)
|
||||
after = calc.calc_doc(after_doc, given, False)
|
||||
out: dict[str, Any] = {"문서": after_doc, "전": _money(before), "후": _money(after)}
|
||||
if job == PRICE:
|
||||
copy = _stored(doc.get("키")) or taken(doc.get(KEY))
|
||||
out["바뀐자원"] = refresh(copy, whole_())[1] if copy else []
|
||||
else:
|
||||
was = _lost(before)
|
||||
out["못얹은"] = [
|
||||
{"cid": c, "까닭": x.get("까닭") or "새 사본에 그 줄 없음"}
|
||||
for c, x in _lost(after).items()
|
||||
if c not in was
|
||||
]
|
||||
return out
|
||||
@@ -5,6 +5,8 @@
|
||||
`/link` · `/to-base`(화면 캐시 문서 고침 · 저장 없음 · 층 읽기) ·
|
||||
불러오기 단산 블록 → 가져온 식(793-3b · `M02_Sangeun_FromHopyo`) `…/sangeun/from-hopyo`
|
||||
(만들 층 쓰기).
|
||||
로직 사본(795-1 · `M02_Sangeun_Copy`) `…/sangeun/copy/price` [단가 갱신] · `/regen` [재생성]
|
||||
(화면 캐시 문서에 할일 얹음 · 저장 없음 · 층 읽기).
|
||||
|
||||
저장 안 함 · 로그인 사용자 · 층 읽기 권한(`Router_Items._root`) · 화면은 최신 응답만.
|
||||
몸 = `문서`(저장 전 화면 문서) 또는 `키`(그 층 저장본) + `입력값`(열린입력 · 시험 값) ·
|
||||
@@ -23,6 +25,7 @@ from pydantic import BaseModel
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Formula as fx
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo
|
||||
@@ -188,3 +191,27 @@ def sangeun_imported_edit(
|
||||
raise HTTPException(status_code=404, detail="price · link · to-base")
|
||||
fn, *args = _EDIT[how](body)
|
||||
return _call(_root(session, layer, owner), _sd(fn), *args)
|
||||
|
||||
|
||||
class CopyBody(BaseModel):
|
||||
문서: dict[str, Any]
|
||||
입력값: dict[str, Any] | None = None
|
||||
|
||||
|
||||
_JOBS = {"price": skc.PRICE, "regen": skc.REGEN}
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/copy/{how}")
|
||||
def sangeun_copy(
|
||||
layer: Layer,
|
||||
how: str,
|
||||
body: CopyBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""로직 사본(795-1) — price [단가 갱신] · regen [재생성] · 답 = 할일 얹은 문서(화면 캐시 ·
|
||||
[저장] 때 서버가 적용) + 전 · 후 금액 + 바뀐자원 | 못얹은."""
|
||||
if how not in _JOBS:
|
||||
raise HTTPException(status_code=404, detail="price · regen")
|
||||
args = (body.문서, _JOBS[how], body.입력값)
|
||||
return _call(_root(session, layer, owner), _sd(skc.preview), *args)
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""795-1 SK 로직 사본 — 저장 때 M01 닫힌 묶음을 찍고 셈은 사본으로(임시 storage · M01 은 메모리 덮개).
|
||||
|
||||
① M01 로직 줄 바꿔도 사본 SK 금액 그대로 ② 노임 바꿔도 [단가 갱신] 전엔 그대로 · 누르면 따라감
|
||||
③ [재생성] 뒤 고친 칸 다시 얹힘 · 못 얹은 칸 알림 ④ M01 새 판 배지 ⑤ 지워진 키 = 마지막 값
|
||||
⑥ 옛 SK(사본 없음) = 옛 길 · 지문 그대로 ⑦ 가져온 식 SK 같은 규칙.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Library_Same as same
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
PCT = "GC001109" # 원/㎡ · 인력 둘 + 「노무비 * 3 / 100」
|
||||
MAN = "LB000002" # 보통인부
|
||||
|
||||
|
||||
def _live():
|
||||
return m01.loaded()[1]
|
||||
|
||||
|
||||
def _fake(monkeypatch, rows=(), drop=()):
|
||||
"""M01 바꿈 흉내 — 지금 색인 위에 바뀐 줄 · 지운 키(정본 파일 안 고침)."""
|
||||
files, live = m01.loaded()
|
||||
fake = skc.Copy(live, [copy.deepcopy(r) for r in rows])
|
||||
for key in drop:
|
||||
skc.Copy._own(fake.index, live.index, key[:2]).pop(key, None)
|
||||
monkeypatch.setattr(m01, "loaded", lambda: (files, fake))
|
||||
return fake
|
||||
|
||||
|
||||
def _sk(folder, **doc):
|
||||
with store.at(folder):
|
||||
body = {"이름": "sub2임시 사본", "단위": "㎡", "더한줄": [], **doc}
|
||||
return refs.create("sangeun", body)["이름"]
|
||||
|
||||
|
||||
def _doc(folder, key):
|
||||
with store.at(folder):
|
||||
return store.read("sangeun", key)["문서"]
|
||||
|
||||
|
||||
def _save(folder, key, doc):
|
||||
with store.at(folder):
|
||||
store.write("sangeun", key, store.read("sangeun", key)["판"], doc)
|
||||
|
||||
|
||||
def _calc(folder, doc):
|
||||
with store.at(folder):
|
||||
return calc.calc_doc(doc, None, False)
|
||||
|
||||
|
||||
def _sum(folder, key):
|
||||
return _calc(folder, _doc(folder, key))["기준"]["계"]
|
||||
|
||||
|
||||
def _cids(folder, key):
|
||||
return [x["cid"] for x in _calc(folder, _doc(folder, key))["줄"]]
|
||||
|
||||
|
||||
def _changed(row, at=0):
|
||||
row = copy.deepcopy(row)
|
||||
row["호표"][at]["수량"] = f"({row['호표'][at]['수량']}) * 2"
|
||||
return row
|
||||
|
||||
|
||||
def test_copy_stamped_and_logic_change_kept(world, monkeypatch):
|
||||
"""① · ④ 저장 때 사본(로직 · 단가 갈래) · M01 로직 바꿔도 금액 그대로 · 배지만."""
|
||||
me = world["personal"]
|
||||
key = _sk(me, 바탕={"로직": PCT})
|
||||
got = _doc(me, key)["사본"]
|
||||
assert PCT in {r["키"] for r in got["로직"]}
|
||||
assert got["단가"] and all(r["키"][:2] in skc.RES for r in got["단가"])
|
||||
assert not any(r["키"][:2] in skc.RES for r in got["로직"])
|
||||
first = _calc(me, _doc(me, key))
|
||||
assert first["사본"]["M01새판"] is False and first["사본"]["지운키"] == []
|
||||
was = first["기준"]["계"]
|
||||
plain = labor.run_line(_live(), PCT, {})["기준"]["계"]
|
||||
_fake(monkeypatch, [_changed(_live().logic(PCT))])
|
||||
assert labor.run_line(m01.loaded()[1], PCT, {})["기준"]["계"] != plain # M01 셈은 바뀜
|
||||
now = _calc(me, _doc(me, key))
|
||||
assert now["기준"]["계"] == was
|
||||
assert now["사본"]["M01새판"] is True
|
||||
|
||||
|
||||
def test_price_refresh_only_on_press(world, monkeypatch):
|
||||
"""② 노임 바꿈 → 그대로 · [단가 갱신] 미리 셈 · 저장 = 새 SK 와 같은 값 · 바뀐자원."""
|
||||
me = world["personal"]
|
||||
key = _sk(me, 바탕={"로직": PCT})
|
||||
was = _sum(me, key)
|
||||
lb = _cids(me, key)[0].split(":")[1].split("#")[0]
|
||||
row = copy.deepcopy(_live().get(lb))
|
||||
row["값"] = row["값"] * 2
|
||||
_fake(monkeypatch, [row])
|
||||
assert _sum(me, key) == was # 누르기 전 = 단가 기준 시점 값
|
||||
fresh = _sum(me, _sk(me, 바탕={"로직": PCT})) # 지금 M01 로 새로 찍은 SK
|
||||
assert fresh != was
|
||||
with store.at(me):
|
||||
got = skc.preview(_doc(me, key), skc.PRICE)
|
||||
assert got["전"]["계"] == was and got["후"]["계"] == fresh
|
||||
assert [x["키"] for x in got["바뀐자원"]] == [lb]
|
||||
assert got["문서"][skc.JOB] == skc.PRICE
|
||||
_save(me, key, got["문서"]) # [저장] = 서버가 할일 적용
|
||||
saved = _doc(me, key)
|
||||
assert skc.JOB not in saved and _sum(me, key) == fresh
|
||||
assert saved["사본"]["단가시점"] >= saved["사본"]["시점"]
|
||||
|
||||
|
||||
def test_regen_reapplies_covers(world, monkeypatch):
|
||||
"""③ [재생성] — 덮개 · 까닭 새 사본에 다시 얹힘 · 없어진 줄 덮개 = 못얹은 · 배지 꺼짐."""
|
||||
me = world["personal"]
|
||||
probe = _sk(me, 바탕={"로직": PCT})
|
||||
one, two = _cids(me, probe)[:2]
|
||||
covers = {one: {"qty": 0.5}, two: {"qty": 0.7}}
|
||||
key = _sk(me, 바탕={"로직": PCT}, 덮개=covers, 까닭={one: "현장 실측"})
|
||||
row = copy.deepcopy(_live().logic(PCT))
|
||||
row["호표"] = [x for n, x in enumerate(row["호표"]) if n != 1] # 둘째 줄 없앤 새 판
|
||||
_fake(monkeypatch, [row])
|
||||
with store.at(me):
|
||||
got = skc.preview(_doc(me, key), skc.REGEN)
|
||||
assert [x["cid"] for x in got["못얹은"]] == [two]
|
||||
assert got["전"]["계"] != got["후"]["계"]
|
||||
_save(me, key, got["문서"])
|
||||
doc = _doc(me, key)
|
||||
assert doc["덮개"][one]["qty"] == 0.5 and doc["까닭"][one] == "현장 실측"
|
||||
now = _calc(me, doc)
|
||||
assert now["줄"][0]["덮개"] == ["qty"]
|
||||
assert now["사본"]["M01새판"] is False
|
||||
assert now["기준"]["계"] == got["후"]["계"]
|
||||
|
||||
|
||||
def test_deleted_keys_keep_last_value(world, monkeypatch):
|
||||
"""⑤ 사본이 부르는 M01 키(바탕 로직 · 노임)가 지워져도 마지막 값 · 지운키 표지."""
|
||||
me = world["personal"]
|
||||
key = _sk(me, 바탕={"로직": PCT})
|
||||
was = _sum(me, key)
|
||||
lb = _cids(me, key)[0].split(":")[1].split("#")[0]
|
||||
_fake(monkeypatch, drop=[PCT, lb])
|
||||
with pytest.raises(Exception):
|
||||
m01.loaded()[1].logic(PCT)
|
||||
now = _calc(me, _doc(me, key))
|
||||
assert now["기준"]["계"] == was
|
||||
assert {PCT, lb} <= set(now["사본"]["지운키"])
|
||||
with store.at(me):
|
||||
got = skc.preview(_doc(me, key), skc.PRICE)
|
||||
assert {"키": lb, "이름": got["바뀐자원"][0]["이름"], "지워짐": True} in got["바뀐자원"]
|
||||
assert got["후"]["계"] == was
|
||||
|
||||
|
||||
def test_old_sk_without_copy_old_path(world, monkeypatch):
|
||||
"""⑥ 사본 없는 옛 SK = M01 지금 값(옛 길) · 같은 내용 지문 = 새 사본 SK 와 같음."""
|
||||
me = world["personal"]
|
||||
key = _sk(me, 바탕={"로직": PCT})
|
||||
path = me / "sangeun" / f"{key}.json"
|
||||
raw = json.loads(path.read_bytes())
|
||||
old = {k: v for k, v in raw.items() if k != "사본"}
|
||||
path.write_text(json.dumps(old, ensure_ascii=False), encoding="utf-8")
|
||||
fresh = _sk(me, 바탕={"로직": PCT})
|
||||
assert _sum(me, key) == _sum(me, fresh)
|
||||
assert "사본" not in _calc(me, _doc(me, key))
|
||||
with store.at(me):
|
||||
assert same.Same().finger("sangeun", old) == same.Same().finger("sangeun", _doc(me, fresh))
|
||||
_fake(monkeypatch, [_changed(_live().logic(PCT))])
|
||||
moved = labor.run_line(m01.loaded()[1], PCT, {})["기준"]["계"]
|
||||
assert _sum(me, key) == pytest.approx(moved, abs=1) # 옛 길 = 따라감
|
||||
assert _sum(me, fresh) != _sum(me, key) # 사본 SK = 그대로
|
||||
with store.at(me): # 판 다른 두 SK = 지문 다름(오판 막음)
|
||||
assert same.Same().finger("sangeun", _doc(me, key)) != same.Same().finger(
|
||||
"sangeun", _doc(me, fresh)
|
||||
)
|
||||
|
||||
|
||||
def _imported_row():
|
||||
man = {"종류": "인력", "이름": "보통인부", "단위": "인", "수량": "0.5", "비목": "노무비"}
|
||||
mat = {"종류": "재료", "이름": "각재", "단위": "㎥", "수량": "0.01", "비목": "재료비"}
|
||||
return {
|
||||
"이름": "sub2임시 가져온",
|
||||
"결과단위": "원/㎡",
|
||||
"호표": [{**man, "단가": 165545, "이음": MAN}, {**mat, "단가": 500661}],
|
||||
}
|
||||
|
||||
|
||||
def test_imported_same_rule(world, monkeypatch):
|
||||
"""⑦ 가져온 식 SK(단가기준 우리) — 이음 노임 바꿔도 그대로 · [단가 갱신] 뒤 따라감."""
|
||||
me = world["personal"]
|
||||
with store.at(me):
|
||||
key = imp.create(_imported_row(), None, "sub2임시 가져온")["이름"]
|
||||
doc = _doc(me, key)
|
||||
assert MAN in {r["키"] for r in doc["사본"]["단가"]}
|
||||
with store.at(me):
|
||||
ours = imp.to_price(doc, "우리")["문서"]
|
||||
_save(me, key, ours)
|
||||
was = _sum(me, key)
|
||||
row = copy.deepcopy(_live().get(MAN))
|
||||
row["값"] = row["값"] * 2
|
||||
_fake(monkeypatch, [row])
|
||||
assert _sum(me, key) == was
|
||||
with store.at(me):
|
||||
got = skc.preview(_doc(me, key), skc.PRICE)
|
||||
assert [x["키"] for x in got["바뀐자원"]] == [MAN]
|
||||
assert got["후"]["노무비"] == pytest.approx(got["전"]["노무비"] * 2, abs=1)
|
||||
@@ -0,0 +1,186 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""795-2 일위대가 · 사전조합(SC) 로직 사본 — 임시 storage · M01 은 메모리 덮개(정본 안 고침).
|
||||
|
||||
① 저장 때 사본 · M01 로직 바꿔도 일위대가 · SC 금액 그대로 · 배지 ② 노임 바꿔도 [단가 갱신] 전엔
|
||||
그대로 · 누르면 따라감 ③ [재생성] 뒤 줄 덮개 다시 얹힘 · 못 얹은 칸 알림 ④ 층 복사 · 열 복사 =
|
||||
사본 그대로 ⑤ TS 짝(Labor_Calc.ts settle) 같은 금액 ⑥ 사본 없는 옛 문서 = 옛 길.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from resources.tester.test_795_1_sk_copy import _changed, _fake, _live
|
||||
from resources.tester.test_m02_labor_calc_mirror import _same, _ts
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
PCT = "GC001109" # 원/㎡ · 인력 둘(LB000036 · LB000002) + 「노무비 * 3 / 100」
|
||||
LB = "LB000036"
|
||||
COVER = {"덮개": {f"labor:{LB}#0": {"qty": 0.5}}}
|
||||
COL = "sub2임시"
|
||||
|
||||
|
||||
def _sc(folder, lines):
|
||||
with store.at(folder):
|
||||
body = {"이름": "sub2임시 조합", "단위": "㎡", "담은로직": lines}
|
||||
return refs.create("precombo", body)["이름"]
|
||||
|
||||
|
||||
def _read(folder, kind, key):
|
||||
with store.at(folder):
|
||||
return store.read(kind, key)["문서"]
|
||||
|
||||
|
||||
def _write(folder, kind, key, doc):
|
||||
with store.at(folder):
|
||||
have = (
|
||||
store.read(kind, key)["판"] if (store.root() / kind / f"{key}.json").is_file() else ""
|
||||
)
|
||||
store.write(kind, key, have, doc)
|
||||
|
||||
|
||||
def _labor(folder, lines):
|
||||
_write(folder, "labor", COL, {"열": COL, "담은로직": lines, "자재": []})
|
||||
return _read(folder, "labor", COL)
|
||||
|
||||
|
||||
def _calc(folder, key, doc=None):
|
||||
"""화면 셈 길(`Labor_Router._calc`) — 열 = 일위대가 열 · SC 키."""
|
||||
with store.at(folder):
|
||||
kind = "precombo" if key.startswith("SC-") else "labor"
|
||||
got = labor.clean({**(doc or store.read(kind, key)["문서"]), "열": key})
|
||||
return lc.mark(labor.calc(got, {}), lc.settle(got))
|
||||
|
||||
|
||||
def _unit(folder, key):
|
||||
"""SC 1 단위 값(쓰는 쪽 `Expand` · 안 줄 수량 1)."""
|
||||
ref = {"층": "personal", "주인": "7/42", "키": key}
|
||||
with store.at(folder):
|
||||
lines = refs.read_ref(ref)["담은로직"]
|
||||
given = {f"{refs.ref_id(ref)}#{i}": 1 for i in range(len(lines))}
|
||||
return refs.Expand(m01.loaded()[1], given=given)(ref)["기준"]["계"]
|
||||
|
||||
|
||||
def _double(key):
|
||||
row = copy.deepcopy(_live().get(key))
|
||||
row["값"] = row["값"] * 2
|
||||
return row
|
||||
|
||||
|
||||
def test_copy_stamped_logic_change_kept(world, monkeypatch):
|
||||
"""① 일위대가 · SC 저장 때 사본 · M01 로직 바꿔도 금액 그대로 · M01 새 판 배지."""
|
||||
me = world["personal"]
|
||||
sc = _sc(me, [{"로직": PCT, "수량": {"손": 1}}])
|
||||
doc = _labor(me, [{"로직": PCT, "수량": {"손": 2}}])
|
||||
for got in (doc, _read(me, "precombo", sc)):
|
||||
assert PCT in {r["키"] for r in got["사본"]["로직"]}
|
||||
assert LB in {r["키"] for r in got["사본"]["단가"]}
|
||||
was = (_calc(me, COL)["반영합"]["계"], _calc(me, sc)["기준합"]["계"], _unit(me, sc))
|
||||
assert _calc(me, COL)["사본"]["M01새판"] is False
|
||||
_fake(monkeypatch, [_changed(_live().logic(PCT))])
|
||||
assert labor.run_line(m01.loaded()[1], PCT, {})["기준"]["계"] != was[2] # M01 셈은 바뀜
|
||||
now = (_calc(me, COL)["반영합"]["계"], _calc(me, sc)["기준합"]["계"], _unit(me, sc))
|
||||
assert now == was
|
||||
assert _calc(me, COL)["사본"]["M01새판"] is True
|
||||
assert labor.server(_read(me, "labor", COL))["계산값"]["반영합"]["계"] == was[0] # 다시 저장
|
||||
|
||||
|
||||
def test_price_refresh_only_on_press(world, monkeypatch):
|
||||
"""② 노임 바꿈 → 그대로 · [단가 갱신] 미리 셈 = 새로 찍은 문서 값 · [저장] 때 적용."""
|
||||
me = world["personal"]
|
||||
sc = _sc(me, [{"로직": PCT, "수량": {"손": 1}}])
|
||||
was = _calc(me, sc)["기준합"]["계"]
|
||||
_fake(monkeypatch, [_double(LB)])
|
||||
assert _calc(me, sc)["기준합"]["계"] == was and _unit(me, sc) == was
|
||||
fresh = _calc(me, _sc(me, [{"로직": PCT, "수량": {"손": 1}}]))["기준합"]["계"]
|
||||
assert fresh != was
|
||||
with store.at(me):
|
||||
got = lc.preview(labor.clean({**_read(me, "precombo", sc), "열": sc}), lc.PRICE)
|
||||
assert got["전"]["기준합"]["계"] == was and got["후"]["기준합"]["계"] == fresh
|
||||
assert [x["키"] for x in got["바뀐자원"]] == [LB]
|
||||
_write(me, "precombo", sc, {**_read(me, "precombo", sc), lc.JOB: got["doc"][lc.JOB]})
|
||||
saved = _read(me, "precombo", sc)
|
||||
assert lc.JOB not in saved and _unit(me, sc) == fresh
|
||||
assert _calc(me, sc)["사본"]["단가새값"] is False
|
||||
|
||||
|
||||
def test_regen_reapplies_line_cover(world, monkeypatch):
|
||||
"""③ [재생성] — 줄 덮개(cid) 새 사본에 다시 얹힘 · 없어진 줄 덮개 = 못얹은 · 배지 꺼짐."""
|
||||
me = world["personal"]
|
||||
two = {"덮개": {f"labor:{LB}#0": {"qty": 0.5}, "labor:LB000002#0": {"qty": 0.7}}}
|
||||
_labor(me, [{"로직": PCT, "수량": {"손": 1}, "패치": two}])
|
||||
row = copy.deepcopy(_live().logic(PCT))
|
||||
row["호표"] = [x for n, x in enumerate(row["호표"]) if n != 1] # 둘째 줄 없앤 새 판
|
||||
_fake(monkeypatch, [row])
|
||||
assert not _calc(me, COL)["줄"][0].get("짝잃음") # 사본 = 옛 판 그대로
|
||||
with store.at(me):
|
||||
got = lc.preview(labor.clean(_read(me, "labor", COL)), lc.REGEN)
|
||||
assert [x["cid"] for x in got["못얹은"]] == ["labor:LB000002#0"]
|
||||
assert got["전"]["기준합"]["계"] != got["후"]["기준합"]["계"]
|
||||
_write(me, "labor", COL, got["doc"])
|
||||
doc = _read(me, "labor", COL)
|
||||
assert doc["담은로직"][0]["패치"]["덮개"][f"labor:{LB}#0"] == {"qty": 0.5}
|
||||
now = _calc(me, COL)
|
||||
assert now["사본"]["M01새판"] is False and now["기준합"]["계"] == got["후"]["기준합"]["계"]
|
||||
|
||||
|
||||
def test_layer_and_column_copy_keep_copy(world, monkeypatch):
|
||||
"""④ 층 사이 복사(회사 → 개인) · 열 복사 = 원본 사본 그대로(지금 M01 로 다시 안 찍음)."""
|
||||
co, me = world["company"], world["personal"]
|
||||
src = _sc(co, [{"로직": PCT, "수량": {"손": 1}, "패치": COVER}])
|
||||
first = _read(co, "precombo", src)["사본"]
|
||||
old = _labor(me, [{"로직": PCT, "수량": {"손": 2}}])["사본"] # 받는 열 = 옛 M01 사본
|
||||
_fake(monkeypatch, [_double(LB), _changed(_live().logic(PCT))])
|
||||
ref = {"층": "company", "주인": "7", "키": src}
|
||||
with store.at(me):
|
||||
got = refs.create(
|
||||
"precombo", refs.carry(refs.origin_mark(refs.read_ref(ref), ref), ("company", "7"))
|
||||
)
|
||||
mine = _read(me, "precombo", got["이름"])["사본"]
|
||||
assert (mine["판"], mine["단가판"]) == (first["판"], first["단가판"])
|
||||
_write(me, "labor", "sub2임시2", {"열": "sub2임시2", "담은로직": [{"로직": PCT}], "자재": []})
|
||||
other = _read(me, "labor", "sub2임시2")["사본"] # 원본 열 = 바뀐 M01 사본
|
||||
assert other["판"] != old["판"]
|
||||
with store.at(me): # 열 복사(copy_from) = 받은 사본 먼저
|
||||
store.copy_from("labor", COL, "sub2임시2")
|
||||
assert _read(me, "labor", COL)["사본"]["판"] == other["판"]
|
||||
|
||||
|
||||
def test_ts_mirror_same_amount(world, monkeypatch):
|
||||
"""⑤ 사본 셈 답 → TS 짝 settle 같은 답 · M01 바뀐 뒤에도 같은 금액."""
|
||||
me = world["personal"]
|
||||
doc = _labor(me, [{"로직": PCT, "수량": None}])
|
||||
with store.at(me):
|
||||
before = labor.calc(labor.clean(doc), {})
|
||||
_fake(monkeypatch, [_double(LB), _changed(_live().logic(PCT))])
|
||||
with store.at(me):
|
||||
clean = labor.clean(_read(me, "labor", COL))
|
||||
answer = labor.calc(clean, {})
|
||||
assert answer["기준합"] == before["기준합"]
|
||||
key = answer["줄"][0]["알약"]
|
||||
for values in ({}, {key: 3}):
|
||||
_same(labor.settle(answer, clean, values), _ts(answer, clean, values))
|
||||
|
||||
|
||||
def test_old_doc_without_copy_old_path(world, monkeypatch):
|
||||
"""⑥ 사본 없는 옛 일위대가 = 셈은 지금 M01(옛 길) · 다시 저장하면 그때 사본."""
|
||||
me = world["personal"]
|
||||
_labor(me, [{"로직": PCT, "수량": {"손": 1}}])
|
||||
path = me / "labor" / f"{COL}.json"
|
||||
raw = {k: v for k, v in json.loads(path.read_bytes()).items() if k != "사본"}
|
||||
path.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8")
|
||||
assert "사본" not in _calc(me, COL)
|
||||
_fake(monkeypatch, [_double(LB)])
|
||||
moved = labor.run_line(m01.loaded()[1], PCT, {})["기준"]["계"]
|
||||
assert _calc(me, COL)["기준합"]["계"] == pytest.approx(moved, abs=1)
|
||||
with store.at(me):
|
||||
assert skc.has(labor.server(raw))
|
||||
@@ -0,0 +1,101 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""795-3 옛 SK · SC · 일위대가 → 로직 사본 옮기기 — 임시 storage 에서만.
|
||||
|
||||
① 보기만 = 안 씀 · 전후 같음 ② 쓰기 = 사본 칸만 더함 · 다른 칸 그대로 · 금액 같음 · 백업 · 되돌림
|
||||
③ 전후 다르면 안 씀(목록) ④ 프로젝트 층 일위대가 = 세기 · 대조만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
from M02_MasterTemplete import M02_Copy_Migrate as mig
|
||||
from M02_MasterTemplete import M02_Labor_Copy as lc
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.test_795_1_sk_copy import _fake, _live
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
PCT, LB = "GC001109", "LB000036"
|
||||
LINE = {"로직": PCT, "수량": {"손": 1}}
|
||||
|
||||
|
||||
def _old(path):
|
||||
"""사본 칸 뗀 옛 문서로 되씀 → 옛 내용."""
|
||||
raw = {k: v for k, v in json.loads(path.read_bytes()).items() if k != skc.KEY}
|
||||
path.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8")
|
||||
return raw
|
||||
|
||||
|
||||
def _make(me):
|
||||
with store.at(me):
|
||||
sk = refs.create(
|
||||
"sangeun", {"이름": "sub2임시", "단위": "㎡", "바탕": {"로직": PCT}, "더한줄": []}
|
||||
)["이름"]
|
||||
sc = refs.create("precombo", {"이름": "sub2임시", "단위": "㎡", "담은로직": [LINE]})["이름"]
|
||||
store.write("labor", "sub2임시", "", {"열": "sub2임시", "담은로직": [LINE], "자재": []})
|
||||
paths = [me / "sangeun" / f"{sk}.json", me / "precombo" / f"{sc}.json"]
|
||||
paths.append(me / "labor" / "sub2임시.json")
|
||||
return {p: _old(p) for p in paths}
|
||||
|
||||
|
||||
def _mine(rows, olds):
|
||||
keys = {p.stem for p in olds}
|
||||
return {r["키"]: r for r in rows if r["어디"].startswith("개인") and r["키"] in keys}
|
||||
|
||||
|
||||
def test_dry_then_apply_then_restore(world, tmp_path):
|
||||
"""① · ② 보기만 → 쓰기(사본만 더함 · 금액 같음) → 되돌림 = 처음 파일."""
|
||||
olds = _make(world["personal"])
|
||||
rows = _mine(mig.migrate(), olds)
|
||||
assert {r["상태"] for r in rows.values()} == {"같음"} and len(rows) == 3
|
||||
assert not any(r["씀"] for r in rows.values())
|
||||
assert all(json.loads(p.read_bytes()) == raw for p, raw in olds.items())
|
||||
back = tmp_path / "back"
|
||||
rows = _mine(mig.migrate(True, back), olds)
|
||||
assert all(r["씀"] for r in rows.values())
|
||||
for path, raw in olds.items():
|
||||
now = json.loads(path.read_bytes())
|
||||
assert skc.has(now) and {k: v for k, v in now.items() if k != skc.KEY} == raw
|
||||
again = _mine(mig.migrate(), olds) # 다시 돌림 = 이미 있음
|
||||
assert {r["상태"] for r in again.values()} == {"있음"}
|
||||
assert {r["전"]["계"] for r in rows.values() if r["종류"] == "sangeun"} == {
|
||||
r["후"]["계"] for r in rows.values() if r["종류"] == "sangeun"
|
||||
}
|
||||
mig.restore(back)
|
||||
assert all(json.loads(p.read_bytes()) == raw for p, raw in olds.items())
|
||||
|
||||
|
||||
def test_different_amount_not_written(world, monkeypatch, tmp_path):
|
||||
"""③ 찍은 사본 금액이 지금과 다르면(노임 다른 판) 안 씀 · 까닭."""
|
||||
olds = _make(world["personal"])
|
||||
real = lc.stamp
|
||||
|
||||
def stale(doc): # 노임 두 배 판으로 찍힌 사본 흉내
|
||||
with monkeypatch.context() as m:
|
||||
row = copy.deepcopy(_live().get(LB))
|
||||
row["값"] = row["값"] * 2
|
||||
_fake(m, [row])
|
||||
return real(doc)
|
||||
|
||||
monkeypatch.setattr(lc, "stamp", stale)
|
||||
rows = _mine(mig.migrate(True, tmp_path / "back"), olds)
|
||||
bad = [r for r in rows.values() if r["종류"] != "sangeun"]
|
||||
assert {r["상태"] for r in bad} == {"다름"} and not any(r["씀"] for r in bad)
|
||||
assert all(not skc.has(json.loads(p.read_bytes())) for p in olds if p.parent.name != "sangeun")
|
||||
|
||||
|
||||
def test_project_labor_count_only(world, tmp_path):
|
||||
"""④ 프로젝트 층 일위대가 = 대조만 · 안 씀."""
|
||||
folder = layers.storage_root() / "7" / "42" / "p1" / "templates" / "labor"
|
||||
folder.mkdir(parents=True)
|
||||
path = folder / "x.json"
|
||||
raw = {"열": "x", "담은로직": [LINE], "자재": []}
|
||||
path.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8")
|
||||
rows = [r for r in mig.migrate(True, tmp_path / "back") if r["어디"].startswith("프로젝트")]
|
||||
assert [(r["키"], r["상태"], r["씀"]) for r in rows] == [("x", "같음", False)]
|
||||
assert json.loads(path.read_bytes()) == raw
|
||||
assert mig.table(rows) == {"프로젝트 labor": {"같음": 1}}
|
||||
Reference in New Issue
Block a user