Files
Aislo/M02_MasterTemplete/M02_Labor_SkRow.py
T
eomsangdonandClaude Opus 5.5 4673fd89ab feat(811-1): 일위대가 단가산출 줄 = SK 참조 한 행 · M01 기초 = 그 층 SK 사본 · 옮기기 도구
- 저장 · 읽기 · 복사 때 SC 안 SK 참조 줄 안 펼침(Library_Flat flat_doc sk=False) · 셈 = SK 1 단위 × 수량
- [로직 추가] M01 기초 = 그 층 SK 사본 참조(POST sangeun/from-logic · 같은 내용 물음) · 다른 층 SK = 이 층 사본 뒤 참조(Labor_SkPick)
- 프로젝트 · 셈 주입 = 옛 로직 줄 그대로
- 옮기기 M02_Labor_SkRow — 층 SC · 구조물 조합 로직 줄 → SK + 참조 한 행 · 면적 칸 잇기 옮김 · 줄마다 금액 같을 때만 · 보기 = 임시 사본
- 낡은 시험 고침: test_740_1 넷(SC 안 SK 줄 펼침 단언 → 참조 한 행) · test_723 AlsoSk(808-1 에서 걷힌 단추 단언)
- 새 시험 test_811_1_labor_ref

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

290 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""일위대가 단가산출 줄 = 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"]))