"""일위대가 단가산출 줄 = SK 참조 한 행(PLAN 811-1 · 요구 707-1 · 681 · 698). 일위대가(SC `precombo`) · 구조물 조합 일위대가(`labor`)의 단가산출 줄 = `{참조: {층, 주인, SK}, 별칭, 메모, 수량}` 한 행 · 저장 때 펼치지 않음(`M02_Library_Flat.flat_doc` SC = `sk=False`) · 셈 = 그 SK 1 단위 × 수량(`M02_Labor_Expand`) · 줄 누름 = 그 SK 「수식」 모달(화면 `Labor_SkEdit`). M01 로직 줄(옛 저장본 · 740-1 로 펼쳐 담은 줄) → 그 층 SK 문서(복사해 내 것 · 681): 몸 = `M02_Labor_Patch.to_sangeun`(바탕 로직 · 입력값 · 고름 · 자재표시 · 켜쌓기율 · 패치 덮개 · 뺀줄 · 더한줄) · 이름 = 별칭 › 로직 이름 · 단위 = 바탕 기준단위(앞 수 뗌 · 원/10㎡ → ㎡ = 일위대가 로직 줄 ÷ 배와 같은 값) · 자재표시 빈 재료 = 일위대가 기본(별도계상)을 적어 둠(SK 빈 칸 = 직접계산이라 금액이 바뀜) · 사본 · 수식 = 그 일위대가 것을 실어 보냄(같은 값 묶음 · M01 이 바뀌었어도 금액 그대로) 옮기기(`migrate` · 한 번) = 층(마스터 · 회사 · 개인) SC · 구조물 조합 일위대가의 로직 줄 → SK + 참조 한 행 · 같은 몸 · 같은 사본 = 한 층 안 SK 하나 공유 · 면적산출 칸 잇기(수량 빈 줄) = 옛 알약 키 → 참조 알약 키 · 전후 금액(원 단위 · 줄마다) 같을 때만 씀 · 다르면 공유 없이 한 번 더 · 그래도 다르면 안 옮기고 목록 · 프로젝트(B09 사본)는 손대지 않음 · 보기 = 임시 storage 사본에서 실제로 옮겨 봄(공유 storage 안 씀). `python -m M02_MasterTemplete.M02_Labor_SkRow [--apply] [--backup <폴더>] [--xlsx <파일>]` """ from __future__ import annotations import json import shutil from contextlib import contextmanager from decimal import Decimal from pathlib import Path from typing import Any from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Template_Layers as layers KINDS = ("precombo", "labor") CARRY = ("사본", "수식") # 일위대가 값 묶음 → 새 SK(같은 금액) def _whole() -> Any: from M01_MasterData import M01_MasterData_Store as m01 return m01.loaded()[1] def logic_doc(one: dict, doc: dict, whole: Any) -> dict[str, Any]: """로직 줄 `one`(일위대가 `doc` 안) → SK 문서 몸(키 없음 · 저장은 부르는 쪽).""" from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Patch as patch from M02_MasterTemplete import M02_Sangeun_Calc as calc code = str(one["로직"]) row = whole.logic(code) shown = dict(one.get("자재표시") or {}) for n, item in enumerate(row.get("호표") or []): if item.get("종류") == "재료": mine = shown.get(str(n)) shown[str(n)] = {**labor.marks(mine), "별도계상": labor.mark_of(mine, "별도계상")} unit = calc.base_unit(row) name = str(one.get("별칭") or row.get("이름") or code) sk = patch.to_sangeun({**one, "자재표시": shown}, name, unit[1] if unit else "식", whole) return {**sk, **{k: doc[k] for k in CARRY if doc.get(k)}} def from_logic(code: Any, new: bool = False, use: str | None = None) -> dict[str, Any]: """[+ 줄] M01 기초 고름 — 지금 층에 SK 사본(같은 내용 = 409 물음 · 733-2) → `{이름: SK 키, 참조}`.""" from M02_MasterTemplete import M02_Library_Ask as ask whole = _whole() try: whole.logic(str(code)) except Exception as e: # noqa: BLE001 — 없는 로직 raise store.StoreError(400, f"없는 기초 「{code}」") from e sk = logic_doc({"로직": str(code), "입력값": {}}, {}, whole) key = ask.ask("sangeun", sk, new, use) or refs.create("sangeun", sk)["이름"] at = refs.here() return {"이름": key, "참조": {"층": at[0], "주인": at[1], "키": key}} def ref_line(one: dict, ref: refs.Ref) -> dict[str, Any]: """옛 로직 줄 → 참조 한 행(별칭 · 메모 · 수량 그대로).""" return {"참조": ref, **{k: one.get(k) for k in ("별칭", "메모", "수량")}} def _finger(sk: dict) -> str: """한 층 안 공유 열쇠 — 몸 + 사본 판(값 묶음이 같아야 같은 금액).""" copy = sk.get("사본") or {} body = {k: v for k, v in sk.items() if k not in CARRY} tag = (copy.get("판"), copy.get("단가판")) if isinstance(copy, dict) else None return json.dumps([body, tag], ensure_ascii=False, sort_keys=True, default=str) def convert( doc: dict, made: dict[str, str] | None, whole: Any ) -> tuple[dict, dict[str, str], list]: """지금 층(`store.at` 안) 문서의 로직 줄 → SK(만들거나 `made` 에서 공유) + 참조 한 행 · 답 = (새 문서, {옛 알약 키: 새 알약 키}(수량 빈 줄), [만든 SK 키]).""" from M01_MasterData import M01_MasterData_Store_Combo as combo_store lines = [x for x in doc.get("담은로직") or [] if isinstance(x, dict)] pills = combo_store.combo_pills({"담은로직": lines})["물량"] by_logic = {code: p["키"] for p in pills for code in p.get("로직") or []} at, out, moved, new = refs.here(), [], {}, [] for one in doc.get("담은로직") or []: if not (isinstance(one, dict) and one.get("로직")): out.append(one) continue sk = logic_doc(one, doc, whole) key = (made or {}).get(_finger(sk)) if key is None: key = refs.create("sangeun", sk)["이름"] new.append(key) if made is not None: made[_finger(sk)] = key ref = {"층": at[0], "주인": at[1], "키": key} out.append(ref_line(one, ref)) if one.get("수량") is None and by_logic.get(one["로직"]): moved.setdefault(by_logic[one["로직"]], []).append(refs.ref_id(ref)) return {**doc, "담은로직": out}, moved, new def relink(area: dict, moved: dict[str, list[str]]) -> dict: """면적산출 칸 잇기 — 옛 로직 알약 키 칸을 새 참조 알약 키에도(같은 칸 · 옛 잇기는 뗌).""" sheets = [] for sheet in area.get("시트") or []: links = [] for link in sheet.get("연결") or []: keys = moved.get(str(link.get("키"))) if isinstance(link, dict) else None links += [{**link, "키": k} for k in keys] if keys else [link] sheets.append({**sheet, "연결": links} if "연결" in sheet else sheet) return {**area, "시트": sheets} # ── 금액(전후 대조) ──────────────────────────────────────────────────── def _won(v: Any) -> Any: return None if v is None else int(Decimal(str(v))) def _unit(line: dict) -> Any: """줄 1 단위 값(기준 ÷ 배 · 0.01 원).""" got = (line.get("기준") or {}).get("계") times = Decimal(str(line.get("배") or 1)) return None if got is None else str((Decimal(str(got)) / times).quantize(Decimal("0.01"))) def money(kind: str, key: str, doc: dict) -> dict[str, Any]: """지금 층 문서 금액 — 조합 = 줄마다 반영 · 합(면적산출 칸 값) · SC = 1 단위(안 줄 저장 수량 · 빈 수량 = 1).""" from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete.M02_Labor_Expand import Expand, inner_pills if kind == "labor": # 기준합 = 줄마다 단위가 달라(로직 줄 결과단위 · 참조 줄 1 단위) 안 견줌 got = labor.calc(doc) rows = [[_won((x.get("반영") or {}).get("계")), _unit(x)] for x in got.get("줄") or []] return {"줄": rows, "합": _won((got.get("반영합") or {}).get("계"))} at = refs.here() ref = {"층": at[0], "주인": at[1], "키": key} given = {p["키"]: 1 for p in inner_pills(ref)} out = {} for name, use in (("저장", {}), ("하나", given)): base = Expand(_whole(), refs.read_ref, use)(ref).get("기준") or {} out[name] = _won(base.get("계")) return out def _path(kind: str, key: str) -> Path: return store.root() / kind / f"{key}.json" def _write(path: Path, doc: dict) -> None: path.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8") def _one(kind: str, path: Path, made: dict[str, str] | None, whole: Any) -> dict[str, Any]: """문서 하나 옮김(지금 층에 씀) — 전후 금액 · 다르면 되돌림(만든 SK 지움 · 파일 · 면적 원래대로).""" raw = path.read_bytes() doc = json.loads(raw) key = path.stem before = money(kind, key, doc) area_path = _path("area", str(doc.get("열") or key)) if kind == "labor" else None area_raw = area_path.read_bytes() if area_path and area_path.is_file() else None got, moved, new = convert(doc, made, whole) path.write_text(json.dumps(got, ensure_ascii=False, indent=2), encoding="utf-8") if area_raw is not None and moved: _write(area_path, relink(json.loads(area_raw), moved)) after = money(kind, key, got) same = before == after if not same: # 되돌림 — 이번에 만든 SK 지움(공유 기억도) path.write_bytes(raw) if area_raw is not None: area_path.write_bytes(area_raw) for sk in new: _path("sangeun", sk).unlink(missing_ok=True) if made is not None: for f in [f for f, k in made.items() if k in new]: made.pop(f) sks = {r["키"] for r in refs.refs_in(got) if str(r.get("키"))[:2] == "SK"} if same else set() return { "줄": sum(1 for x in doc.get("담은로직") or [] if isinstance(x, dict) and x.get("로직")), "SK": sorted(sks), "새SK": new if same else [], "알약": sum(len(v) for v in moved.values()) if area_path else 0, "전": before, "후": after, "같음": same, } def run_here(whole: Any = None) -> list[dict[str, Any]]: """지금 설정 storage 의 층 문서 전부 옮김(쓰기) — 줄마다 결과.""" whole, out = whole or _whole(), [] shared: dict[tuple, dict[str, str]] = {} for kind in KINDS: for where, path in layers.layer_files(kind): try: doc = json.loads(path.read_bytes()) except (OSError, ValueError): continue if not any(isinstance(x, dict) and x.get("로직") for x in doc.get("담은로직") or []): continue folder = path.parent.parent made = shared.setdefault(refs.where(folder), {}) with store.at(folder): row = _one(kind, path, made, whole) if not row["같음"]: # 공유 없이 한 번 더 row = {**_one(kind, path, None, whole), "공유없음": True} out.append({"어디": where, "종류": kind, "키": path.stem, **row}) return out @contextmanager def _sandbox(root: Path): """층 폴더(마스터 · 회사 · 개인 templates)만 임시 storage 로 복사 → 설정을 그리로.""" from config import config_system old = (config_system.STORAGE_BASE_DIR, layers.SYSTEM_ROOT, store.FOLDER) src = layers.storage_root() shutil.copytree(layers.system_dir(), root / "system") for tpl in [*src.glob("*/templates"), *src.glob("*/*/templates")]: shutil.copytree(tpl, root / "storage" / tpl.relative_to(src)) config_system.STORAGE_BASE_DIR = str(root / "storage") layers.SYSTEM_ROOT = store.FOLDER = root / "system" try: yield finally: config_system.STORAGE_BASE_DIR, layers.SYSTEM_ROOT, store.FOLDER = old def migrate(apply: bool = False, backup: Path | None = None, work: Path | None = None) -> list: """보기(`apply` False) = 임시 storage 에서 옮겨 봄 · 씀 = `backup` 에 층 폴더 사본을 떠 둔 뒤 실제 층.""" if not apply: import tempfile with tempfile.TemporaryDirectory(dir=work) as tmp, _sandbox(Path(tmp)): return run_here() if backup is None: raise SystemExit("--apply 는 --backup <폴더> 와 함께") src = layers.storage_root() shutil.copytree(layers.system_dir(), backup / "system") for tpl in [*src.glob("*/templates"), *src.glob("*/*/templates")]: shutil.copytree(tpl, backup / "storage" / tpl.relative_to(src)) return run_here() def to_xlsx(rows: list[dict], path: Path) -> None: from openpyxl import Workbook wb = Workbook() ws = wb.active ws.title = "옮기기" ws.append(["어디", "종류", "키", "로직 줄", "참조 SK", "새 SK", "알약 잇기", "전", "후", "같음", "공유없음"]) # fmt: skip for r in rows: ws.append( [r["어디"], r["종류"], r["키"], r["줄"], " ".join(r["SK"]), len(r["새SK"]), r["알약"]] + [json.dumps(r[k], ensure_ascii=False) for k in ("전", "후")] + ["같음" if r["같음"] else "다름", "예" if r.get("공유없음") else ""] ) path.parent.mkdir(parents=True, exist_ok=True) wb.save(path) if __name__ == "__main__": # 브레인 — 보기 먼저 · `--apply --backup <폴더>` 는 승인 뒤 import sys argv = sys.argv[1:] opt = {a: argv[i + 1] for i, a in enumerate(argv[:-1]) if a in ("--backup", "--xlsx")} got = migrate("--apply" in argv, Path(opt["--backup"]) if "--backup" in opt else None) for one in got: print(json.dumps(one, ensure_ascii=False, default=str)) lines, same = sum(r["줄"] for r in got), sum(r["줄"] for r in got if r["같음"]) sks = len({(r["어디"], k) for r in got for k in r["새SK"]}) print(f"문서 {len(got)} · 로직 줄 {lines} · 옮김 {same} · 새 SK {sks} · 다름 {sum(not r['같음'] for r in got)}") # fmt: skip if "--xlsx" in opt: to_xlsx(got, Path(opt["--xlsx"]))