Files
Aislo/M02_MasterTemplete/M02_Doc_Migrate.py
T
eomsangdonandClaude Opus 5.5 9c803024e0 feat(807-1): 엑셀 불러오기 = 수식 문서 · 단가산출 · 일위대가 하나씩 개인 층으로 · 원문 패널 기초 찾기 + 「어디서 왔나」 카드
- 임도품셈 판 대응표 CSV 새 자리(STmate/old/30_원자료) — 불러오기 시험 26 실패 → 0
- 가져온 식 SK = 수식 문서(멈춘 wip/802-4b-1 M02_Doc_Import 를 메인 판에 다시 얹음 · 빈 산근 = 엑셀 0 원 줄) —
  줄 쌓기 「수식」 탭 길 열림 · 셈 = 단가기준(엑셀 값 기억 | 연결 값) · 화면 캐시 기록 · 수식 길 저장 `단가기준`
- 1,021 블록 전수(실무 여덟 + 표본 밖 셋) 엑셀 검산 · 엑셀 전후 · 우리 전후 모두 같음 · 셈됨 줄지 않음
- 옮기기 도구 「가져온식」 상태 걷음(가져온 줄 문서로 대조)
- [내 일위대가로] = 호표 하나 + 하위 → 개인 층 SC · 부르는 단산 = 가져온 식 SK(같은 내용 = 있는 것 · 못 이은 줄 = 고정)
- [내 단가산출로] = 단산 블록 판 → 개인 층 가져온 식 SK(개인 층이면 [가져온 식으로 열기] 숨김)
- 원문 패널(M01 로직 · M02 단가산출) 맨 위 「단가산출 기초 찾기」 · 가져온 식 SK 첫 화면 = 「어디서 왔나」 카드
  (프로그램 · 파일 · 블록 · 행 · 가져온 때 · 표지 · 엑셀 글의 품셈 절 글만)
- 수식 탭 [엑셀 단가 그대로 | 우리 단가로](단가산출 화면 · 저장 뒤 페이지 문서 단가기준 맞춤)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:11:09 +09:00

233 lines
11 KiB
Python

"""옛 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 <xlsx> 목록 파일(기본 `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))