feat(795-3): 옛 SK · SC · 일위대가 사본 옮기기 도구 — 전후 금액 대조 · 같음만 씀 · 백업 · 되돌림
- 새 M02_Copy_Migrate — 사본 없는 마스터 · 회사 · 개인 층 문서에 지금 M01 사본 찍어 대조(SK 기준 · SC · 일위대가 기준합 · 반영합) - 같음 = 사본 칸만 더해 씀 · 다름 · 못 셈 = 안 씀(목록) · 프로젝트 층 일위대가 = 세기 · 대조만(795-4) - --apply 는 --backup 폴더 필수 · --restore 로 다시 덮음 · 기본 = 보기만(표) - 실제 storage 보기만 결과 — 다름 0 · 층 문서 같음 41 · 필요없음 6 · 프로젝트 같음 70 · 못셈 14(옛 SP 참조) · 옮기지 않음 - 시험 test_795_3_migrate 3(임시 storage) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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,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