206 lines
8.6 KiB
Python
206 lines
8.6 KiB
Python
"""M02 층 집계표 항목 — 회사 · 개인 층 빈 뼈대 · 항목 목록 · 항목 복사(PLAN 21-1 · 21-2).
|
|
|
|
항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) · 자재조합(UM) ·
|
|
도번 SD · 면적번 SA · 재료번 SM.
|
|
층 폴더(시스템 · 회사 · 개인)는 모양이 같아 저장소 함수를 `store.at(층 폴더)` 안에서 그대로 부름.
|
|
권한은 부르는 쪽(`M02_MasterTemplete_Router_Items.py`) 몫 — 여기는 폴더와 파일만.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from common_util.common_util_json import atomic_write_json
|
|
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
|
|
from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store
|
|
from M02_MasterTemplete import M02_MasterTemplete_Matset as matset
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
|
|
# 21-1 목록 칸 — SS 줄에서 그대로(키는 `key`)
|
|
ITEM_FIELDS = (
|
|
"열",
|
|
"경로",
|
|
"이름",
|
|
"종류",
|
|
"분류1",
|
|
"분류2",
|
|
"분류3",
|
|
"단위",
|
|
"일위대가",
|
|
"자재조합",
|
|
"도번",
|
|
"면적번",
|
|
"재료번",
|
|
)
|
|
|
|
|
|
def ensure(folder: Path) -> bool:
|
|
"""회사 · 개인 층 첫 진입 — 빈 집계표(열 0 · 층 이름 · 줄 머리는 시스템 것) ·
|
|
조합 장부(`다음` 1) · 자재 조합 빈 표 · 종류 폴더.
|
|
이미 집계표가 있으면 아무것도 안 함(시스템 층은 늘 있음)."""
|
|
table = Path(folder) / "table" / f"{store.STRUCTURE_TABLE}.json"
|
|
if table.is_file():
|
|
return False
|
|
system = json.loads((store.FOLDER / "table" / f"{store.STRUCTURE_TABLE}.json").read_bytes())
|
|
doc = {
|
|
**system,
|
|
"열": [],
|
|
"줄": [{**r, "값": {}} for r in system.get("줄", []) if isinstance(r, dict)],
|
|
"합계줄": [],
|
|
"보기": {},
|
|
}
|
|
with store.at(folder):
|
|
for kind in store.KINDS:
|
|
(Path(folder) / kind).mkdir(parents=True, exist_ok=True)
|
|
with store._LOCK:
|
|
if table.is_file(): # 그 사이 다른 요청이 만듦
|
|
return False
|
|
atomic_write_json(table, doc)
|
|
store.sync_combos()
|
|
mat = Path(folder) / matset.FILE
|
|
if not mat.is_file():
|
|
atomic_write_json(mat, matset._load()[0])
|
|
return True
|
|
|
|
|
|
def items(folder: Path) -> list[dict[str, Any]]:
|
|
"""그 층 항목 — SS 줄마다 `{key, 열, 경로, 이름, 종류, 분류1~3, 단위, 일위대가, 자재조합,
|
|
도번, 면적번, 재료번, names}` · 경로 = 폴더 이름 배열 · 이름 = 경로 이음(26-1 · 종류 ·
|
|
분류1~3 은 경로 앞 넷) · `names` = 화면 이름(키 대신 · 없으면 null) —
|
|
일위대가 = 조합 이름 · 자재조합 = 그 층 자재 조합 이름 ·
|
|
도번 · 면적번 · 재료번 = 항목(열) 이름."""
|
|
with store.at(folder):
|
|
ua = combos.names() # 마스터 · 이 층이 쓸 수 있는 회사 · 개인 조합(27-2c)
|
|
rows = store.read_combos().get("줄", [])
|
|
um = {str(r.get("키")): str(r.get("이름") or "") for r in matset.listing()["줄"]}
|
|
out = []
|
|
for r in rows:
|
|
head = r.get("이름")
|
|
names = {
|
|
"일위대가": ua.get(str(r.get("일위대가"))) if r.get("일위대가") else None,
|
|
"자재조합": um.get(str(r.get("자재조합"))) if r.get("자재조합") else None,
|
|
**{
|
|
k: (head or r.get("열")) if r.get(k) else None for k in ("도번", "면적번", "재료번")
|
|
},
|
|
}
|
|
out.append(
|
|
{
|
|
"key": r.get("키"),
|
|
**{k: r.get(k) for k in ITEM_FIELDS},
|
|
"names": names,
|
|
**_list_extras(Path(folder), str(r.get("열"))),
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def _list_extras(folder: Path, col: str) -> dict[str, Any]:
|
|
"""목록 칸(39-5) — `수정일` = 도면 · 면적산출 · 일위대가 파일 시각 · `labor` = 일위대가 문서의
|
|
번호 · 담은 로직 수 · 합계(`계산값.반영합.계` · 없으면 null) — 파일이 없으면 칸 없음."""
|
|
stamps: dict[str, str] = {}
|
|
for kind in ("structure", "area", "labor"):
|
|
path = folder / kind / f"{col}.json"
|
|
if path.is_file():
|
|
stamps[kind] = datetime.fromtimestamp(path.stat().st_mtime).isoformat(
|
|
timespec="seconds"
|
|
)
|
|
labor = None
|
|
path = folder / "labor" / f"{col}.json"
|
|
if path.is_file():
|
|
try:
|
|
doc = json.loads(path.read_bytes())
|
|
except ValueError:
|
|
doc = {}
|
|
total = ((doc.get("계산값") or {}).get("반영합") or {}).get("계")
|
|
try:
|
|
total = None if total is None or isinstance(total, bool) else float(total)
|
|
except (TypeError, ValueError):
|
|
total = None # 수 아닌 값(옛 꼴)은 「—」
|
|
labor = {
|
|
"번호": doc.get("일위대가번"),
|
|
"로직수": len(doc.get("담은로직") or []),
|
|
"합계": total,
|
|
}
|
|
return {"수정일": stamps, "labor": labor}
|
|
|
|
|
|
def _fresh_id(want: str, used: set[str]) -> str:
|
|
out, n = want, 2
|
|
while out in used:
|
|
out, n = f"{want}_{n}", n + 1
|
|
return out
|
|
|
|
|
|
def copy_item(
|
|
source: Path,
|
|
target: Path,
|
|
key: str,
|
|
origin: dict[str, Any],
|
|
path: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""다른 층 항목(SS `key`) → `target` 층 새 열 하나 — 머리 · 단위 · 바인딩 그대로 ·
|
|
도면 · 일위대가(새 SU · 조합 키는 안 받음 · 39-1b) · 면적산출 · 재료산출 내용(알약 연결 포함) ·
|
|
항목 연결 · 자재 조합 줄(새 UM) 복사 ·
|
|
새 번호 · 새 SS 키 ·
|
|
열 `본뜬` = `origin`(원래 층 · 주인 · 키) · `path` = 새 자리(폴더 + 이름 · 없으면 원래 경로 ·
|
|
붙여넣기 PLAN 30-2). 돌려줌 `{key, 열, 이름}`."""
|
|
with store.at(source):
|
|
row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도
|
|
if row is None:
|
|
raise store.StoreError(404, f"없는 항목 「{key}」")
|
|
col_id = row["열"]
|
|
col = next(c for c in store._structure_columns() if c["id"] == col_id)
|
|
docs = {}
|
|
for kind, _, _, _ in store.PARTS:
|
|
try:
|
|
docs[kind] = store.read(kind, col_id)["문서"]
|
|
except store.StoreError:
|
|
pass # 없는 파일은 새 층에서 빈 문서로 섬
|
|
um = None
|
|
if col.get("자재조합"):
|
|
try:
|
|
um = matset.one(col["자재조합"])
|
|
except store.StoreError:
|
|
um = None # 가리키던 줄이 없으면 비움
|
|
ensure(target)
|
|
with store.at(target):
|
|
new_um = None
|
|
if um:
|
|
names = matset.listing()["줄"]
|
|
taken = {str(r.get("이름") or "") for r in names}
|
|
name, n = str(um.get("이름") or key), 2
|
|
base = name
|
|
while name in taken:
|
|
name, n = f"{base} ({n})", n + 1
|
|
got = {k: um.get(k) for k in ("담은자재", "소유", "비고")}
|
|
new_um = matset.create({**got, "이름": name, "본뜬키": None})["키"]
|
|
table = store.read("table", store.STRUCTURE_TABLE)
|
|
doc = table["문서"]
|
|
new_id = _fresh_id(col_id, {c.get("id") for c in doc.get("열", []) if isinstance(c, dict)})
|
|
made = {
|
|
**json.loads(json.dumps(col)),
|
|
"id": new_id,
|
|
"일위대가": None, # 정본 = 일위대가 문서(아래 복사)
|
|
"자재조합": new_um,
|
|
"본뜬": origin,
|
|
}
|
|
if path:
|
|
made.update({"경로": list(path), "머리": list(path)}) # 머리 줄 수는 저장 맞춤이 채움
|
|
doc["열"] = [*doc.get("열", []), made]
|
|
store.write("table", store.STRUCTURE_TABLE, table["판"], doc) # 빈 세 파일 · 번호 · SS
|
|
for kind, got in docs.items():
|
|
number = store._KEY[kind]
|
|
have = store.read(kind, new_id)["문서"]
|
|
body = {k: v for k, v in got.items() if k != "계산값"}
|
|
body = {**body, "열": new_id, number: have.get(number)}
|
|
body = store._server(kind, body)
|
|
with store._LOCK:
|
|
atomic_write_json(store._path(kind, new_id), body)
|
|
store._refresh_labor(new_id) # 면적이 일위대가 뒤에 서므로 칸 값으로 다시
|
|
ss = next(r for r in store.read_combos()["줄"] if r.get("열") == new_id)
|
|
inputs_store.copy_between(source, target, col_id, new_id) # 항목 연결도 같이(판 1)
|
|
return {"key": ss["키"], "열": new_id, "이름": ss["이름"]}
|