- 문서 칸 `수식`(옛 `사본` 은 802-5 까지 옆에) = 값 사본(로직 · 자원 단가 줄) + 부름 닫힌 묶음 + SK 문서 · 끊음 · 따로 — `M02_Doc_Copy`(새)
· 부름 문서 = 값 줄이 지금 M01 줄과 같으면 M01 수식 문서 · 다르면 만든 때 줄 변환
- SK 금액 = 문서 셈(`M02_Doc_Sk.on_run`) — 옛 칸(덮개 · 더한줄 …)은 파생(엔진에 넣던 고친 줄 → 문서 · 덮개 없는 바탕 = M01 문서 사본) · 수식 길 고침 = 따로(옛 칸 고침 저장 409 · 재생성 = 파생으로)
- 일위대가 · SC 로직 줄 = 문서 셈(`M02_Labor_Copy.line`) · 줄 옵션 = 문서 규칙(켜쌓기율 변수 r · 고른 자재 = 요소 키 · 별도계상 = 합밖 + 뒤 「재료비의 %」 밑엔 그 줄 금액 더함)
- [단가 갱신] = 자원 단가 줄만(문서 그대로) · [재생성] = 지금 M01 문서로(고친 문서 · 끊음 버림 알림) · 빈 SK = 빈 문서 「—」
- 수식 길 GET/POST …/sangeun/{키}/doc · /preview · /save(층 쓰기 · 서버 정본 셈) — `M02_Doc_Sk_Path` · `M02_Doc_Router`(새 · main 등록)
- B09 구조물 · 서고 사본 = `수식.json` 함께 굳힘(`M02_Doc_B09` · 원본판 안 듦) · B09 셈 그대로(문서 셈 전환 = 802-6)
- 옮기기 `M02_Doc_Migrate`(보기만 · --apply --backup · --restore) · 목록 `docs/raw/요구_1104/_목록_802-4a_M02대조.xlsx` — 층 SK 15 · 일위대가 21 · SC 4 · B09 서고 사본 3 같음 · 빈 SK 2 = 옛 0 → 「—」 · 가져온 식 1 = 802-4b
- 시험 test_802_4a(12) · test_204_2 서고 사본 파일 목록에 수식.json · test_795_1 `_fake` 가 M01 수식 문서도 같이 바꿈(M01 정본 = 수식 문서 — 옛 줄만 바꾼 흉내는 문서 사본에 안 닿음) · 옛 SK 흉내 = 사본 · 수식 둘 다 뗌
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
234 lines
11 KiB
Python
234 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 = 802-4b(그대로) · 바탕 참조 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, "상태": "있음"}
|
|
if kind == "sangeun" and doc.get("가져온"):
|
|
return {**head, "상태": "가져온식", "까닭": "엑셀 가져온 식 = 802-4b 몫(옛 길 그대로)"}
|
|
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("더한줄")):
|
|
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))
|