"""옛 SK · SC · 일위대가 · B09 사본 → 수식 문서 사본 찍어 넣기 — 한 번 돌리는 도구(PLAN 802-4a 3). 대상 = 마스터 · 회사 · 개인 층 문서(`sangeun` · `precombo` · `labor`) · B09 사본(구조물 · 서고). 층 문서 — 전 = 옛 셈 길(`M02_Doc_Sk.OLD` · 로직 사본 엔진) · 후 = 수식 사본 문서 셈: SK = 기준(노 · 재 · 경 · 계) · SC · 일위대가 = `Labor_Doc.calc` 기준합 · 반영합(원 단위 · 반올림 6 자리). 같음 = 파일에 `수식` 칸만 더해 씀(그 밖 칸 그대로) · 다름 · 못 셈 = 안 씀(목록 · 까닭) · 가져온 식 SK(807-1) = 가져온 줄 문서(`M02_Doc_Import`) · 전 = 옛 엔진 · 후 = 문서 셈 · 바탕 참조 SK · 로직 사본 없는 문서 = 필요없음 · 이미 있음 = 있음. B09 사본 — `수식.json`(`M02_Doc_B09.build` · 만든 때 `단가.json` 줄 기준) 더함 · 견적 셈은 그대로 (`prices.calc` 전후 같음) · 문서 대조 = 일위대가 로직 줄마다 옛 엔진(`run_line`) 대 문서 셈 (M01 규칙 · 같은 사본 값) — 다르면 목록. 되돌릴 길 = 쓰기 전 파일 그대로 `--backup` 폴더(층 자리 그대로) · `--restore` = 다시 덮음 (B09 = 더한 `수식.json` 을 지움). ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Doc_Migrate # 보기만(표 · 목록) ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Doc_Migrate --apply --backup tmp/802_4a ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Doc_Migrate --restore tmp/802_4a --list 목록 파일(기본 `docs/raw/요구_1104/_목록_802-4a_M02대조.xlsx`) """ 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_Copy_Migrate as old from M02_MasterTemplete import M02_Doc_B09 as doc_b09 from M02_MasterTemplete import M02_Doc_Copy as dc from M02_MasterTemplete import M02_Doc_Sk as dsk from M02_MasterTemplete import M02_Sangeun_Copy as skc LIST = Path(__file__).resolve().parents[1] / "docs/raw/요구_1104/_목록_802-4a_M02대조.xlsx" HEAD = ("어디", "종류", "키", "이름", "상태", "전", "후", "까닭") def _money(kind: str, doc: dict) -> Any: from M02_MasterTemplete import M02_Labor_Copy as lc from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Sangeun_Calc as calc if kind == "sangeun": got = calc.calc_doc(doc, None, False) return old._round(got.get("기준")), got.get("수식셈") lc._LAST[0] = (None, None) got = labor.calc(lc.settle(doc), None if kind == "labor" else {}) return old._round({k: got.get(k) for k in ("기준합", "반영합")}), None def _with_old(fn, *args): token = dsk.OLD.set(True) try: return fn(*args) finally: dsk.OLD.reset(token) def formula_of(kind: str, doc: dict) -> dict | None: """그 문서에 찍을 수식 사본(저장 첫머리와 같은 길 · 저장 없음).""" from M02_MasterTemplete import M02_Labor_Copy as lc from M02_MasterTemplete import M02_Sangeun_Doc as sd if kind != "sangeun": return lc.settle(doc).get(dc.KEY) if not dsk.own(doc): return None clean, whole = None, None settled = skc.settle(doc) whole = skc.over(settled, skc.whole_()) clean = sd.clean(settled, None, whole) return dsk.settle(clean, whole, None, doc).get(dc.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()) head = {"이름": doc.get("이름") or ""} if dc.has(doc): return {**head, "상태": "있음"} try: with store.at(path.parent.parent): copy = formula_of(kind, doc) if copy is None: return {**head, "상태": "필요없음", "까닭": "로직 사본 없음(바탕 참조 · 손 줄만)"} before = _with_old(_money, kind, doc)[0] after, mark = _money(kind, {**doc, dc.KEY: copy}) except Exception as e: # noqa: BLE001 — 못 셈 = 목록(통째로 멈추지 않음) return {**head, "상태": "못셈", "까닭": str(e) or type(e).__name__} row = {**head, "전": before, "후": after} if before == after: return {**row, "상태": "같음", "수식": copy} why = (mark or {}).get("까닭") or "전후 금액 다름 — 안 옮김" if kind == "sangeun" and not (doc.get("바탕") or doc.get("더한줄") or doc.get("가져온")): why = "빈 SK(바탕 · 줄 없음) — 옛 0 · 새 빈 문서 「—」(요구서_664 0 장 · 브레인 802-4a ③)" return {**row, "상태": "다름(빈 문서)", "까닭": why, "수식": copy} return {**row, "상태": "다름", "까닭": why} def b09_check(folder: Path) -> dict[str, Any]: """B09 사본 하나 — {상태: 있음 | 필요없음 | 같음 | 다름 | 못셈, 전, 후, 수식?, 까닭?}.""" from M02_MasterTemplete import M02_Item_Prices as prices from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete.M02_Item_Source import read_json if (folder / doc_b09.FILE).is_file(): return {"상태": "있음"} own = {p.name: read_json(p) for p in folder.glob("*.json")} bundle = own.get("단가.json") if not isinstance(bundle, dict) or not doc_b09.logic_keys(bundle): return {"상태": "필요없음", "까닭": "로직 줄 없음"} try: before = old._copy_money(own) made = doc_b09.build(bundle, own.get("일위대가.json")) after = old._copy_money({**own, doc_b09.FILE: made}) whole, copy = prices.load_prices(bundle), {"부름": made["문서"]} lines = ((own.get("일위대가.json") or {}).get("줄") or {}).get("담은로직") or [] bad = [] for one in [x for x in lines if isinstance(x, dict) and x.get("로직")]: code = str(one["로직"]) was = labor.run_line( whole, code, one.get("입력값"), one.get("켜쌓기율"), one.get("고름") ) if was.get("기준") is None or code not in made["문서"]: continue doc = dc.line_doc(made["문서"][code], one.get("켜쌓기율"), one.get("고름")) got = dc.money(dc.run(doc, copy, dc.given_of(whole, code, one.get("입력값")), whole)) if old._round(got) != old._round(was["기준"]): bad.append(f"{code} 옛 {old._round(was['기준'])} · 문서 {old._round(got)}") except Exception as e: # noqa: BLE001 return {"상태": "못셈", "까닭": str(e) or type(e).__name__} row = {"전": before, "후": after, "수식": made} if before != after: return {**row, "상태": "다름", "까닭": "견적 셈이 바뀜 — 안 옮김"} if bad: return {**row, "상태": "다름", "까닭": "문서 대조 다름 — " + " / ".join(bad[:5])} return {**row, "상태": "같음", "까닭": f"문서 {len(made['문서'])} · 견적 셈 그대로"} def migrate(apply: bool = False, backup: Path | None = None) -> list[dict[str, Any]]: """모든 층 · B09 사본 — {어디, 종류, 키, 상태, 씀, …} · `apply` = 같음(빈 문서 포함)만 씀.""" if apply and backup is None: raise ValueError("--apply 는 --backup 폴더가 있어야 함") out = [] for where_, kind, path, project in old._files(): if project: continue # 프로젝트 층 일위대가 = B09 몫(옛 길 · 795-4) got = check(kind, path) copy = got.pop("수식", None) row = {"어디": where_, "종류": kind, "키": path.stem, **got, "씀": False} if apply and copy is not None: keep = old._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()), dc.KEY: copy}) row["씀"] = True out.append(row) for where_, kind, folder in old._b09_files(): got = b09_check(folder) made = got.pop("수식", None) row = {"어디": where_, "종류": kind, "키": folder.name, **got, "씀": False} if apply and made is not None and got["상태"] == "같음": note = old._backup_path(folder / doc_b09.FILE, backup).with_suffix(".added") note.parent.mkdir(parents=True, exist_ok=True) note.write_text("802-4a 이 더함 — --restore 가 지움", encoding="utf-8") atomic_write_json(folder / doc_b09.FILE, made) row["씀"] = True out.append(row) return out def restore(backup: Path) -> list[str]: """`--backup` 폴더 파일을 제자리로 · 더한 `수식.json` 지움 — 손댄 파일 목록.""" done = old.restore(backup) for name, anchor in old._anchors().items(): for note in sorted((backup / name).rglob("*.added")): target = (anchor / note.relative_to(backup / name)).with_suffix(".json") target.unlink(missing_ok=True) done.append(f"지움 {target}") return done def table(rows: list[dict[str, Any]]) -> dict[str, dict[str, int]]: count: Counter = Counter((r["어디"].split(" ")[0], r["종류"], r["상태"]) for r in rows) out: dict[str, dict[str, int]] = {} for (layer, kind, state), n in sorted(count.items()): out.setdefault(f"{layer} {kind}", {})[state] = n return out def write_list(rows: list[dict[str, Any]], path: Path = LIST) -> Path: from openpyxl import Workbook book = Workbook() sheet = book.active sheet.title = "요약" sheet.append(("층 · 종류", "상태", "개수")) for group, states in table(rows).items(): for state, n in states.items(): sheet.append((group, state, n)) rows_sheet = book.create_sheet("목록") rows_sheet.append(HEAD) for r in rows: cells = [r.get(k) for k in HEAD] plain = (str, int, float, type(None)) rows_sheet.append( [c if isinstance(c, plain) else json.dumps(c, ensure_ascii=False) for c in cells] ) path.parent.mkdir(parents=True, exist_ok=True) book.save(path) return path 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)) if "--list" in args or "--apply" not in args: to = Path(args[args.index("--list") + 1]) if "--list" in args else LIST print("목록", write_list(rows, to))